Added support for nested commands and running cutekit in containers.
This commit is contained in:
parent
8a9c4689e9
commit
72c982ab7b
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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])
|
||||
|
|
20
cutekit/entrypoint.sh
Executable file
20
cutekit/entrypoint.sh
Executable 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 "$@"
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
201
cutekit/pods.py
Normal file
201
cutekit/pods.py
Normal 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")
|
|
@ -1,3 +1,4 @@
|
|||
requests ~= 2.31.0
|
||||
graphviz ~= 0.20.1
|
||||
dataclasses-json ~= 0.6.2
|
||||
docker ~= 6.1.3
|
|
@ -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"),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue