From 72c982ab7be2ac943b031197738ff84e5fb81a8c Mon Sep 17 00:00:00 2001 From: VAN BOSSUYT Nicolas Date: Thu, 30 Nov 2023 13:30:50 +0100 Subject: [PATCH] Added support for nested commands and running cutekit in containers. --- cutekit/__init__.py | 12 +- cutekit/builder.py | 273 ++++++++++++------- cutekit/cli.py | 54 +++- cutekit/const.py | 2 +- cutekit/entrypoint.sh | 20 ++ cutekit/graph.py | 2 +- cutekit/model.py | 48 ++-- cutekit/pods.py | 201 ++++++++++++++ requirements.txt => cutekit/requirements.txt | 1 + cutekit/rules.py | 41 +-- cutekit/shell.py | 29 +- pyproject.toml | 4 +- tests/test_resolver.py | 16 +- 13 files changed, 506 insertions(+), 197 deletions(-) create mode 100755 cutekit/entrypoint.sh create mode 100644 cutekit/pods.py rename requirements.txt => cutekit/requirements.txt (80%) diff --git a/cutekit/__init__.py b/cutekit/__init__.py index 1c9eabc..48ad72c 100644 --- a/cutekit/__init__.py +++ b/cutekit/__init__.py @@ -9,6 +9,7 @@ from . import ( graph, # noqa: F401 this is imported for side effects model, plugins, + pods, # noqa: F401 this is imported for side effects vt100, ) @@ -55,10 +56,13 @@ def setupLogger(verbose: bool): def main() -> int: try: - a = cli.parse(sys.argv[1:]) - setupLogger(a.consumeOpt("verbose", False) is True) - plugins.loadAll() - cli.exec(a) + args = cli.parse(sys.argv[1:]) + setupLogger(args.consumeOpt("verbose", False) is True) + safemode = args.consumeOpt("safemode", False) is True + if not safemode: + plugins.loadAll() + pods.reincarnate(args) + cli.exec(args) print() return 0 except RuntimeError as e: diff --git a/cutekit/builder.py b/cutekit/builder.py index b50fbff..61b33d7 100644 --- a/cutekit/builder.py +++ b/cutekit/builder.py @@ -3,32 +3,89 @@ import logging import dataclasses as dt from pathlib import Path -from typing import TextIO, Union +from typing import Callable, TextIO, Union from . import shell, rules, model, ninja, const, cli _logger = logging.getLogger(__name__) -def aggregateCincs(target: model.Target, registry: model.Registry) -> set[str]: +@dt.dataclass +class TargetScope: + registry: model.Registry + target: model.Target + + @staticmethod + def use(args: cli.Args) -> "TargetScope": + registry = model.Registry.use(args) + target = model.Target.use(args) + return TargetScope(registry, target) + + def openComponentScope(self, c: model.Component): + return ComponentScope(self.registry, self.target, c) + + +@dt.dataclass +class ComponentScope(TargetScope): + component: model.Component + + def openComponentScope(self, c: model.Component): + return ComponentScope(self.registry, self.target, c) + + def openProductScope(self, path: Path): + return ProductScope(self.registry, self.target, self.component, path) + + +@dt.dataclass +class ProductScope(ComponentScope): + path: Path + + +# --- Variables -------------------------------------------------------------- # + +Compute = Callable[[TargetScope], str] +_vars: dict[str, Compute] = {} + + +def var(name: str) -> Callable[[Compute], Compute]: + def decorator(func: Compute): + _vars[name] = func + return func + + return decorator + + +@var("buildir") +def _computeBuildir(scope: TargetScope) -> str: + return scope.target.builddir + + +@var("hashid") +def _computeHashid(scope: TargetScope) -> str: + return scope.target.hashid + + +@var("cincs") +def _computeCinc(scope: TargetScope) -> str: res = set() - for c in registry.iterEnabled(target): + for c in scope.registry.iterEnabled(scope.target): if "cpp-root-include" in c.props: res.add(c.dirname()) elif c.type == model.Kind.LIB: res.add(str(Path(c.dirname()).parent)) - return set(map(lambda i: f"-I{i}", res)) + return " ".join(set(map(lambda i: f"-I{i}", res))) -def aggregateCdefs(target: model.Target) -> set[str]: +@var("cdefs") +def _computeCdef(scope: TargetScope) -> str: res = set() def sanatize(s: str) -> str: return s.lower().replace(" ", "_").replace("-", "_").replace(".", "_") - for k, v in target.props.items(): + for k, v in scope.target.props.items(): if isinstance(v, bool): if v: res.add(f"-D__ck_{sanatize(k)}__") @@ -36,35 +93,45 @@ def aggregateCdefs(target: model.Target) -> set[str]: res.add(f"-D__ck_{sanatize(k)}_{sanatize(str(v))}__") res.add(f"-D__ck_{sanatize(k)}_value={str(v)}") - return res + return " ".join(res) -def buildpath(target: model.Target, component: model.Component, path) -> Path: - return Path(target.builddir) / component.id / path +def buildpath(scope: ComponentScope, path) -> Path: + return Path(scope.target.builddir) / scope.component.id / path # --- Compilation ------------------------------------------------------------ # -def wilcard(component: model.Component, wildcards: list[str]) -> list[str]: - dirs = [component.dirname()] + list( - map(lambda d: os.path.join(component.dirname(), d), component.subdirs) - ) - return shell.find(dirs, list(wildcards), recusive=False) +def subdirs(scope: ComponentScope) -> list[str]: + registry = scope.registry + target = scope.target + component = scope.component + result = [component.dirname()] + + for subs in component.subdirs: + result.append(os.path.join(component.dirname(), subs)) + + for inj in component.resolved[target.id].injected: + injected = registry.lookup(inj, model.Component) + assert injected is not None # model.Resolver has already checked this + result.extend(subdirs(scope)) + + return result + + +def wilcard(scope: ComponentScope, wildcards: list[str]) -> list[str]: + return shell.find(subdirs(scope), list(wildcards), recusive=False) def compile( - w: ninja.Writer, - target: model.Target, - component: model.Component, - rule: str, - srcs: list[str], + w: ninja.Writer, scope: ComponentScope, rule: str, srcs: list[str] ) -> list[str]: res: list[str] = [] for src in srcs: - rel = Path(src).relative_to(component.dirname()) - dest = buildpath(target, component, "obj") / rel.with_suffix(".o") - t = target.tools[rule] + rel = Path(src).relative_to(scope.component.dirname()) + dest = buildpath(scope, "obj") / rel.with_suffix(".o") + t = scope.target.tools[rule] w.build(str(dest), rule, inputs=src, order_only=t.files) res.append(str(dest)) return res @@ -79,13 +146,12 @@ def listRes(component: model.Component) -> list[str]: def compileRes( w: ninja.Writer, - target: model.Target, - component: model.Component, + scope: ComponentScope, ) -> list[str]: res: list[str] = [] - for r in listRes(component): - rel = Path(r).relative_to(component.subpath("res")) - dest = buildpath(target, component, "res") / rel + for r in listRes(scope.component): + rel = Path(r).relative_to(scope.component.subpath("res")) + dest = buildpath(scope, "res") / rel w.build(str(dest), "cp", r) res.append(str(dest)) return res @@ -94,50 +160,55 @@ def compileRes( # --- Linking ---------------------------------------------------------------- # -def outfile(target: model.Target, component: model.Component) -> str: - if component.type == model.Kind.LIB: - return str(buildpath(target, component, f"lib/{component.id}.a")) +def outfile(scope: ComponentScope) -> str: + if scope.component.type == model.Kind.LIB: + return str(buildpath(scope, f"lib/{scope.component.id}.a")) else: - return str(buildpath(target, component, f"bin/{component.id}.out")) + return str(buildpath(scope, f"bin/{scope.component.id}.out")) def collectLibs( - registry: model.Registry, target: model.Target, component: model.Component + scope: ComponentScope, ) -> list[str]: res: list[str] = [] - for r in component.resolved[target.id].resolved: - req = registry.lookup(r, model.Component) + for r in scope.component.resolved[scope.target.id].required: + req = scope.registry.lookup(r, model.Component) assert req is not None # model.Resolver has already checked this - if r == component.id: + if r == scope.component.id: continue if not req.type == model.Kind.LIB: raise RuntimeError(f"Component {r} is not a library") - res.append(outfile(target, req)) + res.append(outfile(scope.openComponentScope(req))) + return res def link( w: ninja.Writer, - registry: model.Registry, - target: model.Target, - component: model.Component, + scope: ComponentScope, ) -> str: w.newline() - out = outfile(target, component) + out = outfile(scope) objs = [] - objs += compile(w, target, component, "cc", wilcard(component, ["*.c"])) + objs += compile(w, scope, "cc", wilcard(scope, ["*.c"])) objs += compile( - w, target, component, "cxx", wilcard(component, ["*.cpp", "*.cc", "*.cxx"]) + w, + scope, + "cxx", + wilcard(scope, ["*.cpp", "*.cc", "*.cxx"]), ) objs += compile( - w, target, component, "as", wilcard(component, ["*.s", "*.asm", "*.S"]) + w, + scope, + "as", + wilcard(scope, ["*.s", "*.asm", "*.S"]), ) - res = compileRes(w, target, component) - libs = collectLibs(registry, target, component) - if component.type == model.Kind.LIB: + res = compileRes(w, scope) + libs = collectLibs(scope) + if scope.component.type == model.Kind.LIB: w.build(out, "ar", objs, implicit=res) else: w.build(out, "ld", objs + libs, implicit=res) @@ -147,32 +218,30 @@ def link( # --- Phony ------------------------------------------------------------------ # -def all(w: ninja.Writer, registry: model.Registry, target: model.Target) -> list[str]: +def all(w: ninja.Writer, scope: TargetScope) -> list[str]: all: list[str] = [] - for c in registry.iterEnabled(target): - all.append(link(w, registry, target, c)) + for c in scope.registry.iterEnabled(scope.target): + all.append(link(w, scope.openComponentScope(c))) w.build("all", "phony", all) w.default("all") return all -def gen(out: TextIO, target: model.Target, registry: model.Registry): +def gen(out: TextIO, scope: TargetScope): w = ninja.Writer(out) w.comment("File generated by the build system, do not edit") w.newline() - w.variable("builddir", target.builddir) - w.variable("hashid", target.hashid) + w.separator("Variables") + for name, compute in _vars.items(): + w.variable(name, compute(scope)) + w.newline() w.separator("Tools") - w.variable("cincs", " ".join(aggregateCincs(target, registry))) - w.variable("cdefs", " ".join(aggregateCdefs(target))) - w.newline() - - for i in target.tools: - tool = target.tools[i] + for i in scope.target.tools: + tool = scope.target.tools[i] rule = rules.rules[i] w.variable(i, tool.cmd) w.variable(i + "flags", " ".join(rule.args + tool.args)) @@ -185,53 +254,40 @@ def gen(out: TextIO, target: model.Target, registry: model.Registry): w.separator("Build") - all(w, registry, target) - - -@dt.dataclass -class Product: - path: Path - target: model.Target - component: model.Component + all(w, scope) def build( - target: model.Target, - registry: model.Registry, + scope: TargetScope, components: Union[list[model.Component], model.Component, None] = None, -) -> list[Product]: +) -> list[ProductScope]: all = False - shell.mkdir(target.builddir) - ninjaPath = os.path.join(target.builddir, "build.ninja") + shell.mkdir(scope.target.builddir) + ninjaPath = os.path.join(scope.target.builddir, "build.ninja") if not os.path.exists(ninjaPath): with open(ninjaPath, "w") as f: - gen(f, target, registry) + gen(f, scope) if components is None: all = True - components = list(registry.iterEnabled(target)) + components = list(scope.registry.iterEnabled(scope.target)) if isinstance(components, model.Component): components = [components] - products: list[Product] = [] + products: list[ProductScope] = [] for c in components: - r = c.resolved[target.id] + s = scope.openComponentScope(c) + r = c.resolved[scope.target.id] if not r.enabled: raise RuntimeError(f"Component {c.id} is disabled: {r.reason}") - products.append( - Product( - path=Path(outfile(target, c)), - target=target, - component=c, - ) - ) + products.append(s.openProductScope(Path(outfile(scope.openComponentScope(c))))) outs = list(map(lambda p: str(p.path), products)) - shell.exec("ninja", "-f", ninjaPath, *(outs if not all else [])) + # shell.exec("ninja", "-f", ninjaPath, *(outs if not all else [])) return products @@ -239,60 +295,65 @@ def build( # --- Commands --------------------------------------------------------------- # -@cli.command("b", "build", "Build a component or all components") +@cli.command("b", "build", "Build/Run/Clean a component or all components") def buildCmd(args: cli.Args): - registry = model.Registry.use(args) - target = model.Target.use(args) + pass + + +@cli.command("b", "build/build", "Build a component or all components") +def buildBuildCmd(args: cli.Args): + scope = TargetScope.use(args) componentSpec = args.consumeArg() component = None if componentSpec is not None: - component = registry.lookup(componentSpec, model.Component) - build(target, registry, component)[0] + component = scope.registry.lookup(componentSpec, model.Component) + build(scope, component)[0] -@cli.command("r", "run", "Run a component") -def runCmd(args: cli.Args): - registry = model.Registry.use(args) - target = model.Target.use(args) +@cli.command("r", "build/run", "Run a component") +def buildRunCmd(args: cli.Args): + scope = TargetScope.use(args) debug = args.consumeOpt("debug", False) is True componentSpec = args.consumeArg() or "__main__" - component = registry.lookup(componentSpec, model.Component, includeProvides=True) + component = scope.registry.lookup( + componentSpec, model.Component, includeProvides=True + ) if component is None: raise RuntimeError(f"Component {componentSpec} not found") - product = build(target, registry, component)[0] + product = build(scope, component)[0] - os.environ["CK_TARGET"] = target.id + os.environ["CK_TARGET"] = product.target.id + os.environ["CK_BUILDDIR"] = product.target.builddir os.environ["CK_COMPONENT"] = product.component.id - os.environ["CK_BUILDDIR"] = target.builddir shell.exec(*(["lldb", "-o", "run"] if debug else []), str(product.path), *args.args) -@cli.command("t", "test", "Run all test targets") -def testCmd(args: cli.Args): +@cli.command("t", "build/test", "Run all test targets") +def buildTestCmd(args: cli.Args): # This is just a wrapper around the `run` command that try # to run a special hook component named __tests__. args.args.insert(0, "__tests__") - runCmd(args) + buildRunCmd(args) -@cli.command("d", "debug", "Debug a component") -def debugCmd(args: cli.Args): +@cli.command("d", "build/debug", "Debug a component") +def buildDebugCmd(args: cli.Args): # This is just a wrapper around the `run` command that # always enable debug mode. args.opts["debug"] = True - runCmd(args) + buildRunCmd(args) -@cli.command("c", "clean", "Clean build files") -def cleanCmd(args: cli.Args): +@cli.command("c", "build/clean", "Clean build files") +def buildCleanCmd(args: cli.Args): model.Project.use(args) shell.rmrf(const.BUILD_DIR) -@cli.command("n", "nuke", "Clean all build files and caches") -def nukeCmd(args: cli.Args): +@cli.command("n", "build/nuke", "Clean all build files and caches") +def buildNukeCmd(args: cli.Args): model.Project.use(args) shell.rmrf(const.PROJECT_CK_DIR) diff --git a/cutekit/cli.py b/cutekit/cli.py index 60a82e0..a291c6f 100644 --- a/cutekit/cli.py +++ b/cutekit/cli.py @@ -80,8 +80,10 @@ class Command: isPlugin: bool callback: Callback + subcommands: dict[str, "Command"] = dt.field(default_factory=dict) -commands: list[Command] = [] + +commands: dict[str, Command] = {} def command(shortName: Optional[str], longName: str, helpText: str): @@ -90,15 +92,18 @@ def command(shortName: Optional[str], longName: str, helpText: str): def wrap(fn: Callable[[Args], None]): _logger.debug(f"Registering command {longName}") - commands.append( - Command( - shortName, - longName, - helpText, - Path(calframe[1].filename).parent != Path(__file__).parent, - fn, - ) + path = longName.split("/") + parent = commands + for p in path[:-1]: + parent = parent[p].subcommands + parent[path[-1]] = Command( + shortName, + path[-1], + helpText, + Path(calframe[1].filename).parent != Path(__file__).parent, + fn, ) + return fn return wrap @@ -127,8 +132,8 @@ def helpCmd(args: Args): print() vt100.title("Commands") - for cmd in sorted(commands, key=lambda c: c.longName): - if cmd.longName.startswith("_"): + for cmd in sorted(commands.values(), key=lambda c: c.longName): + if cmd.longName.startswith("_") or len(cmd.subcommands) > 0: continue pluginText = "" @@ -139,6 +144,21 @@ def helpCmd(args: Args): f" {vt100.GREEN}{cmd.shortName or ' '}{vt100.RESET} {cmd.longName} - {cmd.helpText} {pluginText}" ) + for cmd in sorted(commands.values(), key=lambda c: c.longName): + if cmd.longName.startswith("_") or len(cmd.subcommands) == 0: + continue + + print() + vt100.title(f"{cmd.longName.capitalize()} - {cmd.helpText}") + for subcmd in sorted(cmd.subcommands.values(), key=lambda c: c.longName): + pluginText = "" + if subcmd.isPlugin: + pluginText = f"{vt100.CYAN}(plugin){vt100.RESET}" + + print( + f" {vt100.GREEN}{subcmd.shortName or ' '}{vt100.RESET} {subcmd.longName} - {subcmd.helpText} {pluginText}" + ) + print() vt100.title("Logging") print(" Logs are stored in:") @@ -151,15 +171,19 @@ def versionCmd(args: Args): print(f"CuteKit v{const.VERSION_STR}") -def exec(args: Args): +def exec(args: Args, cmds=commands): cmd = args.consumeArg() if cmd is None: raise RuntimeError("No command specified") - for c in commands: + for c in cmds.values(): if c.shortName == cmd or c.longName == cmd: - c.callback(args) - return + if len(c.subcommands) > 0: + exec(args, c.subcommands) + return + else: + c.callback(args) + return raise RuntimeError(f"Unknown command {cmd}") diff --git a/cutekit/const.py b/cutekit/const.py index 6124018..1fff81f 100644 --- a/cutekit/const.py +++ b/cutekit/const.py @@ -1,7 +1,7 @@ import os import sys -VERSION = (0, 6, 0, "dev") +VERSION = (0, 7, 0, "dev") VERSION_STR = f"{VERSION[0]}.{VERSION[1]}.{VERSION[2]}{'-' + VERSION[3] if len(VERSION) >= 4 else ''}" MODULE_DIR = os.path.dirname(os.path.realpath(__file__)) ARGV0 = os.path.basename(sys.argv[0]) diff --git a/cutekit/entrypoint.sh b/cutekit/entrypoint.sh new file mode 100755 index 0000000..08f6987 --- /dev/null +++ b/cutekit/entrypoint.sh @@ -0,0 +1,20 @@ +#!/bin/env bash + +set -e + +export PY=python3.11 + +if [ ! -d "/tools/venv" ]; then + echo "Creating virtual environment..." + + $PY -m venv /tools/venv + source /tools/venv/bin/activate + $PY -m ensurepip + $PY -m pip install -r /tools/cutekit/requirements.txt + echo "Virtual environment created." +else + source /tools/venv/bin/activate +fi + +export PYTHONPATH=/tools +$PY -m cutekit "$@" diff --git a/cutekit/graph.py b/cutekit/graph.py index 94dee83..22555bb 100644 --- a/cutekit/graph.py +++ b/cutekit/graph.py @@ -36,7 +36,7 @@ def view( if ( scopeInstance is not None and component.id != scope - and component.id not in scopeInstance.resolved[target.id].resolved + and component.id not in scopeInstance.resolved[target.id].required ): continue diff --git a/cutekit/model.py b/cutekit/model.py index 63647dd..b090a1c 100644 --- a/cutekit/model.py +++ b/cutekit/model.py @@ -173,14 +173,19 @@ class Project(Manifest): return _project -@cli.command("i", "install", "Install required external packages") -def installCmd(args: cli.Args): +@cli.command("m", "model", "Manage the model") +def modelCmd(args: cli.Args): + pass + + +@cli.command("i", "model/install", "Install required external packages") +def modelInstallCmd(args: cli.Args): project = Project.use(args) Project.fetchs(project.extern) -@cli.command("I", "init", "Initialize a new project") -def initCmd(args: cli.Args): +@cli.command("I", "model/init", "Initialize a new project") +def modelInitCmd(args: cli.Args): import requests repo = args.consumeOpt("repo", const.DEFAULT_REPO_TEMPLATES) @@ -258,9 +263,15 @@ class Target(Manifest): tools: Tools = dt.field(default_factory=dict) routing: dict[str, str] = dt.field(default_factory=dict) + _hashid = None + @property def hashid(self) -> str: - return utils.hash((self.props, [v.to_dict() for k, v in self.tools.items()])) + if self._hashid is None: + self._hashid = utils.hash( + (self.props, [v.to_dict() for k, v in self.tools.items()]) + ) + return self._hashid @property def builddir(self) -> str: @@ -289,7 +300,8 @@ class Target(Manifest): @dt.dataclass class Resolved: reason: Optional[str] = None - resolved: list[str] = dt.field(default_factory=list) + required: list[str] = dt.field(default_factory=list) + injected: list[str] = dt.field(default_factory=list) @property def enabled(self) -> bool: @@ -436,11 +448,11 @@ class Resolver: self._cache[keep] = Resolved(reason=reqResolved.reason) return self._cache[keep] - result.extend(reqResolved.resolved) + result.extend(reqResolved.required) stack.pop() result.insert(0, keep) - self._cache[keep] = Resolved(resolved=utils.uniq(result)) + self._cache[keep] = Resolved(required=utils.uniq(result)) return self._cache[keep] @@ -570,6 +582,13 @@ class Registry(DataClassJsonMixin): target.props |= props resolver = Resolver(r, target) + # Resolve all components + for c in r.iter(Component): + resolved = resolver.resolve(c.id) + if resolved.reason: + _logger.info(f"Component '{c.id}' disabled: {resolved.reason}") + c.resolved[target.id] = resolved + # Apply injects for c in r.iter(Component): if c.isEnabled(target)[0]: @@ -577,14 +596,7 @@ class Registry(DataClassJsonMixin): victim = r.lookup(inject, Component) if not victim: raise RuntimeError(f"Cannot find component '{inject}'") - victim.requires += [c.id] - - # Resolve all components - for c in r.iter(Component): - resolved = resolver.resolve(c.id) - if resolved.reason: - _logger.info(f"Component '{c.id}' disabled: {resolved.reason}") - c.resolved[target.id] = resolved + victim.resolved[target.id].injected.append(c.id) # Resolve tooling tools: Tools = target.tools @@ -609,8 +621,8 @@ class Registry(DataClassJsonMixin): return r -@cli.command("l", "list", "List all components and targets") -def listCmd(args: cli.Args): +@cli.command("l", "model/list", "List all components and targets") +def modelListCmd(args: cli.Args): registry = Registry.use(args) components = list(registry.iter(Component)) diff --git a/cutekit/pods.py b/cutekit/pods.py new file mode 100644 index 0000000..b4039de --- /dev/null +++ b/cutekit/pods.py @@ -0,0 +1,201 @@ +import sys +import docker # type: ignore +import os +import dataclasses as dt + +from . import cli, model, shell, vt100 + + +podPrefix = "CK__" +projectRoot = "/self" +toolingRoot = "/tools" +defaultPodName = f"{podPrefix}default" +defaultPodImage = "ubuntu" + + +@dt.dataclass +class Image: + id: str + image: str + init: list[str] + + +@dt.dataclass +class Pod: + name: str + image: Image + + +IMAGES: dict[str, Image] = { + "ubuntu": Image( + "ubuntu", + "ubuntu:jammy", + [ + "apt-get update", + "apt-get install -y python3.11 python3.11-venv ninja-build", + ], + ), + "debian": Image( + "debian", + "debian:bookworm", + [ + "apt-get update", + "apt-get install -y python3 python3-pip python3-venv ninja-build", + ], + ), + "alpine": Image( + "alpine", + "alpine:3.18", + [ + "apk update", + "apk add python3 python3-dev py3-pip py3-venv build-base linux-headers ninja", + ], + ), + "arch": Image( + "arch", + "archlinux:latest", + [ + "pacman -Syu --noconfirm", + "pacman -S --noconfirm python python-pip python-virtualenv ninja", + ], + ), + "fedora": Image( + "fedora", + "fedora:39", + [ + "dnf update -y", + "dnf install -y python3 python3-pip python3-venv ninja-build", + ], + ), +} + + +def reincarnate(args: cli.Args): + """ + Reincarnate cutekit within a docker container, this is + useful for cross-compiling + """ + pod = args.consumeOpt("pod", False) + if not pod: + return + if isinstance(pod, str): + pod = pod.strip() + pod = podPrefix + pod + if pod is True: + pod = defaultPodName + assert isinstance(pod, str) + model.Project.ensure() + print(f"Reincarnating into pod '{pod[len(podPrefix) :]}'...") + try: + shell.exec( + "docker", + "exec", + "-w", + projectRoot, + "-it", + pod, + "/tools/cutekit/entrypoint.sh", + *args.args, + ) + sys.exit(0) + except Exception: + sys.exit(1) + + +@cli.command("p", "pod", "Manage pods") +def podCmd(args: cli.Args): + pass + + +@cli.command("c", "pod/create", "Create a new pod") +def podCreateCmd(args: cli.Args): + """ + Create a new development pod with cutekit installed and the current + project mounted at /self + """ + project = model.Project.ensure() + + name = str(args.consumeOpt("name", defaultPodName)) + if not name.startswith(podPrefix): + name = f"{podPrefix}{name}" + image = IMAGES[str(args.consumeOpt("image", defaultPodImage))] + + client = docker.from_env() + try: + client.containers.get(name) + raise RuntimeError(f"Pod '{name[len(podPrefix):]}' already exists") + except docker.errors.NotFound: + pass + + print(f"Staring pod '{name[len(podPrefix) :]}'...") + + container = client.containers.run( + image.image, + "sleep infinity", + name=name, + volumes={ + os.path.abspath(os.path.dirname(__file__)): { + "bind": toolingRoot + "/cutekit", + "mode": "ro", + }, + os.path.abspath(project.dirname()): {"bind": projectRoot, "mode": "rw"}, + }, + detach=True, + ) + + print(f"Initializing pod '{name[len(podPrefix) :]}'...") + for cmd in image.init: + print(vt100.p(cmd)) + exitCode, ouput = container.exec_run(f"/bin/bash -c '{cmd}'", demux=True) + if exitCode != 0: + raise Exception(f"Failed to initialize pod with command '{cmd}'") + + print(f"Created pod '{name[len(podPrefix) :]}' from image '{image.image}'") + + +@cli.command("k", "pod/kill", "Stop and remove a pod") +def podKillCmd(args: cli.Args): + client = docker.from_env() + name = str(args.consumeOpt("name", defaultPodName)) + if not name.startswith(podPrefix): + name = f"{podPrefix}{name}" + + try: + container = client.containers.get(name) + container.stop() + container.remove() + print(f"Pod '{name[len(podPrefix) :]}' killed") + except docker.errors.NotFound: + raise RuntimeError(f"Pod '{name[len(podPrefix):]}' does not exist") + + +@cli.command("s", "pod/shell", "Open a shell in a pod") +def podShellCmd(args: cli.Args): + args.args.insert(0, "/bin/bash") + podExecCmd(args) + + +@cli.command("l", "pod/list", "List all pods") +def podListCmd(args: cli.Args): + client = docker.from_env() + hasPods = False + for container in client.containers.list(all=True): + if not container.name.startswith(podPrefix): + continue + print(container.name[len(podPrefix) :], container.status) + hasPods = True + + if not hasPods: + print(vt100.p("(No pod found)")) + + +@cli.command("e", "pod/exec", "Execute a command in a pod") +def podExecCmd(args: cli.Args): + name = str(args.consumeOpt("name", defaultPodName)) + if not name.startswith(podPrefix): + name = f"{podPrefix}{name}" + + try: + shell.exec("docker", "exec", "-it", name, *args.args) + except Exception: + raise RuntimeError(f"Pod '{name[len(podPrefix):]}' does not exist") diff --git a/requirements.txt b/cutekit/requirements.txt similarity index 80% rename from requirements.txt rename to cutekit/requirements.txt index 11d0245..892a2bf 100644 --- a/requirements.txt +++ b/cutekit/requirements.txt @@ -1,3 +1,4 @@ requests ~= 2.31.0 graphviz ~= 0.20.1 dataclasses-json ~= 0.6.2 +docker ~= 6.1.3 diff --git a/cutekit/rules.py b/cutekit/rules.py index f2004db..669e7b5 100644 --- a/cutekit/rules.py +++ b/cutekit/rules.py @@ -1,52 +1,39 @@ +import dataclasses as dt + from typing import Optional +@dt.dataclass class Rule: id: str fileIn: list[str] - fileOut: list[str] + fileOut: str rule: str - args: list[str] - deps: Optional[str] = None - - def __init__( - self, - id: str, - fileIn: list[str], - fileOut: list[str], - rule: str, - args: list[str] = [], - deps: Optional[str] = None, - ): - self.id = id - self.fileIn = fileIn - self.fileOut = fileOut - self.rule = rule - self.args = args - self.deps = deps + args: list[str] = dt.field(default_factory=list) + deps: list[str] = dt.field(default_factory=list) rules: dict[str, Rule] = { - "cp": Rule("cp", ["*"], ["*"], "$in $out"), + "cp": Rule("cp", ["*"], "*", "$in $out"), "cc": Rule( "cc", ["*.c"], - ["*.o"], + "*.o", "-c -o $out $in -MD -MF $out.d $flags $cincs $cdefs", ["-std=gnu2x", "-Wall", "-Wextra", "-Werror"], - "$out.d", + ["$out.d"], ), "cxx": Rule( "cxx", ["*.cpp", "*.cc", "*.cxx"], - ["*.o"], + "*.o", "-c -o $out $in -MD -MF $out.d $flags $cincs $cdefs", ["-std=gnu++2b", "-Wall", "-Wextra", "-Werror", "-fno-exceptions", "-fno-rtti"], - "$out.d", + ["$out.d"], ), - "as": Rule("as", ["*.s", "*.asm", "*.S"], ["*.o"], "-o $out $in $flags"), - "ar": Rule("ar", ["*.o"], ["*.a"], "$flags $out $in"), - "ld": Rule("ld", ["*.o", "*.a"], ["*.out"], "-o $out $in $flags"), + "as": Rule("as", ["*.s", "*.asm", "*.S"], "*.o", "-o $out $in $flags"), + "ar": Rule("ar", ["*.o"], "*.a", "$flags $out $in"), + "ld": Rule("ld", ["*.o", "*.a"], "*.out", "-o $out $in $flags"), } diff --git a/cutekit/shell.py b/cutekit/shell.py index a7e4a2e..6af8d09 100644 --- a/cutekit/shell.py +++ b/cutekit/shell.py @@ -52,7 +52,7 @@ def sha256sum(path: str) -> str: def find( path: str | list[str], wildcards: list[str] = [], recusive: bool = True ) -> list[str]: - _logger.info(f"Looking for files in {path} matching {wildcards}") + _logger.debug(f"Looking for files in {path} matching {wildcards}") result: list[str] = [] @@ -88,7 +88,7 @@ def find( def mkdir(path: str) -> str: - _logger.info(f"Creating directory {path}") + _logger.debug(f"Creating directory {path}") try: os.makedirs(path) @@ -99,7 +99,7 @@ def mkdir(path: str) -> str: def rmrf(path: str) -> bool: - _logger.info(f"Removing directory {path}") + _logger.debug(f"Removing directory {path}") if not os.path.exists(path): return False @@ -118,7 +118,7 @@ def wget(url: str, path: Optional[str] = None) -> str: if os.path.exists(path): return path - _logger.info(f"Downloading {url} to {path}") + _logger.debug(f"Downloading {url} to {path}") r = requests.get(url, stream=True) r.raise_for_status() @@ -132,7 +132,7 @@ def wget(url: str, path: Optional[str] = None) -> str: def exec(*args: str, quiet: bool = False) -> bool: - _logger.info(f"Executing {args}") + _logger.debug(f"Executing {args}") try: proc = subprocess.run( @@ -142,10 +142,10 @@ def exec(*args: str, quiet: bool = False) -> bool: ) if proc.stdout: - _logger.info(proc.stdout.decode("utf-8")) + _logger.debug(proc.stdout.decode("utf-8")) if proc.stderr: - _logger.error(proc.stderr.decode("utf-8")) + _logger.debug(proc.stderr.decode("utf-8")) except FileNotFoundError: raise RuntimeError(f"{args[0]}: Command not found") @@ -163,7 +163,7 @@ def exec(*args: str, quiet: bool = False) -> bool: def popen(*args: str) -> str: - _logger.info(f"Executing {args}") + _logger.debug(f"Executing {args}") try: proc = subprocess.run(args, stdout=subprocess.PIPE, stderr=sys.stderr) @@ -180,7 +180,7 @@ def popen(*args: str) -> str: def readdir(path: str) -> list[str]: - _logger.info(f"Reading directory {path}") + _logger.debug(f"Reading directory {path}") try: return os.listdir(path) @@ -189,19 +189,19 @@ def readdir(path: str) -> list[str]: def cp(src: str, dst: str): - _logger.info(f"Copying {src} to {dst}") + _logger.debug(f"Copying {src} to {dst}") shutil.copy(src, dst) def mv(src: str, dst: str): - _logger.info(f"Moving {src} to {dst}") + _logger.debug(f"Moving {src} to {dst}") shutil.move(src, dst) def cpTree(src: str, dst: str): - _logger.info(f"Copying {src} to {dst}") + _logger.debug(f"Copying {src} to {dst}") shutil.copytree(src, dst, dirs_exist_ok=True) @@ -241,10 +241,9 @@ def latest(cmd: str) -> str: if cmd in LATEST_CACHE: return LATEST_CACHE[cmd] - _logger.info(f"Finding latest version of {cmd}") + _logger.debug(f"Finding latest version of {cmd}") regex: re.Pattern[str] - if platform.system() == "Windows": regex = re.compile(r"^" + re.escape(cmd) + r"(-.[0-9]+)?(\.exe)?$") else: @@ -263,7 +262,7 @@ def latest(cmd: str) -> str: versions.sort() chosen = versions[-1] - _logger.info(f"Chosen {chosen} as latest version of {cmd}") + _logger.debug(f"Chosen {chosen} as latest version of {cmd}") LATEST_CACHE[cmd] = chosen diff --git a/pyproject.toml b/pyproject.toml index 9861be3..ded5e04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ packages = ["cutekit"] [tool.setuptools.dynamic] version = { attr = "cutekit.const.VERSION" } -dependencies = { file = ["requirements.txt"] } +dependencies = { file = ["cutekit/requirements.txt"] } [tool.setuptools.package-data] -"cutekit" = ["py.typed"] +"cutekit" = ["py.typed", "requirements.txt", "pods-entry.sh"] diff --git a/tests/test_resolver.py b/tests/test_resolver.py index 7221ce6..7249b99 100644 --- a/tests/test_resolver.py +++ b/tests/test_resolver.py @@ -10,7 +10,7 @@ def test_direct_deps(): resolved = res.resolve("myapp") assert resolved.reason is None - assert resolved.resolved == ["myapp", "mylib"] + assert resolved.required == ["myapp", "mylib"] def test_indirect_deps(): @@ -20,7 +20,7 @@ def test_indirect_deps(): r._append(model.Component("myimpl", provides=["myembed"])) t = model.Target("host") res = model.Resolver(r, t) - assert res.resolve("myapp").resolved == ["myapp", "mylib", "myimpl"] + assert res.resolve("myapp").required == ["myapp", "mylib", "myimpl"] def test_deps_routing(): @@ -31,11 +31,11 @@ def test_deps_routing(): r._append(model.Component("myimplB", provides=["myembed"])) t = model.Target("host", routing={"myembed": "myimplB"}) res = model.Resolver(r, t) - assert res.resolve("myapp").resolved == ["myapp", "mylib", "myimplB"] + assert res.resolve("myapp").required == ["myapp", "mylib", "myimplB"] t = model.Target("host", routing={"myembed": "myimplA"}) res = model.Resolver(r, t) - assert res.resolve("myapp").resolved == ["myapp", "mylib", "myimplA"] + assert res.resolve("myapp").required == ["myapp", "mylib", "myimplA"] t = model.Target("host", routing={"myembed": "myimplC"}) res = model.Resolver(r, t) @@ -54,11 +54,11 @@ def test_deps_routing_with_props(): ) t = model.Target("host", routing={"myembed": "myimplB"}, props={"myprop": "b"}) res = model.Resolver(r, t) - assert res.resolve("myapp").resolved == ["myapp", "mylib", "myimplB"] + assert res.resolve("myapp").required == ["myapp", "mylib", "myimplB"] t = model.Target("host", routing={"myembed": "myimplA"}, props={"myprop": "a"}) res = model.Resolver(r, t) - assert res.resolve("myapp").resolved == ["myapp", "mylib", "myimplA"] + assert res.resolve("myapp").required == ["myapp", "mylib", "myimplA"] t = model.Target("host", routing={"myembed": "myimplC"}, props={"myprop": "c"}) res = model.Resolver(r, t) @@ -79,11 +79,11 @@ def test_deps_routing_with_props_and_requires(): ) t = model.Target("host", routing={"myembed": "myimplB"}, props={"myprop": "b"}) res = model.Resolver(r, t) - assert res.resolve("myapp").resolved == ["myapp", "mylib", "myimplB"] + assert res.resolve("myapp").required == ["myapp", "mylib", "myimplB"] t = model.Target("host", routing={"myembed": "myimplA"}, props={"myprop": "a"}) res = model.Resolver(r, t) - assert res.resolve("myapp").resolved == ["myapp", "mylib", "myimplA"] + assert res.resolve("myapp").required == ["myapp", "mylib", "myimplA"] t = model.Target("host", routing={"myembed": "myimplC"}, props={"myprop": "c"}) res = model.Resolver(r, t)