Added support for nested commands and running cutekit in containers.

This commit is contained in:
Sleepy Monax 2023-11-30 13:30:50 +01:00
parent 8a9c4689e9
commit 72c982ab7b
13 changed files with 506 additions and 197 deletions

View file

@ -9,6 +9,7 @@ from . import (
graph, # noqa: F401 this is imported for side effects graph, # noqa: F401 this is imported for side effects
model, model,
plugins, plugins,
pods, # noqa: F401 this is imported for side effects
vt100, vt100,
) )
@ -55,10 +56,13 @@ def setupLogger(verbose: bool):
def main() -> int: def main() -> int:
try: try:
a = cli.parse(sys.argv[1:]) args = cli.parse(sys.argv[1:])
setupLogger(a.consumeOpt("verbose", False) is True) setupLogger(args.consumeOpt("verbose", False) is True)
plugins.loadAll() safemode = args.consumeOpt("safemode", False) is True
cli.exec(a) if not safemode:
plugins.loadAll()
pods.reincarnate(args)
cli.exec(args)
print() print()
return 0 return 0
except RuntimeError as e: except RuntimeError as e:

View file

@ -3,32 +3,89 @@ import logging
import dataclasses as dt import dataclasses as dt
from pathlib import Path from pathlib import Path
from typing import TextIO, Union from typing import Callable, TextIO, Union
from . import shell, rules, model, ninja, const, cli from . import shell, rules, model, ninja, const, cli
_logger = logging.getLogger(__name__) _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() res = set()
for c in registry.iterEnabled(target): for c in scope.registry.iterEnabled(scope.target):
if "cpp-root-include" in c.props: if "cpp-root-include" in c.props:
res.add(c.dirname()) res.add(c.dirname())
elif c.type == model.Kind.LIB: elif c.type == model.Kind.LIB:
res.add(str(Path(c.dirname()).parent)) 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() res = set()
def sanatize(s: str) -> str: def sanatize(s: str) -> str:
return s.lower().replace(" ", "_").replace("-", "_").replace(".", "_") 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 isinstance(v, bool):
if v: if v:
res.add(f"-D__ck_{sanatize(k)}__") 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)}_{sanatize(str(v))}__")
res.add(f"-D__ck_{sanatize(k)}_value={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: def buildpath(scope: ComponentScope, path) -> Path:
return Path(target.builddir) / component.id / path return Path(scope.target.builddir) / scope.component.id / path
# --- Compilation ------------------------------------------------------------ # # --- Compilation ------------------------------------------------------------ #
def wilcard(component: model.Component, wildcards: list[str]) -> list[str]: def subdirs(scope: ComponentScope) -> list[str]:
dirs = [component.dirname()] + list( registry = scope.registry
map(lambda d: os.path.join(component.dirname(), d), component.subdirs) target = scope.target
) component = scope.component
return shell.find(dirs, list(wildcards), recusive=False) 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( def compile(
w: ninja.Writer, w: ninja.Writer, scope: ComponentScope, rule: str, srcs: list[str]
target: model.Target,
component: model.Component,
rule: str,
srcs: list[str],
) -> list[str]: ) -> list[str]:
res: list[str] = [] res: list[str] = []
for src in srcs: for src in srcs:
rel = Path(src).relative_to(component.dirname()) rel = Path(src).relative_to(scope.component.dirname())
dest = buildpath(target, component, "obj") / rel.with_suffix(".o") dest = buildpath(scope, "obj") / rel.with_suffix(".o")
t = target.tools[rule] t = scope.target.tools[rule]
w.build(str(dest), rule, inputs=src, order_only=t.files) w.build(str(dest), rule, inputs=src, order_only=t.files)
res.append(str(dest)) res.append(str(dest))
return res return res
@ -79,13 +146,12 @@ def listRes(component: model.Component) -> list[str]:
def compileRes( def compileRes(
w: ninja.Writer, w: ninja.Writer,
target: model.Target, scope: ComponentScope,
component: model.Component,
) -> list[str]: ) -> list[str]:
res: list[str] = [] res: list[str] = []
for r in listRes(component): for r in listRes(scope.component):
rel = Path(r).relative_to(component.subpath("res")) rel = Path(r).relative_to(scope.component.subpath("res"))
dest = buildpath(target, component, "res") / rel dest = buildpath(scope, "res") / rel
w.build(str(dest), "cp", r) w.build(str(dest), "cp", r)
res.append(str(dest)) res.append(str(dest))
return res return res
@ -94,50 +160,55 @@ def compileRes(
# --- Linking ---------------------------------------------------------------- # # --- Linking ---------------------------------------------------------------- #
def outfile(target: model.Target, component: model.Component) -> str: def outfile(scope: ComponentScope) -> str:
if component.type == model.Kind.LIB: if scope.component.type == model.Kind.LIB:
return str(buildpath(target, component, f"lib/{component.id}.a")) return str(buildpath(scope, f"lib/{scope.component.id}.a"))
else: else:
return str(buildpath(target, component, f"bin/{component.id}.out")) return str(buildpath(scope, f"bin/{scope.component.id}.out"))
def collectLibs( def collectLibs(
registry: model.Registry, target: model.Target, component: model.Component scope: ComponentScope,
) -> list[str]: ) -> list[str]:
res: list[str] = [] res: list[str] = []
for r in component.resolved[target.id].resolved: for r in scope.component.resolved[scope.target.id].required:
req = registry.lookup(r, model.Component) req = scope.registry.lookup(r, model.Component)
assert req is not None # model.Resolver has already checked this assert req is not None # model.Resolver has already checked this
if r == component.id: if r == scope.component.id:
continue continue
if not req.type == model.Kind.LIB: if not req.type == model.Kind.LIB:
raise RuntimeError(f"Component {r} is not a library") raise RuntimeError(f"Component {r} is not a library")
res.append(outfile(target, req)) res.append(outfile(scope.openComponentScope(req)))
return res return res
def link( def link(
w: ninja.Writer, w: ninja.Writer,
registry: model.Registry, scope: ComponentScope,
target: model.Target,
component: model.Component,
) -> str: ) -> str:
w.newline() w.newline()
out = outfile(target, component) out = outfile(scope)
objs = [] objs = []
objs += compile(w, target, component, "cc", wilcard(component, ["*.c"])) objs += compile(w, scope, "cc", wilcard(scope, ["*.c"]))
objs += compile( objs += compile(
w, target, component, "cxx", wilcard(component, ["*.cpp", "*.cc", "*.cxx"]) w,
scope,
"cxx",
wilcard(scope, ["*.cpp", "*.cc", "*.cxx"]),
) )
objs += compile( objs += compile(
w, target, component, "as", wilcard(component, ["*.s", "*.asm", "*.S"]) w,
scope,
"as",
wilcard(scope, ["*.s", "*.asm", "*.S"]),
) )
res = compileRes(w, target, component) res = compileRes(w, scope)
libs = collectLibs(registry, target, component) libs = collectLibs(scope)
if component.type == model.Kind.LIB: if scope.component.type == model.Kind.LIB:
w.build(out, "ar", objs, implicit=res) w.build(out, "ar", objs, implicit=res)
else: else:
w.build(out, "ld", objs + libs, implicit=res) w.build(out, "ld", objs + libs, implicit=res)
@ -147,32 +218,30 @@ def link(
# --- Phony ------------------------------------------------------------------ # # --- 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] = [] all: list[str] = []
for c in registry.iterEnabled(target): for c in scope.registry.iterEnabled(scope.target):
all.append(link(w, registry, target, c)) all.append(link(w, scope.openComponentScope(c)))
w.build("all", "phony", all) w.build("all", "phony", all)
w.default("all") w.default("all")
return all return all
def gen(out: TextIO, target: model.Target, registry: model.Registry): def gen(out: TextIO, scope: TargetScope):
w = ninja.Writer(out) w = ninja.Writer(out)
w.comment("File generated by the build system, do not edit") w.comment("File generated by the build system, do not edit")
w.newline() w.newline()
w.variable("builddir", target.builddir) w.separator("Variables")
w.variable("hashid", target.hashid) for name, compute in _vars.items():
w.variable(name, compute(scope))
w.newline()
w.separator("Tools") w.separator("Tools")
w.variable("cincs", " ".join(aggregateCincs(target, registry))) for i in scope.target.tools:
w.variable("cdefs", " ".join(aggregateCdefs(target))) tool = scope.target.tools[i]
w.newline()
for i in target.tools:
tool = target.tools[i]
rule = rules.rules[i] rule = rules.rules[i]
w.variable(i, tool.cmd) w.variable(i, tool.cmd)
w.variable(i + "flags", " ".join(rule.args + tool.args)) 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") w.separator("Build")
all(w, registry, target) all(w, scope)
@dt.dataclass
class Product:
path: Path
target: model.Target
component: model.Component
def build( def build(
target: model.Target, scope: TargetScope,
registry: model.Registry,
components: Union[list[model.Component], model.Component, None] = None, components: Union[list[model.Component], model.Component, None] = None,
) -> list[Product]: ) -> list[ProductScope]:
all = False all = False
shell.mkdir(target.builddir) shell.mkdir(scope.target.builddir)
ninjaPath = os.path.join(target.builddir, "build.ninja") ninjaPath = os.path.join(scope.target.builddir, "build.ninja")
if not os.path.exists(ninjaPath): if not os.path.exists(ninjaPath):
with open(ninjaPath, "w") as f: with open(ninjaPath, "w") as f:
gen(f, target, registry) gen(f, scope)
if components is None: if components is None:
all = True all = True
components = list(registry.iterEnabled(target)) components = list(scope.registry.iterEnabled(scope.target))
if isinstance(components, model.Component): if isinstance(components, model.Component):
components = [components] components = [components]
products: list[Product] = [] products: list[ProductScope] = []
for c in components: for c in components:
r = c.resolved[target.id] s = scope.openComponentScope(c)
r = c.resolved[scope.target.id]
if not r.enabled: if not r.enabled:
raise RuntimeError(f"Component {c.id} is disabled: {r.reason}") raise RuntimeError(f"Component {c.id} is disabled: {r.reason}")
products.append( products.append(s.openProductScope(Path(outfile(scope.openComponentScope(c)))))
Product(
path=Path(outfile(target, c)),
target=target,
component=c,
)
)
outs = list(map(lambda p: str(p.path), products)) 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 return products
@ -239,60 +295,65 @@ def build(
# --- Commands --------------------------------------------------------------- # # --- 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): def buildCmd(args: cli.Args):
registry = model.Registry.use(args) pass
target = model.Target.use(args)
@cli.command("b", "build/build", "Build a component or all components")
def buildBuildCmd(args: cli.Args):
scope = TargetScope.use(args)
componentSpec = args.consumeArg() componentSpec = args.consumeArg()
component = None component = None
if componentSpec is not None: if componentSpec is not None:
component = registry.lookup(componentSpec, model.Component) component = scope.registry.lookup(componentSpec, model.Component)
build(target, registry, component)[0] build(scope, component)[0]
@cli.command("r", "run", "Run a component") @cli.command("r", "build/run", "Run a component")
def runCmd(args: cli.Args): def buildRunCmd(args: cli.Args):
registry = model.Registry.use(args) scope = TargetScope.use(args)
target = model.Target.use(args)
debug = args.consumeOpt("debug", False) is True debug = args.consumeOpt("debug", False) is True
componentSpec = args.consumeArg() or "__main__" 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: if component is None:
raise RuntimeError(f"Component {componentSpec} not found") 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_COMPONENT"] = product.component.id
os.environ["CK_BUILDDIR"] = target.builddir
shell.exec(*(["lldb", "-o", "run"] if debug else []), str(product.path), *args.args) shell.exec(*(["lldb", "-o", "run"] if debug else []), str(product.path), *args.args)
@cli.command("t", "test", "Run all test targets") @cli.command("t", "build/test", "Run all test targets")
def testCmd(args: cli.Args): def buildTestCmd(args: cli.Args):
# This is just a wrapper around the `run` command that try # This is just a wrapper around the `run` command that try
# to run a special hook component named __tests__. # to run a special hook component named __tests__.
args.args.insert(0, "__tests__") args.args.insert(0, "__tests__")
runCmd(args) buildRunCmd(args)
@cli.command("d", "debug", "Debug a component") @cli.command("d", "build/debug", "Debug a component")
def debugCmd(args: cli.Args): def buildDebugCmd(args: cli.Args):
# This is just a wrapper around the `run` command that # This is just a wrapper around the `run` command that
# always enable debug mode. # always enable debug mode.
args.opts["debug"] = True args.opts["debug"] = True
runCmd(args) buildRunCmd(args)
@cli.command("c", "clean", "Clean build files") @cli.command("c", "build/clean", "Clean build files")
def cleanCmd(args: cli.Args): def buildCleanCmd(args: cli.Args):
model.Project.use(args) model.Project.use(args)
shell.rmrf(const.BUILD_DIR) shell.rmrf(const.BUILD_DIR)
@cli.command("n", "nuke", "Clean all build files and caches") @cli.command("n", "build/nuke", "Clean all build files and caches")
def nukeCmd(args: cli.Args): def buildNukeCmd(args: cli.Args):
model.Project.use(args) model.Project.use(args)
shell.rmrf(const.PROJECT_CK_DIR) shell.rmrf(const.PROJECT_CK_DIR)

View file

@ -80,8 +80,10 @@ class Command:
isPlugin: bool isPlugin: bool
callback: Callback 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): 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]): def wrap(fn: Callable[[Args], None]):
_logger.debug(f"Registering command {longName}") _logger.debug(f"Registering command {longName}")
commands.append( path = longName.split("/")
Command( parent = commands
shortName, for p in path[:-1]:
longName, parent = parent[p].subcommands
helpText, parent[path[-1]] = Command(
Path(calframe[1].filename).parent != Path(__file__).parent, shortName,
fn, path[-1],
) helpText,
Path(calframe[1].filename).parent != Path(__file__).parent,
fn,
) )
return fn return fn
return wrap return wrap
@ -127,8 +132,8 @@ def helpCmd(args: Args):
print() print()
vt100.title("Commands") vt100.title("Commands")
for cmd in sorted(commands, key=lambda c: c.longName): for cmd in sorted(commands.values(), key=lambda c: c.longName):
if cmd.longName.startswith("_"): if cmd.longName.startswith("_") or len(cmd.subcommands) > 0:
continue continue
pluginText = "" pluginText = ""
@ -139,6 +144,21 @@ def helpCmd(args: Args):
f" {vt100.GREEN}{cmd.shortName or ' '}{vt100.RESET} {cmd.longName} - {cmd.helpText} {pluginText}" 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() print()
vt100.title("Logging") vt100.title("Logging")
print(" Logs are stored in:") print(" Logs are stored in:")
@ -151,15 +171,19 @@ def versionCmd(args: Args):
print(f"CuteKit v{const.VERSION_STR}") print(f"CuteKit v{const.VERSION_STR}")
def exec(args: Args): def exec(args: Args, cmds=commands):
cmd = args.consumeArg() cmd = args.consumeArg()
if cmd is None: if cmd is None:
raise RuntimeError("No command specified") raise RuntimeError("No command specified")
for c in commands: for c in cmds.values():
if c.shortName == cmd or c.longName == cmd: if c.shortName == cmd or c.longName == cmd:
c.callback(args) if len(c.subcommands) > 0:
return exec(args, c.subcommands)
return
else:
c.callback(args)
return
raise RuntimeError(f"Unknown command {cmd}") raise RuntimeError(f"Unknown command {cmd}")

View file

@ -1,7 +1,7 @@
import os import os
import sys 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 ''}" 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__)) MODULE_DIR = os.path.dirname(os.path.realpath(__file__))
ARGV0 = os.path.basename(sys.argv[0]) ARGV0 = os.path.basename(sys.argv[0])

20
cutekit/entrypoint.sh Executable file
View file

@ -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 "$@"

View file

@ -36,7 +36,7 @@ def view(
if ( if (
scopeInstance is not None scopeInstance is not None
and component.id != scope 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 continue

View file

@ -173,14 +173,19 @@ class Project(Manifest):
return _project return _project
@cli.command("i", "install", "Install required external packages") @cli.command("m", "model", "Manage the model")
def installCmd(args: cli.Args): 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 = Project.use(args)
Project.fetchs(project.extern) Project.fetchs(project.extern)
@cli.command("I", "init", "Initialize a new project") @cli.command("I", "model/init", "Initialize a new project")
def initCmd(args: cli.Args): def modelInitCmd(args: cli.Args):
import requests import requests
repo = args.consumeOpt("repo", const.DEFAULT_REPO_TEMPLATES) repo = args.consumeOpt("repo", const.DEFAULT_REPO_TEMPLATES)
@ -258,9 +263,15 @@ class Target(Manifest):
tools: Tools = dt.field(default_factory=dict) tools: Tools = dt.field(default_factory=dict)
routing: dict[str, str] = dt.field(default_factory=dict) routing: dict[str, str] = dt.field(default_factory=dict)
_hashid = None
@property @property
def hashid(self) -> str: 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 @property
def builddir(self) -> str: def builddir(self) -> str:
@ -289,7 +300,8 @@ class Target(Manifest):
@dt.dataclass @dt.dataclass
class Resolved: class Resolved:
reason: Optional[str] = None 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 @property
def enabled(self) -> bool: def enabled(self) -> bool:
@ -436,11 +448,11 @@ class Resolver:
self._cache[keep] = Resolved(reason=reqResolved.reason) self._cache[keep] = Resolved(reason=reqResolved.reason)
return self._cache[keep] return self._cache[keep]
result.extend(reqResolved.resolved) result.extend(reqResolved.required)
stack.pop() stack.pop()
result.insert(0, keep) result.insert(0, keep)
self._cache[keep] = Resolved(resolved=utils.uniq(result)) self._cache[keep] = Resolved(required=utils.uniq(result))
return self._cache[keep] return self._cache[keep]
@ -570,6 +582,13 @@ class Registry(DataClassJsonMixin):
target.props |= props target.props |= props
resolver = Resolver(r, target) 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 # Apply injects
for c in r.iter(Component): for c in r.iter(Component):
if c.isEnabled(target)[0]: if c.isEnabled(target)[0]:
@ -577,14 +596,7 @@ class Registry(DataClassJsonMixin):
victim = r.lookup(inject, Component) victim = r.lookup(inject, Component)
if not victim: if not victim:
raise RuntimeError(f"Cannot find component '{inject}'") raise RuntimeError(f"Cannot find component '{inject}'")
victim.requires += [c.id] victim.resolved[target.id].injected.append(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
# Resolve tooling # Resolve tooling
tools: Tools = target.tools tools: Tools = target.tools
@ -609,8 +621,8 @@ class Registry(DataClassJsonMixin):
return r return r
@cli.command("l", "list", "List all components and targets") @cli.command("l", "model/list", "List all components and targets")
def listCmd(args: cli.Args): def modelListCmd(args: cli.Args):
registry = Registry.use(args) registry = Registry.use(args)
components = list(registry.iter(Component)) components = list(registry.iter(Component))

201
cutekit/pods.py Normal file
View file

@ -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")

View file

@ -1,3 +1,4 @@
requests ~= 2.31.0 requests ~= 2.31.0
graphviz ~= 0.20.1 graphviz ~= 0.20.1
dataclasses-json ~= 0.6.2 dataclasses-json ~= 0.6.2
docker ~= 6.1.3

View file

@ -1,52 +1,39 @@
import dataclasses as dt
from typing import Optional from typing import Optional
@dt.dataclass
class Rule: class Rule:
id: str id: str
fileIn: list[str] fileIn: list[str]
fileOut: list[str] fileOut: str
rule: str rule: str
args: list[str] args: list[str] = dt.field(default_factory=list)
deps: Optional[str] = None deps: list[str] = dt.field(default_factory=list)
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
rules: dict[str, Rule] = { rules: dict[str, Rule] = {
"cp": Rule("cp", ["*"], ["*"], "$in $out"), "cp": Rule("cp", ["*"], "*", "$in $out"),
"cc": Rule( "cc": Rule(
"cc", "cc",
["*.c"], ["*.c"],
["*.o"], "*.o",
"-c -o $out $in -MD -MF $out.d $flags $cincs $cdefs", "-c -o $out $in -MD -MF $out.d $flags $cincs $cdefs",
["-std=gnu2x", "-Wall", "-Wextra", "-Werror"], ["-std=gnu2x", "-Wall", "-Wextra", "-Werror"],
"$out.d", ["$out.d"],
), ),
"cxx": Rule( "cxx": Rule(
"cxx", "cxx",
["*.cpp", "*.cc", "*.cxx"], ["*.cpp", "*.cc", "*.cxx"],
["*.o"], "*.o",
"-c -o $out $in -MD -MF $out.d $flags $cincs $cdefs", "-c -o $out $in -MD -MF $out.d $flags $cincs $cdefs",
["-std=gnu++2b", "-Wall", "-Wextra", "-Werror", "-fno-exceptions", "-fno-rtti"], ["-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"), "as": Rule("as", ["*.s", "*.asm", "*.S"], "*.o", "-o $out $in $flags"),
"ar": Rule("ar", ["*.o"], ["*.a"], "$flags $out $in"), "ar": Rule("ar", ["*.o"], "*.a", "$flags $out $in"),
"ld": Rule("ld", ["*.o", "*.a"], ["*.out"], "-o $out $in $flags"), "ld": Rule("ld", ["*.o", "*.a"], "*.out", "-o $out $in $flags"),
} }

View file

@ -52,7 +52,7 @@ def sha256sum(path: str) -> str:
def find( def find(
path: str | list[str], wildcards: list[str] = [], recusive: bool = True path: str | list[str], wildcards: list[str] = [], recusive: bool = True
) -> list[str]: ) -> 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] = [] result: list[str] = []
@ -88,7 +88,7 @@ def find(
def mkdir(path: str) -> str: def mkdir(path: str) -> str:
_logger.info(f"Creating directory {path}") _logger.debug(f"Creating directory {path}")
try: try:
os.makedirs(path) os.makedirs(path)
@ -99,7 +99,7 @@ def mkdir(path: str) -> str:
def rmrf(path: str) -> bool: def rmrf(path: str) -> bool:
_logger.info(f"Removing directory {path}") _logger.debug(f"Removing directory {path}")
if not os.path.exists(path): if not os.path.exists(path):
return False return False
@ -118,7 +118,7 @@ def wget(url: str, path: Optional[str] = None) -> str:
if os.path.exists(path): if os.path.exists(path):
return path return path
_logger.info(f"Downloading {url} to {path}") _logger.debug(f"Downloading {url} to {path}")
r = requests.get(url, stream=True) r = requests.get(url, stream=True)
r.raise_for_status() 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: def exec(*args: str, quiet: bool = False) -> bool:
_logger.info(f"Executing {args}") _logger.debug(f"Executing {args}")
try: try:
proc = subprocess.run( proc = subprocess.run(
@ -142,10 +142,10 @@ def exec(*args: str, quiet: bool = False) -> bool:
) )
if proc.stdout: if proc.stdout:
_logger.info(proc.stdout.decode("utf-8")) _logger.debug(proc.stdout.decode("utf-8"))
if proc.stderr: if proc.stderr:
_logger.error(proc.stderr.decode("utf-8")) _logger.debug(proc.stderr.decode("utf-8"))
except FileNotFoundError: except FileNotFoundError:
raise RuntimeError(f"{args[0]}: Command not found") 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: def popen(*args: str) -> str:
_logger.info(f"Executing {args}") _logger.debug(f"Executing {args}")
try: try:
proc = subprocess.run(args, stdout=subprocess.PIPE, stderr=sys.stderr) 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]: def readdir(path: str) -> list[str]:
_logger.info(f"Reading directory {path}") _logger.debug(f"Reading directory {path}")
try: try:
return os.listdir(path) return os.listdir(path)
@ -189,19 +189,19 @@ def readdir(path: str) -> list[str]:
def cp(src: str, dst: 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) shutil.copy(src, dst)
def mv(src: str, dst: str): def mv(src: str, dst: str):
_logger.info(f"Moving {src} to {dst}") _logger.debug(f"Moving {src} to {dst}")
shutil.move(src, dst) shutil.move(src, dst)
def cpTree(src: str, dst: str): 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) shutil.copytree(src, dst, dirs_exist_ok=True)
@ -241,10 +241,9 @@ def latest(cmd: str) -> str:
if cmd in LATEST_CACHE: if cmd in LATEST_CACHE:
return LATEST_CACHE[cmd] return LATEST_CACHE[cmd]
_logger.info(f"Finding latest version of {cmd}") _logger.debug(f"Finding latest version of {cmd}")
regex: re.Pattern[str] regex: re.Pattern[str]
if platform.system() == "Windows": if platform.system() == "Windows":
regex = re.compile(r"^" + re.escape(cmd) + r"(-.[0-9]+)?(\.exe)?$") regex = re.compile(r"^" + re.escape(cmd) + r"(-.[0-9]+)?(\.exe)?$")
else: else:
@ -263,7 +262,7 @@ def latest(cmd: str) -> str:
versions.sort() versions.sort()
chosen = versions[-1] 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 LATEST_CACHE[cmd] = chosen

View file

@ -26,7 +26,7 @@ packages = ["cutekit"]
[tool.setuptools.dynamic] [tool.setuptools.dynamic]
version = { attr = "cutekit.const.VERSION" } version = { attr = "cutekit.const.VERSION" }
dependencies = { file = ["requirements.txt"] } dependencies = { file = ["cutekit/requirements.txt"] }
[tool.setuptools.package-data] [tool.setuptools.package-data]
"cutekit" = ["py.typed"] "cutekit" = ["py.typed", "requirements.txt", "pods-entry.sh"]

View file

@ -10,7 +10,7 @@ def test_direct_deps():
resolved = res.resolve("myapp") resolved = res.resolve("myapp")
assert resolved.reason is None assert resolved.reason is None
assert resolved.resolved == ["myapp", "mylib"] assert resolved.required == ["myapp", "mylib"]
def test_indirect_deps(): def test_indirect_deps():
@ -20,7 +20,7 @@ def test_indirect_deps():
r._append(model.Component("myimpl", provides=["myembed"])) r._append(model.Component("myimpl", provides=["myembed"]))
t = model.Target("host") t = model.Target("host")
res = model.Resolver(r, t) 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(): def test_deps_routing():
@ -31,11 +31,11 @@ def test_deps_routing():
r._append(model.Component("myimplB", provides=["myembed"])) r._append(model.Component("myimplB", provides=["myembed"]))
t = model.Target("host", routing={"myembed": "myimplB"}) t = model.Target("host", routing={"myembed": "myimplB"})
res = model.Resolver(r, t) 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"}) t = model.Target("host", routing={"myembed": "myimplA"})
res = model.Resolver(r, t) 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"}) t = model.Target("host", routing={"myembed": "myimplC"})
res = model.Resolver(r, t) 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"}) t = model.Target("host", routing={"myembed": "myimplB"}, props={"myprop": "b"})
res = model.Resolver(r, t) 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"}) t = model.Target("host", routing={"myembed": "myimplA"}, props={"myprop": "a"})
res = model.Resolver(r, t) 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"}) t = model.Target("host", routing={"myembed": "myimplC"}, props={"myprop": "c"})
res = model.Resolver(r, t) 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"}) t = model.Target("host", routing={"myembed": "myimplB"}, props={"myprop": "b"})
res = model.Resolver(r, t) 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"}) t = model.Target("host", routing={"myembed": "myimplA"}, props={"myprop": "a"})
res = model.Resolver(r, t) 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"}) t = model.Target("host", routing={"myembed": "myimplC"}, props={"myprop": "c"})
res = model.Resolver(r, t) res = model.Resolver(r, t)