Refactored build context and dependecy resolution code
This commit is contained in:
		
							parent
							
								
									3dbf269cdd
								
							
						
					
					
						commit
						f6f36ea79e
					
				
					 12 changed files with 896 additions and 756 deletions
				
			
		|  | @ -3,12 +3,20 @@ import os | |||
| import logging | ||||
| 
 | ||||
| from . import ( | ||||
|     builder, | ||||
|     cli, | ||||
|     compat, | ||||
|     const, | ||||
|     graph, | ||||
|     jexpr, | ||||
|     mixins, | ||||
|     model, | ||||
|     ninja, | ||||
|     plugins, | ||||
|     rules, | ||||
|     shell, | ||||
|     utils, | ||||
|     vt100, | ||||
|     cmds,  # noqa: F401 | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,170 +1,290 @@ | |||
| import os | ||||
| import logging | ||||
| from typing import TextIO | ||||
| from pathlib import Path | ||||
| from dataclasses import dataclass | ||||
| from itertools import chain | ||||
| from typing import Generator, TextIO, Union, cast | ||||
| 
 | ||||
| from . import shell, rules, model, ninja, context | ||||
| from . import shell, rules, model, ninja, const, utils, cli | ||||
| 
 | ||||
| _logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| def gen(out: TextIO, context: context.Context): | ||||
|     writer = ninja.Writer(out) | ||||
| def aggregateCincs(target: model.Target, registry: model.Registry) -> set[str]: | ||||
|     res = set() | ||||
| 
 | ||||
|     target = context.target | ||||
|     for c in registry.iterEnabled(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)) | ||||
| 
 | ||||
|     writer.comment("File generated by the build system, do not edit") | ||||
|     writer.newline() | ||||
|     writer.variable("builddir", context.builddir()) | ||||
|     return set(map(lambda i: f"-I{i}", res)) | ||||
| 
 | ||||
|     writer.separator("Tools") | ||||
| 
 | ||||
|     writer.variable("cincs", " ".join(map(lambda i: f"-I{i}", context.cincls()))) | ||||
| def aggregateCdefs(target: model.Target) -> set[str]: | ||||
|     res = set() | ||||
| 
 | ||||
|     writer.variable("cdefs", " ".join(context.cdefs())) | ||||
|     def sanatize(s: str) -> str: | ||||
|         return s.lower().replace(" ", "_").replace("-", "_").replace(".", "_") | ||||
| 
 | ||||
|     writer.newline() | ||||
|     for k, v in target.props.items(): | ||||
|         if isinstance(v, bool): | ||||
|             if v: | ||||
|                 res.add(f"-D__ck_{sanatize(k)}__") | ||||
|         else: | ||||
|             res.add(f"-D__ck_{sanatize(k)}_{sanatize(str(v))}__") | ||||
|             res.add(f"-D__ck_{sanatize(k)}_value={str(v)}") | ||||
| 
 | ||||
|     writer.rule("cp", "cp $in $out") | ||||
|     writer.newline() | ||||
|     return res | ||||
| 
 | ||||
| 
 | ||||
| def buildpath(target: model.Target, component: model.Component, path) -> Path: | ||||
|     return Path(target.builddir) / component.id / path | ||||
| 
 | ||||
| 
 | ||||
| # --- Compilation ------------------------------------------------------------ # | ||||
| 
 | ||||
| 
 | ||||
| def listSrc(component: model.Component) -> list[str]: | ||||
|     wildcards = set(chain(*map(lambda rule: rule.fileIn, rules.rules.values()))) | ||||
|     dirs = [component.dirname()] + list( | ||||
|         map(lambda d: os.path.join(component.dirname(), d), component.subdirs) | ||||
|     ) | ||||
|     return shell.find(dirs, list(wildcards), recusive=False) | ||||
| 
 | ||||
| 
 | ||||
| def compileSrc( | ||||
|     w: ninja.Writer, target: model.Target, component: model.Component | ||||
| ) -> list[str]: | ||||
|     res: list[str] = [] | ||||
|     for src in listSrc(component): | ||||
|         rel = Path(src).relative_to(component.dirname()) | ||||
| 
 | ||||
|         r = rules.byFileIn(src) | ||||
|         if r is None: | ||||
|             raise RuntimeError(f"Unknown rule for file {src}") | ||||
| 
 | ||||
|         dest = buildpath(target, component, "obj") / rel.with_suffix(r.fileOut[0][1:]) | ||||
|         t = target.tools[r.id] | ||||
|         w.build(str(dest), r.id, inputs=src, order_only=t.files) | ||||
|         res.append(str(dest)) | ||||
|     return res | ||||
| 
 | ||||
| 
 | ||||
| # --- Ressources ------------------------------------------------------------- # | ||||
| 
 | ||||
| 
 | ||||
| def listRes(component: model.Component) -> list[str]: | ||||
|     return shell.find(str(component.subpath("res"))) | ||||
| 
 | ||||
| 
 | ||||
| def compileRes( | ||||
|     w: ninja.Writer, | ||||
|     target: model.Target, | ||||
|     component: model.Component, | ||||
| ) -> list[str]: | ||||
|     res: list[str] = [] | ||||
|     for r in listRes(component): | ||||
|         rel = Path(r).relative_to(component.subpath("res")) | ||||
|         dest = buildpath(target, component, "res") / rel | ||||
|         w.build(str(dest), "cp", r) | ||||
|         res.append(str(dest)) | ||||
|     return res | ||||
| 
 | ||||
| 
 | ||||
| # --- 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")) | ||||
|     else: | ||||
|         return str(buildpath(target, component, f"bin/{component.id}.out")) | ||||
| 
 | ||||
| 
 | ||||
| def collectLibs( | ||||
|     registry: model.Registry, target: model.Target, component: model.Component | ||||
| ) -> list[str]: | ||||
|     res: list[str] = [] | ||||
|     for r in component.resolved[target.id].resolved: | ||||
|         req = registry.lookup(r, model.Component) | ||||
|         assert req is not None  # model.Resolver has already checked this | ||||
| 
 | ||||
|         if r == component.id: | ||||
|             continue | ||||
|         if not req.type == model.Kind.LIB: | ||||
|             raise RuntimeError(f"Component {r} is not a library") | ||||
|         res.append(outfile(target, req)) | ||||
|     return res | ||||
| 
 | ||||
| 
 | ||||
| def link( | ||||
|     w: ninja.Writer, | ||||
|     registry: model.Registry, | ||||
|     target: model.Target, | ||||
|     component: model.Component, | ||||
| ) -> str: | ||||
|     w.newline() | ||||
|     out = outfile(target, component) | ||||
|     objs: list[str] = compileSrc(w, target, component) | ||||
|     res = compileRes(w, target, component) | ||||
|     libs = collectLibs(registry, target, component) | ||||
|     if component.type == model.Kind.LIB: | ||||
|         w.build(out, "ar", objs, implicit=res) | ||||
|     else: | ||||
|         w.build(out, "ld", objs + libs, implicit=res) | ||||
|     return out | ||||
| 
 | ||||
| 
 | ||||
| # --- Phony ------------------------------------------------------------------ # | ||||
| 
 | ||||
| 
 | ||||
| def all(w: ninja.Writer, registry: model.Registry, target: model.Target) -> list[str]: | ||||
|     all: list[str] = [] | ||||
|     for c in registry.iterEnabled(target): | ||||
|         all.append(link(w, registry, target, c)) | ||||
|     w.build("all", "phony", all) | ||||
|     w.default("all") | ||||
|     return all | ||||
| 
 | ||||
| 
 | ||||
| def gen(out: TextIO, target: model.Target, registry: model.Registry): | ||||
|     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("Tools") | ||||
| 
 | ||||
|     w.variable("cincs", " ".join(aggregateCincs(target, registry))) | ||||
|     w.variable("cdefs", " ".join(aggregateCdefs(target))) | ||||
|     w.newline() | ||||
| 
 | ||||
|     w.rule("cp", "cp $in $out") | ||||
|     for i in target.tools: | ||||
|         tool = target.tools[i] | ||||
|         rule = rules.rules[i] | ||||
|         writer.variable(i, tool.cmd) | ||||
|         writer.variable(i + "flags", " ".join(rule.args + tool.args)) | ||||
|         writer.rule( | ||||
|         w.variable(i, tool.cmd) | ||||
|         w.variable(i + "flags", " ".join(rule.args + tool.args)) | ||||
|         w.rule( | ||||
|             i, | ||||
|             f"{tool.cmd} {rule.rule.replace('$flags',f'${i}flags')}", | ||||
|             depfile=rule.deps, | ||||
|         ) | ||||
|         writer.newline() | ||||
|         w.newline() | ||||
| 
 | ||||
|     writer.separator("Components") | ||||
|     w.separator("Build") | ||||
| 
 | ||||
|     all: list[str] = [] | ||||
|     all(w, registry, target) | ||||
| 
 | ||||
|     for instance in context.enabledInstances(): | ||||
|         objects = instance.objsfiles() | ||||
|         assets = instance.resfiles() | ||||
|         writer.comment(f"Component: {instance.manifest.id}") | ||||
|         writer.comment(f"Resolved: {', '.join(instance.resolved)}") | ||||
| 
 | ||||
|         for obj in objects: | ||||
|             r = rules.byFileIn(obj[0]) | ||||
|             if r is None: | ||||
|                 raise RuntimeError(f"Unknown rule for file {obj[0]}") | ||||
|             t = target.tools[r.id] | ||||
|             writer.build(obj[1], r.id, obj[0], order_only=t.files) | ||||
| 
 | ||||
|         for asset in assets: | ||||
|             writer.build(asset[1], "cp", asset[0]) | ||||
| 
 | ||||
|         writer.newline() | ||||
| 
 | ||||
|         if instance.isLib(): | ||||
|             writer.build( | ||||
|                 instance.outfile(), | ||||
|                 "ar", | ||||
|                 list(map(lambda o: o[1], objects)), | ||||
|                 implicit=list(map(lambda o: o[1], assets)), | ||||
|             ) | ||||
|         else: | ||||
|             libraries: list[str] = [] | ||||
| 
 | ||||
|             for req in instance.resolved: | ||||
|                 reqInstance = context.componentByName(req) | ||||
| 
 | ||||
|                 if reqInstance is None: | ||||
|                     raise RuntimeError(f"Component {req} not found") | ||||
| 
 | ||||
|                 if not reqInstance.isLib(): | ||||
|                     raise RuntimeError(f"Component {req} is not a library") | ||||
| 
 | ||||
|                 libraries.append(reqInstance.outfile()) | ||||
| 
 | ||||
|             writer.build( | ||||
|                 instance.outfile(), | ||||
|                 "ld", | ||||
|                 list(map(lambda o: o[1], objects)) + libraries, | ||||
|                 implicit=list(map(lambda o: o[1], assets)), | ||||
|             ) | ||||
| 
 | ||||
|             all.append(instance.outfile()) | ||||
| 
 | ||||
|         writer.newline() | ||||
| 
 | ||||
|     writer.separator("Phony targets") | ||||
| 
 | ||||
|     writer.build("all", "phony", all) | ||||
|     writer.default("all") | ||||
| @dataclass | ||||
| class Product: | ||||
|     path: Path | ||||
|     target: model.Target | ||||
|     component: model.Component | ||||
| 
 | ||||
| 
 | ||||
| def build( | ||||
|     componentSpec: str, targetSpec: str, props: model.Props = {} | ||||
| ) -> context.ComponentInstance: | ||||
|     ctx = context.contextFor(targetSpec, props) | ||||
| 
 | ||||
|     shell.mkdir(ctx.builddir()) | ||||
|     ninjaPath = os.path.join(ctx.builddir(), "build.ninja") | ||||
| 
 | ||||
|     target: model.Target, | ||||
|     registry: model.Registry, | ||||
|     components: Union[list[model.Component], model.Component, None] = None, | ||||
| ) -> list[Product]: | ||||
|     all = False | ||||
|     shell.mkdir(target.builddir) | ||||
|     ninjaPath = os.path.join(target.builddir, "build.ninja") | ||||
|     with open(ninjaPath, "w") as f: | ||||
|         gen(f, ctx) | ||||
|         gen(f, target, registry) | ||||
| 
 | ||||
|     instance = ctx.componentByName(componentSpec) | ||||
|     if components is None: | ||||
|         all = True | ||||
|         components = list(registry.iterEnabled(target)) | ||||
| 
 | ||||
|     if instance is None: | ||||
|         raise RuntimeError(f"Component {componentSpec} not found") | ||||
|     if isinstance(components, model.Component): | ||||
|         components = [components] | ||||
| 
 | ||||
|     if not instance.enabled: | ||||
|         raise RuntimeError( | ||||
|             f"Component {componentSpec} is disabled: {instance.disableReason}" | ||||
|     products: list[Product] = [] | ||||
|     for c in components: | ||||
|         products.append( | ||||
|             Product( | ||||
|                 path=Path(outfile(target, c)), | ||||
|                 target=target, | ||||
|                 component=c, | ||||
|             ) | ||||
|         ) | ||||
| 
 | ||||
|     shell.exec("ninja", "-f", ninjaPath, instance.outfile()) | ||||
| 
 | ||||
|     return instance | ||||
|     outs = list(map(lambda p: str(p.path), products)) | ||||
|     if all: | ||||
|         shell.exec("ninja", "-v", "-f", ninjaPath) | ||||
|     else: | ||||
|         shell.exec("ninja", "-v", "-f", ninjaPath, *outs) | ||||
|     return products | ||||
| 
 | ||||
| 
 | ||||
| class Paths: | ||||
|     bin: str | ||||
|     lib: str | ||||
|     obj: str | ||||
| 
 | ||||
|     def __init__(self, bin: str, lib: str, obj: str): | ||||
|         self.bin = bin | ||||
|         self.lib = lib | ||||
|         self.obj = obj | ||||
| # --- Commands --------------------------------------------------------------- # | ||||
| 
 | ||||
| 
 | ||||
| def buildAll(targetSpec: str, props: model.Props = {}) -> context.Context: | ||||
|     ctx = context.contextFor(targetSpec, props) | ||||
| 
 | ||||
|     shell.mkdir(ctx.builddir()) | ||||
|     ninjaPath = os.path.join(ctx.builddir(), "build.ninja") | ||||
| 
 | ||||
|     with open(ninjaPath, "w") as f: | ||||
|         gen(f, ctx) | ||||
| 
 | ||||
|     shell.exec("ninja", "-v", "-f", ninjaPath) | ||||
| 
 | ||||
|     return ctx | ||||
| @cli.command("b", "build", "Build a component or all components") | ||||
| def buildCmd(args: cli.Args): | ||||
|     registry = model.Registry.use(args) | ||||
|     target = model.Target.use(args) | ||||
|     componentSpec = args.consumeArg() | ||||
|     if componentSpec is None: | ||||
|         raise RuntimeError("No component specified") | ||||
|     component = registry.lookup(componentSpec, model.Component) | ||||
|     build(target, registry, component)[0] | ||||
| 
 | ||||
| 
 | ||||
| def testAll(targetSpec: str): | ||||
|     ctx = context.contextFor(targetSpec) | ||||
| @cli.command("p", "project", "Show project information") | ||||
| def runCmd(args: cli.Args): | ||||
|     registry = model.Registry.use(args) | ||||
|     target = model.Target.use(args) | ||||
|     debug = args.consumeOpt("debug", False) is True | ||||
| 
 | ||||
|     shell.mkdir(ctx.builddir()) | ||||
|     ninjaPath = os.path.join(ctx.builddir(), "build.ninja") | ||||
|     componentSpec = args.consumeArg() | ||||
|     if componentSpec is None: | ||||
|         raise RuntimeError("No component specified") | ||||
| 
 | ||||
|     with open(ninjaPath, "w") as f: | ||||
|         gen(f, ctx) | ||||
|     component = registry.lookup(componentSpec, model.Component) | ||||
|     if component is None: | ||||
|         raise RuntimeError(f"Component {componentSpec} not found") | ||||
| 
 | ||||
|     shell.exec("ninja", "-v", "-f", ninjaPath, "all") | ||||
|     product = build(target, registry, component)[0] | ||||
| 
 | ||||
|     for instance in ctx.enabledInstances(): | ||||
|         if instance.isLib(): | ||||
|             continue | ||||
|     os.environ["CK_TARGET"] = target.id | ||||
|     os.environ["CK_COMPONENT"] = product.component.id | ||||
|     os.environ["CK_BUILDDIR"] = target.builddir | ||||
| 
 | ||||
|         if instance.id().endswith("-tests"): | ||||
|             print(f"Running {instance.id()}") | ||||
|             shell.exec(instance.outfile()) | ||||
|     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): | ||||
|     # 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) | ||||
| 
 | ||||
| 
 | ||||
| @cli.command("d", "debug", "Debug a component") | ||||
| def debugCmd(args: cli.Args): | ||||
|     # This is just a wrapper around the `run` command that | ||||
|     # always enable debug mode. | ||||
|     args.opts["debug"] = True | ||||
|     runCmd(args) | ||||
| 
 | ||||
| 
 | ||||
| @cli.command("c", "clean", "Clean build files") | ||||
| def cleanCmd(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): | ||||
|     model.Project.use(args) | ||||
|     shell.rmrf(const.PROJECT_CK_DIR) | ||||
|  |  | |||
|  | @ -70,7 +70,7 @@ Callback = Callable[[Args], None] | |||
| 
 | ||||
| @dataclass | ||||
| class Command: | ||||
|     shortName: str | ||||
|     shortName: Optional[str] | ||||
|     longName: str | ||||
|     helpText: str | ||||
|     isPlugin: bool | ||||
|  | @ -86,7 +86,7 @@ def append(command: Command): | |||
|     commands.sort(key=lambda c: c.shortName or c.longName) | ||||
| 
 | ||||
| 
 | ||||
| def command(shortName: str, longName: str, helpText: str): | ||||
| def command(shortName: Optional[str], longName: str, helpText: str): | ||||
|     curframe = inspect.currentframe() | ||||
|     calframe = inspect.getouterframes(curframe, 2) | ||||
| 
 | ||||
|  | @ -103,7 +103,7 @@ def command(shortName: str, longName: str, helpText: str): | |||
| 
 | ||||
| 
 | ||||
| @command("u", "usage", "Show usage information") | ||||
| def usage(args: Args | None = None): | ||||
| def usage(args: Optional[Args] = None): | ||||
|     print(f"Usage: {const.ARGV0} <command> [args...]") | ||||
| 
 | ||||
| 
 | ||||
|  | @ -122,7 +122,7 @@ def helpCmd(args: Args): | |||
| 
 | ||||
|     print() | ||||
|     vt100.title("Commands") | ||||
|     for cmd in commands: | ||||
|     for cmd in sorted(commands, key=lambda c: c.shortName or c.longName): | ||||
|         pluginText = "" | ||||
|         if cmd.isPlugin: | ||||
|             pluginText = f"{vt100.CYAN}(plugin){vt100.RESET}" | ||||
|  | @ -140,7 +140,7 @@ def helpCmd(args: Args): | |||
| 
 | ||||
| @command("v", "version", "Show current version") | ||||
| def versionCmd(args: Args): | ||||
|     print(f"CuteKit v{const.VERSION_STR}\n") | ||||
|     print(f"CuteKit v{const.VERSION_STR}") | ||||
| 
 | ||||
| 
 | ||||
| def exec(args: Args): | ||||
|  |  | |||
							
								
								
									
										196
									
								
								cutekit/cmds.py
									
										
									
									
									
								
							
							
						
						
									
										196
									
								
								cutekit/cmds.py
									
										
									
									
									
								
							|  | @ -1,196 +0,0 @@ | |||
| import logging | ||||
| import os | ||||
| 
 | ||||
| 
 | ||||
| from . import ( | ||||
|     context, | ||||
|     shell, | ||||
|     const, | ||||
|     vt100, | ||||
|     builder, | ||||
|     cli, | ||||
|     model, | ||||
|     jexpr, | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| _logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| @cli.command("p", "project", "Show project information") | ||||
| def runCmd(args: cli.Args): | ||||
|     model.Project.chdir() | ||||
| 
 | ||||
|     targetSpec = str(args.consumeOpt("target", "host-" + shell.uname().machine)) | ||||
|     props = args.consumePrefix("prop:") | ||||
| 
 | ||||
|     componentSpec = args.consumeArg() | ||||
| 
 | ||||
|     if componentSpec is None: | ||||
|         raise RuntimeError("Component not specified") | ||||
| 
 | ||||
|     component = builder.build(componentSpec, targetSpec, props) | ||||
| 
 | ||||
|     os.environ["CK_TARGET"] = component.context.target.id | ||||
|     os.environ["CK_COMPONENT"] = component.id() | ||||
|     os.environ["CK_BUILDDIR"] = component.context.builddir() | ||||
| 
 | ||||
|     shell.exec(component.outfile(), *args.args) | ||||
| 
 | ||||
| 
 | ||||
| @cli.command("t", "test", "Run all test targets") | ||||
| def testCmd(args: cli.Args): | ||||
|     model.Project.chdir() | ||||
| 
 | ||||
|     targetSpec = str(args.consumeOpt("target", "host-" + shell.uname().machine)) | ||||
|     builder.testAll(targetSpec) | ||||
| 
 | ||||
| 
 | ||||
| @cli.command("d", "debug", "Debug a component") | ||||
| def debugCmd(args: cli.Args): | ||||
|     model.Project.chdir() | ||||
| 
 | ||||
|     targetSpec = str(args.consumeOpt("target", "host-" + shell.uname().machine)) | ||||
|     props = args.consumePrefix("prop:") | ||||
| 
 | ||||
|     componentSpec = args.consumeArg() | ||||
| 
 | ||||
|     if componentSpec is None: | ||||
|         raise RuntimeError("Component not specified") | ||||
| 
 | ||||
|     component = builder.build(componentSpec, targetSpec, props) | ||||
| 
 | ||||
|     os.environ["CK_TARGET"] = component.context.target.id | ||||
|     os.environ["CK_COMPONENT"] = component.id() | ||||
|     os.environ["CK_BUILDDIR"] = component.context.builddir() | ||||
| 
 | ||||
|     shell.exec("lldb", "-o", "run", component.outfile(), *args.args) | ||||
| 
 | ||||
| 
 | ||||
| @cli.command("b", "build", "Build a component or all components") | ||||
| def buildCmd(args: cli.Args): | ||||
|     model.Project.chdir() | ||||
| 
 | ||||
|     targetSpec = str(args.consumeOpt("target", "host-" + shell.uname().machine)) | ||||
|     props = args.consumePrefix("prop:") | ||||
|     componentSpec = args.consumeArg() | ||||
| 
 | ||||
|     if componentSpec is None: | ||||
|         builder.buildAll(targetSpec, props) | ||||
|     else: | ||||
|         builder.build(componentSpec, targetSpec, props) | ||||
| 
 | ||||
| 
 | ||||
| @cli.command("l", "list", "List all components and targets") | ||||
| def listCmd(args: cli.Args): | ||||
|     model.Project.chdir() | ||||
| 
 | ||||
|     components = context.loadAllComponents() | ||||
|     targets = context.loadAllTargets() | ||||
| 
 | ||||
|     vt100.title("Components") | ||||
|     if len(components) == 0: | ||||
|         print("   (No components available)") | ||||
|     else: | ||||
|         print(vt100.indent(vt100.wordwrap(", ".join(map(lambda m: m.id, components))))) | ||||
|     print() | ||||
| 
 | ||||
|     vt100.title("Targets") | ||||
| 
 | ||||
|     if len(targets) == 0: | ||||
|         print("   (No targets available)") | ||||
|     else: | ||||
|         print(vt100.indent(vt100.wordwrap(", ".join(map(lambda m: m.id, targets))))) | ||||
| 
 | ||||
|     print() | ||||
| 
 | ||||
| 
 | ||||
| @cli.command("c", "clean", "Clean build files") | ||||
| def cleanCmd(args: cli.Args): | ||||
|     model.Project.chdir() | ||||
|     shell.rmrf(const.BUILD_DIR) | ||||
| 
 | ||||
| 
 | ||||
| @cli.command("n", "nuke", "Clean all build files and caches") | ||||
| def nukeCmd(args: cli.Args): | ||||
|     model.Project.chdir() | ||||
|     shell.rmrf(const.PROJECT_CK_DIR) | ||||
| 
 | ||||
| 
 | ||||
| def grabExtern(extern: dict[str, model.Extern]): | ||||
|     for extSpec, ext in extern.items(): | ||||
|         extPath = os.path.join(const.EXTERN_DIR, extSpec) | ||||
| 
 | ||||
|         if os.path.exists(extPath): | ||||
|             print(f"Skipping {extSpec}, already installed") | ||||
|             continue | ||||
| 
 | ||||
|         print(f"Installing {extSpec}-{ext.tag} from {ext.git}...") | ||||
|         shell.popen( | ||||
|             "git", "clone", "--depth", "1", "--branch", ext.tag, ext.git, extPath | ||||
|         ) | ||||
| 
 | ||||
|         if os.path.exists(os.path.join(extPath, "project.json")): | ||||
|             grabExtern(context.loadProject(extPath).extern) | ||||
| 
 | ||||
| 
 | ||||
| @cli.command("i", "install", "Install required external packages") | ||||
| def installCmd(args: cli.Args): | ||||
|     model.Project.chdir() | ||||
|     pj = context.loadProject(".") | ||||
|     grabExtern(pj.extern) | ||||
| 
 | ||||
| 
 | ||||
| @cli.command("I", "init", "Initialize a new project") | ||||
| def initCmd(args: cli.Args): | ||||
|     import requests | ||||
| 
 | ||||
|     repo = args.consumeOpt("repo", const.DEFAULT_REPO_TEMPLATES) | ||||
|     list = args.consumeOpt("list") | ||||
| 
 | ||||
|     template = args.consumeArg() | ||||
|     name = args.consumeArg() | ||||
| 
 | ||||
|     _logger.info("Fetching registry...") | ||||
|     r = requests.get(f"https://raw.githubusercontent.com/{repo}/main/registry.json") | ||||
| 
 | ||||
|     if r.status_code != 200: | ||||
|         _logger.error("Failed to fetch registry") | ||||
|         exit(1) | ||||
| 
 | ||||
|     registry = r.json() | ||||
| 
 | ||||
|     if list: | ||||
|         print( | ||||
|             "\n".join(f"* {entry['id']} - {entry['description']}" for entry in registry) | ||||
|         ) | ||||
|         return | ||||
| 
 | ||||
|     if not template: | ||||
|         raise RuntimeError("Template not specified") | ||||
| 
 | ||||
|     def template_match(t: jexpr.Json) -> str: | ||||
|         return t["id"] == template | ||||
| 
 | ||||
|     if not any(filter(template_match, registry)): | ||||
|         raise LookupError(f"Couldn't find a template named {template}") | ||||
| 
 | ||||
|     if not name: | ||||
|         _logger.info(f"No name was provided, defaulting to {template}") | ||||
|         name = template | ||||
| 
 | ||||
|     if os.path.exists(name): | ||||
|         raise RuntimeError(f"Directory {name} already exists") | ||||
| 
 | ||||
|     print(f"Creating project {name} from template {template}...") | ||||
|     shell.cloneDir(f"https://github.com/{repo}", template, name) | ||||
|     print(f"Project {name} created\n") | ||||
| 
 | ||||
|     print("We suggest that you begin by typing:") | ||||
|     print(f"  {vt100.GREEN}cd {name}{vt100.RESET}") | ||||
|     print( | ||||
|         f"  {vt100.GREEN}cutekit install{vt100.BRIGHT_BLACK} # Install external packages{vt100.RESET}" | ||||
|     ) | ||||
|     print( | ||||
|         f"  {vt100.GREEN}cutekit build{vt100.BRIGHT_BLACK}  # Build the project{vt100.RESET}" | ||||
|     ) | ||||
|  | @ -1,351 +0,0 @@ | |||
| from typing import cast, Optional, Protocol, Iterable | ||||
| from itertools import chain | ||||
| from pathlib import Path | ||||
| import os | ||||
| import logging | ||||
| 
 | ||||
| 
 | ||||
| from . import const, shell, jexpr, utils, rules, mixins, model | ||||
| 
 | ||||
| _logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| class IContext(Protocol): | ||||
|     target: model.Target | ||||
| 
 | ||||
|     def builddir(self) -> str: | ||||
|         ... | ||||
| 
 | ||||
| 
 | ||||
| class ComponentInstance: | ||||
|     enabled: bool = True | ||||
|     disableReason = "" | ||||
|     manifest: model.Component | ||||
|     sources: list[str] = [] | ||||
|     res: list[str] = [] | ||||
|     resolved: list[str] = [] | ||||
|     context: IContext | ||||
| 
 | ||||
|     def __init__( | ||||
|         self, | ||||
|         enabled: bool, | ||||
|         disableReason: str, | ||||
|         manifest: model.Component, | ||||
|         sources: list[str], | ||||
|         res: list[str], | ||||
|         resolved: list[str], | ||||
|     ): | ||||
|         self.enabled = enabled | ||||
|         self.disableReason = disableReason | ||||
|         self.manifest = manifest | ||||
|         self.sources = sources | ||||
|         self.res = res | ||||
|         self.resolved = resolved | ||||
| 
 | ||||
|     def id(self) -> str: | ||||
|         return self.manifest.id | ||||
| 
 | ||||
|     def isLib(self): | ||||
|         return self.manifest.type == model.Kind.LIB | ||||
| 
 | ||||
|     def objdir(self) -> str: | ||||
|         return os.path.join(self.context.builddir(), f"{self.manifest.id}/obj") | ||||
| 
 | ||||
|     def resdir(self) -> str: | ||||
|         return os.path.join(self.context.builddir(), f"{self.manifest.id}/res") | ||||
| 
 | ||||
|     def objsfiles(self) -> list[tuple[str, str]]: | ||||
|         def toOFile(s: str) -> str: | ||||
|             return os.path.join( | ||||
|                 self.objdir(), | ||||
|                 s.replace(os.path.join(self.manifest.dirname(), ""), "") + ".o", | ||||
|             ) | ||||
| 
 | ||||
|         return list(map(lambda s: (s, toOFile(s)), self.sources)) | ||||
| 
 | ||||
|     def resfiles(self) -> list[tuple[str, str, str]]: | ||||
|         def toAssetFile(s: str) -> str: | ||||
|             return os.path.join( | ||||
|                 self.resdir(), | ||||
|                 s.replace(os.path.join(self.manifest.dirname(), "res/"), ""), | ||||
|             ) | ||||
| 
 | ||||
|         def toAssetId(s: str) -> str: | ||||
|             return s.replace(os.path.join(self.manifest.dirname(), "res/"), "") | ||||
| 
 | ||||
|         return list(map(lambda s: (s, toAssetFile(s), toAssetId(s)), self.res)) | ||||
| 
 | ||||
|     def outfile(self) -> str: | ||||
|         if self.isLib(): | ||||
|             return os.path.join( | ||||
|                 self.context.builddir(), self.manifest.id, f"lib/{self.manifest.id}.a" | ||||
|             ) | ||||
|         else: | ||||
|             return os.path.join( | ||||
|                 self.context.builddir(), self.manifest.id, f"bin/{self.manifest.id}.out" | ||||
|             ) | ||||
| 
 | ||||
|     def cinclude(self) -> str: | ||||
|         if "cpp-root-include" in self.manifest.props: | ||||
|             return self.manifest.dirname() | ||||
|         elif self.manifest.type == model.Kind.LIB: | ||||
|             return str(Path(self.manifest.dirname()).parent) | ||||
|         else: | ||||
|             return "" | ||||
| 
 | ||||
| 
 | ||||
| class Context(IContext): | ||||
|     target: model.Target | ||||
|     instances: list[ComponentInstance] | ||||
|     tools: model.Tools | ||||
| 
 | ||||
|     def enabledInstances(self) -> Iterable[ComponentInstance]: | ||||
|         return filter(lambda x: x.enabled, self.instances) | ||||
| 
 | ||||
|     def __init__( | ||||
|         self, | ||||
|         target: model.Target, | ||||
|         instances: list[ComponentInstance], | ||||
|         tools: model.Tools, | ||||
|     ): | ||||
|         self.target = target | ||||
|         self.instances = instances | ||||
|         self.tools = tools | ||||
| 
 | ||||
|     def componentByName(self, name: str) -> Optional[ComponentInstance]: | ||||
|         result = list(filter(lambda x: x.manifest.id == name, self.instances)) | ||||
|         if len(result) == 0: | ||||
|             return None | ||||
|         return result[0] | ||||
| 
 | ||||
|     def cincls(self) -> list[str]: | ||||
|         includes = list( | ||||
|             filter( | ||||
|                 lambda x: x != "", map(lambda x: x.cinclude(), self.enabledInstances()) | ||||
|             ) | ||||
|         ) | ||||
|         return utils.uniq(includes) | ||||
| 
 | ||||
|     def cdefs(self) -> list[str]: | ||||
|         return self.target.cdefs() | ||||
| 
 | ||||
|     def hashid(self) -> str: | ||||
|         return utils.hash( | ||||
|             (self.target.props, [self.tools[t].to_dict() for t in self.tools]) | ||||
|         )[0:8] | ||||
| 
 | ||||
|     def builddir(self) -> str: | ||||
|         return os.path.join(const.BUILD_DIR, f"{self.target.id}-{self.hashid()[:8]}") | ||||
| 
 | ||||
| 
 | ||||
| def loadAllTargets() -> list[model.Target]: | ||||
|     projectRoot = model.Project.root() | ||||
|     if projectRoot is None: | ||||
|         return [] | ||||
| 
 | ||||
|     pj = loadProject(projectRoot) | ||||
|     paths = list( | ||||
|         map( | ||||
|             lambda e: os.path.join(const.EXTERN_DIR, e, const.TARGETS_DIR), | ||||
|             pj.extern.keys(), | ||||
|         ) | ||||
|     ) + [const.TARGETS_DIR] | ||||
| 
 | ||||
|     ret = [] | ||||
|     for entry in paths: | ||||
|         files = shell.find(entry, ["*.json"]) | ||||
|         ret += list( | ||||
|             map( | ||||
|                 lambda path: model.Manifest.load(Path(path)).ensureType(model.Target), | ||||
|                 files, | ||||
|             ) | ||||
|         ) | ||||
| 
 | ||||
|     return ret | ||||
| 
 | ||||
| 
 | ||||
| def loadProject(path: str) -> model.Project: | ||||
|     path = os.path.join(path, "project.json") | ||||
|     return model.Manifest.load(Path(path)).ensureType(model.Project) | ||||
| 
 | ||||
| 
 | ||||
| def loadTarget(id: str) -> model.Target: | ||||
|     try: | ||||
|         return next(filter(lambda t: t.id == id, loadAllTargets())) | ||||
|     except StopIteration: | ||||
|         raise RuntimeError(f"Target '{id}' not found") | ||||
| 
 | ||||
| 
 | ||||
| def loadAllComponents() -> list[model.Component]: | ||||
|     files = shell.find(const.SRC_DIR, ["manifest.json"]) | ||||
|     files += shell.find(const.EXTERN_DIR, ["manifest.json"]) | ||||
| 
 | ||||
|     return list( | ||||
|         map( | ||||
|             lambda path: model.Manifest.load(Path(path)).ensureType(model.Component), | ||||
|             files, | ||||
|         ) | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| def filterDisabled( | ||||
|     components: list[model.Component], target: model.Target | ||||
| ) -> tuple[list[model.Component], list[model.Component]]: | ||||
|     return list(filter(lambda c: c.isEnabled(target)[0], components)), list( | ||||
|         filter(lambda c: not c.isEnabled(target)[0], components) | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| def providerFor( | ||||
|     what: str, components: list[model.Component] | ||||
| ) -> tuple[Optional[str], str]: | ||||
|     result: list[model.Component] = list(filter(lambda c: c.id == what, components)) | ||||
| 
 | ||||
|     if len(result) == 0: | ||||
|         # Try to find a provider | ||||
|         result = list(filter(lambda x: (what in x.provides), components)) | ||||
| 
 | ||||
|     if len(result) == 0: | ||||
|         _logger.error(f"No provider for '{what}'") | ||||
|         return (None, f"No provider for '{what}'") | ||||
| 
 | ||||
|     if len(result) > 1: | ||||
|         ids = list(map(lambda x: x.id, result)) | ||||
|         _logger.error(f"Multiple providers for '{what}': {result}") | ||||
|         return (None, f"Multiple providers for '{what}': {','.join(ids)}") | ||||
| 
 | ||||
|     return (result[0].id, "") | ||||
| 
 | ||||
| 
 | ||||
| def resolveDeps( | ||||
|     componentSpec: str, components: list[model.Component], target: model.Target | ||||
| ) -> tuple[bool, str, list[str]]: | ||||
|     mapping = dict(map(lambda c: (c.id, c), components)) | ||||
| 
 | ||||
|     def resolveInner(what: str, stack: list[str] = []) -> tuple[bool, str, list[str]]: | ||||
|         result: list[str] = [] | ||||
|         what = target.route(what) | ||||
|         resolved, unresolvedReason = providerFor(what, components) | ||||
| 
 | ||||
|         if resolved is None: | ||||
|             return False, unresolvedReason, [] | ||||
| 
 | ||||
|         if resolved in stack: | ||||
|             raise RuntimeError(f"Dependency loop: {stack} -> {resolved}") | ||||
| 
 | ||||
|         stack.append(resolved) | ||||
| 
 | ||||
|         for req in mapping[resolved].requires: | ||||
|             keep, unresolvedReason, reqs = resolveInner(req, stack) | ||||
| 
 | ||||
|             if not keep: | ||||
|                 stack.pop() | ||||
|                 _logger.error(f"Dependency '{req}' not met for '{resolved}'") | ||||
|                 return False, unresolvedReason, [] | ||||
| 
 | ||||
|             result.extend(reqs) | ||||
| 
 | ||||
|         stack.pop() | ||||
|         result.insert(0, resolved) | ||||
| 
 | ||||
|         return True, "", result | ||||
| 
 | ||||
|     enabled, unresolvedReason, resolved = resolveInner(componentSpec) | ||||
| 
 | ||||
|     return enabled, unresolvedReason, utils.uniq(resolved) | ||||
| 
 | ||||
| 
 | ||||
| def instanciate( | ||||
|     componentSpec: str, components: list[model.Component], target: model.Target | ||||
| ) -> Optional[ComponentInstance]: | ||||
|     manifest = next(filter(lambda c: c.id == componentSpec, components)) | ||||
|     wildcards = set(chain(*map(lambda rule: rule.fileIn, rules.rules.values()))) | ||||
|     dirs = [manifest.dirname()] + list( | ||||
|         map(lambda d: os.path.join(manifest.dirname(), d), manifest.subdirs) | ||||
|     ) | ||||
|     sources = shell.find(dirs, list(wildcards), recusive=False) | ||||
| 
 | ||||
|     res = shell.find(os.path.join(manifest.dirname(), "res")) | ||||
| 
 | ||||
|     enabled, unresolvedReason, resolved = resolveDeps(componentSpec, components, target) | ||||
| 
 | ||||
|     return ComponentInstance( | ||||
|         enabled, unresolvedReason, manifest, sources, res, resolved[1:] | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| def instanciateDisabled( | ||||
|     component: model.Component, target: model.Target | ||||
| ) -> ComponentInstance: | ||||
|     return ComponentInstance( | ||||
|         enabled=False, | ||||
|         disableReason=component.isEnabled(target)[1], | ||||
|         manifest=component, | ||||
|         sources=[], | ||||
|         res=[], | ||||
|         resolved=[], | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| context: dict[str, Context] = {} | ||||
| 
 | ||||
| 
 | ||||
| def contextFor(targetSpec: str, props: model.Props = {}) -> Context: | ||||
|     if targetSpec in context: | ||||
|         return context[targetSpec] | ||||
| 
 | ||||
|     _logger.info(f"Loading context for '{targetSpec}'") | ||||
| 
 | ||||
|     targetEls = targetSpec.split(":") | ||||
| 
 | ||||
|     if targetEls[0] == "": | ||||
|         targetEls[0] = "host-" + shell.uname().machine | ||||
| 
 | ||||
|     target = loadTarget(targetEls[0]) | ||||
|     target.props |= props | ||||
| 
 | ||||
|     components = loadAllComponents() | ||||
|     components, disabled = filterDisabled(components, target) | ||||
| 
 | ||||
|     tools: model.Tools = {} | ||||
| 
 | ||||
|     for toolSpec in target.tools: | ||||
|         tool = target.tools[toolSpec] | ||||
| 
 | ||||
|         tools[toolSpec] = model.Tool(cmd=tool.cmd, args=tool.args, files=tool.files) | ||||
| 
 | ||||
|         tools[toolSpec].args += rules.rules[toolSpec].args | ||||
| 
 | ||||
|     for m in targetEls[1:]: | ||||
|         mixin = mixins.byId(m) | ||||
|         tools = mixin(target, tools) | ||||
| 
 | ||||
|     for component in components: | ||||
|         for toolSpec in component.tools: | ||||
|             tool = component.tools[toolSpec] | ||||
|             tools[toolSpec].args += tool.args | ||||
| 
 | ||||
|     instances: list[ComponentInstance] = list( | ||||
|         map(lambda c: instanciateDisabled(c, target), disabled) | ||||
|     ) | ||||
| 
 | ||||
|     instances += cast( | ||||
|         list[ComponentInstance], | ||||
|         list( | ||||
|             filter( | ||||
|                 lambda e: e is not None, | ||||
|                 map(lambda c: instanciate(c.id, components, target), components), | ||||
|             ) | ||||
|         ), | ||||
|     ) | ||||
| 
 | ||||
|     context[targetSpec] = Context( | ||||
|         target, | ||||
|         instances, | ||||
|         tools, | ||||
|     ) | ||||
| 
 | ||||
|     for instance in instances: | ||||
|         instance.context = context[targetSpec] | ||||
| 
 | ||||
|     return context[targetSpec] | ||||
|  | @ -1,96 +1,95 @@ | |||
| import os | ||||
| 
 | ||||
| from typing import cast | ||||
| from . import vt100, context, cli, shell, model | ||||
| from typing import Optional, cast | ||||
| 
 | ||||
| from . import vt100, cli, model | ||||
| 
 | ||||
| 
 | ||||
| def view( | ||||
|     context: context.Context, | ||||
|     scope: str | None = None, | ||||
|     registry: model.Registry, | ||||
|     target: model.Target, | ||||
|     scope: Optional[str] = None, | ||||
|     showExe: bool = True, | ||||
|     showDisabled: bool = False, | ||||
| ): | ||||
|     from graphviz import Digraph  # type: ignore | ||||
| 
 | ||||
|     g = Digraph(context.target.id, filename="graph.gv") | ||||
|     g = Digraph(target.id, filename="graph.gv") | ||||
| 
 | ||||
|     g.attr("graph", splines="ortho", rankdir="BT", ranksep="1.5") | ||||
|     g.attr("node", shape="ellipse") | ||||
|     g.attr( | ||||
|         "graph", | ||||
|         label=f"<<B>{scope or 'Full Dependency Graph'}</B><BR/>{context.target.id}>", | ||||
|         label=f"<<B>{scope or 'Full Dependency Graph'}</B><BR/>{target.id}>", | ||||
|         labelloc="t", | ||||
|     ) | ||||
| 
 | ||||
|     scopeInstance = None | ||||
| 
 | ||||
|     if scope is not None: | ||||
|         scopeInstance = context.componentByName(scope) | ||||
|         scopeInstance = registry.lookup(scope, model.Component) | ||||
| 
 | ||||
|     for instance in context.instances: | ||||
|         if not instance.isLib() and not showExe: | ||||
|     for component in registry.iterEnabled(target): | ||||
|         if not component.type == model.Kind.LIB and not showExe: | ||||
|             continue | ||||
| 
 | ||||
|         if ( | ||||
|             scopeInstance is not None | ||||
|             and instance.manifest.id != scope | ||||
|             and instance.manifest.id not in scopeInstance.resolved | ||||
|             and component.id != scope | ||||
|             and component.id not in scopeInstance.resolved[target.id].resolved | ||||
|         ): | ||||
|             continue | ||||
| 
 | ||||
|         if instance.enabled: | ||||
|             fillcolor = "lightgrey" if instance.isLib() else "lightblue" | ||||
|             shape = "plaintext" if not scope == instance.manifest.id else "box" | ||||
|         if component.resolved[target.id].enabled: | ||||
|             fillcolor = "lightgrey" if component.type == model.Kind.LIB else "lightblue" | ||||
|             shape = "plaintext" if not scope == component.id else "box" | ||||
| 
 | ||||
|             g.node( | ||||
|                 instance.manifest.id, | ||||
|                 f"<<B>{instance.manifest.id}</B><BR/>{vt100.wordwrap(instance.manifest.decription, 40,newline='<BR/>')}>", | ||||
|                 component.id, | ||||
|                 f"<<B>{component.id}</B><BR/>{vt100.wordwrap(component.decription, 40,newline='<BR/>')}>", | ||||
|                 shape=shape, | ||||
|                 style="filled", | ||||
|                 fillcolor=fillcolor, | ||||
|             ) | ||||
| 
 | ||||
|             for req in instance.manifest.requires: | ||||
|                 g.edge(instance.manifest.id, req) | ||||
|             for req in component.requires: | ||||
|                 g.edge(component.id, req) | ||||
| 
 | ||||
|             for req in instance.manifest.provides: | ||||
|                 isChosen = context.target.routing.get(req, None) == instance.manifest.id | ||||
|             for req in component.provides: | ||||
|                 isChosen = target.routing.get(req, None) == component.id | ||||
| 
 | ||||
|                 g.edge( | ||||
|                     req, | ||||
|                     instance.manifest.id, | ||||
|                     component.id, | ||||
|                     arrowhead="none", | ||||
|                     color=("blue" if isChosen else "black"), | ||||
|                 ) | ||||
|         elif showDisabled: | ||||
|             g.node( | ||||
|                 instance.manifest.id, | ||||
|                 f"<<B>{instance.manifest.id}</B><BR/>{vt100.wordwrap(instance.manifest.decription, 40,newline='<BR/>')}<BR/><BR/><I>{vt100.wordwrap(instance.disableReason, 40,newline='<BR/>')}</I>>", | ||||
|                 component.id, | ||||
|                 f"<<B>{component.id}</B><BR/>{vt100.wordwrap(component.decription, 40,newline='<BR/>')}<BR/><BR/><I>{vt100.wordwrap(str(component.resolved[target.id].reason), 40,newline='<BR/>')}</I>>", | ||||
|                 shape="plaintext", | ||||
|                 style="filled", | ||||
|                 fontcolor="#999999", | ||||
|                 fillcolor="#eeeeee", | ||||
|             ) | ||||
| 
 | ||||
|             for req in instance.manifest.requires: | ||||
|                 g.edge(instance.manifest.id, req, color="#aaaaaa") | ||||
|             for req in component.requires: | ||||
|                 g.edge(component.id, req, color="#aaaaaa") | ||||
| 
 | ||||
|             for req in instance.manifest.provides: | ||||
|                 g.edge(req, instance.manifest.id, arrowhead="none", color="#aaaaaa") | ||||
|             for req in component.provides: | ||||
|                 g.edge(req, component.id, arrowhead="none", color="#aaaaaa") | ||||
| 
 | ||||
|     g.view(filename=os.path.join(context.builddir(), "graph.gv")) | ||||
|     g.view(filename=os.path.join(target.builddir, "graph.gv")) | ||||
| 
 | ||||
| 
 | ||||
| @cli.command("g", "graph", "Show the dependency graph") | ||||
| def graphCmd(args: cli.Args): | ||||
|     model.Project.chdir() | ||||
|     registry = model.Registry.use(args) | ||||
|     target = model.Target.use(args) | ||||
| 
 | ||||
|     targetSpec = str(args.consumeOpt("target", "host-" + shell.uname().machine)) | ||||
|     scope = cast(Optional[str], args.tryConsumeOpt("scope")) | ||||
|     onlyLibs = args.consumeOpt("only-libs", False) is True | ||||
|     showDisabled = args.consumeOpt("show-disabled", False) is True | ||||
| 
 | ||||
|     scope: str | None = cast(str | None, args.tryConsumeOpt("scope")) | ||||
|     onlyLibs: bool = args.consumeOpt("only-libs", False) is True | ||||
|     showDisabled: bool = args.consumeOpt("show-disabled", False) is True | ||||
| 
 | ||||
|     ctx = context.contextFor(targetSpec) | ||||
| 
 | ||||
|     view(ctx, scope=scope, showExe=not onlyLibs, showDisabled=showDisabled) | ||||
|     view(registry, target, scope=scope, showExe=not onlyLibs, showDisabled=showDisabled) | ||||
|  |  | |||
							
								
								
									
										488
									
								
								cutekit/model.py
									
										
									
									
									
								
							
							
						
						
									
										488
									
								
								cutekit/model.py
									
										
									
									
									
								
							|  | @ -3,12 +3,14 @@ import logging | |||
| 
 | ||||
| 
 | ||||
| from enum import Enum | ||||
| from typing import Any, Type, cast | ||||
| from typing import Any, Generator, Optional, Type, cast | ||||
| from pathlib import Path | ||||
| from dataclasses_json import DataClassJsonMixin, config | ||||
| from dataclasses_json import DataClassJsonMixin | ||||
| from dataclasses import dataclass, field | ||||
| 
 | ||||
| from . import jexpr, compat, utils | ||||
| from cutekit import const, shell | ||||
| 
 | ||||
| from . import jexpr, compat, utils, cli, vt100 | ||||
| 
 | ||||
| _logger = logging.getLogger(__name__) | ||||
| 
 | ||||
|  | @ -23,6 +25,9 @@ class Kind(Enum): | |||
|     EXE = "exe" | ||||
| 
 | ||||
| 
 | ||||
| # --- Manifest --------------------------------------------------------------- # | ||||
| 
 | ||||
| 
 | ||||
| @dataclass | ||||
| class Manifest(DataClassJsonMixin): | ||||
|     id: str | ||||
|  | @ -31,6 +36,9 @@ class Manifest(DataClassJsonMixin): | |||
| 
 | ||||
|     @staticmethod | ||||
|     def parse(path: Path, data: dict[str, Any]) -> "Manifest": | ||||
|         """ | ||||
|         Parse a manifest from a given path and data | ||||
|         """ | ||||
|         compat.ensureSupportedManifest(data, path) | ||||
|         kind = Kind(data["type"]) | ||||
|         del data["$schema"] | ||||
|  | @ -40,12 +48,24 @@ class Manifest(DataClassJsonMixin): | |||
| 
 | ||||
|     @staticmethod | ||||
|     def load(path: Path) -> "Manifest": | ||||
|         """ | ||||
|         Load a manifest from a given path | ||||
|         """ | ||||
|         return Manifest.parse(path, jexpr.evalRead(path)) | ||||
| 
 | ||||
|     def dirname(self) -> str: | ||||
|         """ | ||||
|         Return the directory of the manifest | ||||
|         """ | ||||
|         return os.path.dirname(self.path) | ||||
| 
 | ||||
|     def subpath(self, path) -> Path: | ||||
|         return Path(self.dirname()) / path | ||||
| 
 | ||||
|     def ensureType(self, t: Type[utils.T]) -> utils.T: | ||||
|         """ | ||||
|         Ensure that the manifest is of a given type | ||||
|         """ | ||||
|         if not isinstance(self, t): | ||||
|             raise RuntimeError( | ||||
|                 f"{self.path} should be a {type.__name__} manifest but is a {self.__class__.__name__} manifest" | ||||
|  | @ -53,6 +73,11 @@ class Manifest(DataClassJsonMixin): | |||
|         return cast(utils.T, self) | ||||
| 
 | ||||
| 
 | ||||
| # --- Project ---------------------------------------------------------------- # | ||||
| 
 | ||||
| _project: Optional["Project"] = None | ||||
| 
 | ||||
| 
 | ||||
| @dataclass | ||||
| class Extern(DataClassJsonMixin): | ||||
|     git: str | ||||
|  | @ -64,8 +89,16 @@ class Project(Manifest): | |||
|     description: str = field(default="(No description)") | ||||
|     extern: dict[str, Extern] = field(default_factory=dict) | ||||
| 
 | ||||
|     @property | ||||
|     def externDirs(self) -> list[str]: | ||||
|         res = map(lambda e: os.path.join(const.EXTERN_DIR, e), self.extern.keys()) | ||||
|         return list(res) | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def root() -> str | None: | ||||
|     def root() -> Optional[str]: | ||||
|         """ | ||||
|         Find the root of the project by looking for a project.json | ||||
|         """ | ||||
|         cwd = Path.cwd() | ||||
|         while str(cwd) != cwd.root: | ||||
|             if (cwd / "project.json").is_file(): | ||||
|  | @ -75,6 +108,9 @@ class Project(Manifest): | |||
| 
 | ||||
|     @staticmethod | ||||
|     def chdir() -> None: | ||||
|         """ | ||||
|         Change the current working directory to the root of the project | ||||
|         """ | ||||
|         path = Project.root() | ||||
|         if path is None: | ||||
|             raise RuntimeError( | ||||
|  | @ -82,6 +118,121 @@ class Project(Manifest): | |||
|             ) | ||||
|         os.chdir(path) | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def at(path: str) -> Optional["Project"]: | ||||
|         path = os.path.join(path, "project.json") | ||||
|         if not os.path.exists(path): | ||||
|             return None | ||||
|         return Manifest.load(Path(path)).ensureType(Project) | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def ensure() -> "Project": | ||||
|         root = Project.root() | ||||
|         if root is None: | ||||
|             raise RuntimeError( | ||||
|                 "No project.json found in this directory or any parent directory" | ||||
|             ) | ||||
|         os.chdir(root) | ||||
|         return Manifest.load(Path(os.path.join(root, "project.json"))).ensureType( | ||||
|             Project | ||||
|         ) | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def fetchs(extern: dict[str, Extern]): | ||||
|         for extSpec, ext in extern.items(): | ||||
|             extPath = os.path.join(const.EXTERN_DIR, extSpec) | ||||
| 
 | ||||
|             if os.path.exists(extPath): | ||||
|                 print(f"Skipping {extSpec}, already installed") | ||||
|                 continue | ||||
| 
 | ||||
|             print(f"Installing {extSpec}-{ext.tag} from {ext.git}...") | ||||
|             shell.popen( | ||||
|                 "git", | ||||
|                 "clone", | ||||
|                 "--quiet", | ||||
|                 "--depth", | ||||
|                 "1", | ||||
|                 "--branch", | ||||
|                 ext.tag, | ||||
|                 ext.git, | ||||
|                 extPath, | ||||
|             ) | ||||
|             project = Project.at(extPath) | ||||
|             if project is not None: | ||||
|                 Project.fetchs(project.extern) | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def use(args: cli.Args) -> "Project": | ||||
|         global _project | ||||
|         if _project is None: | ||||
|             _project = Project.ensure() | ||||
|         return _project | ||||
| 
 | ||||
| 
 | ||||
| @cli.command("i", "install", "Install required external packages") | ||||
| def installCmd(args: cli.Args): | ||||
|     project = Project.use(args) | ||||
|     Project.fetchs(project.extern) | ||||
| 
 | ||||
| 
 | ||||
| @cli.command("I", "init", "Initialize a new project") | ||||
| def initCmd(args: cli.Args): | ||||
|     import requests | ||||
| 
 | ||||
|     repo = args.consumeOpt("repo", const.DEFAULT_REPO_TEMPLATES) | ||||
|     list = args.consumeOpt("list") | ||||
| 
 | ||||
|     template = args.consumeArg() | ||||
|     name = args.consumeArg() | ||||
| 
 | ||||
|     _logger.info("Fetching registry...") | ||||
|     r = requests.get(f"https://raw.githubusercontent.com/{repo}/main/registry.json") | ||||
| 
 | ||||
|     if r.status_code != 200: | ||||
|         _logger.error("Failed to fetch registry") | ||||
|         exit(1) | ||||
| 
 | ||||
|     registry = r.json() | ||||
| 
 | ||||
|     if list: | ||||
|         print( | ||||
|             "\n".join(f"* {entry['id']} - {entry['description']}" for entry in registry) | ||||
|         ) | ||||
|         return | ||||
| 
 | ||||
|     if not template: | ||||
|         raise RuntimeError("Template not specified") | ||||
| 
 | ||||
|     def template_match(t: jexpr.Json) -> str: | ||||
|         return t["id"] == template | ||||
| 
 | ||||
|     if not any(filter(template_match, registry)): | ||||
|         raise LookupError(f"Couldn't find a template named {template}") | ||||
| 
 | ||||
|     if not name: | ||||
|         _logger.info(f"No name was provided, defaulting to {template}") | ||||
|         name = template | ||||
| 
 | ||||
|     if os.path.exists(name): | ||||
|         raise RuntimeError(f"Directory {name} already exists") | ||||
| 
 | ||||
|     print(f"Creating project {name} from template {template}...") | ||||
|     shell.cloneDir(f"https://github.com/{repo}", template, name) | ||||
|     print(f"Project {name} created\n") | ||||
| 
 | ||||
|     print("We suggest that you begin by typing:") | ||||
|     print(f"  {vt100.GREEN}cd {name}{vt100.RESET}") | ||||
|     print( | ||||
|         f"  {vt100.GREEN}cutekit install{vt100.BRIGHT_BLACK} # Install external packages{vt100.RESET}" | ||||
|     ) | ||||
|     print( | ||||
|         f"  {vt100.GREEN}cutekit build{vt100.BRIGHT_BLACK}  # Build the project{vt100.RESET}" | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| # --- Target ----------------------------------------------------------------- # | ||||
| 
 | ||||
| 
 | ||||
| @dataclass | ||||
| class Tool(DataClassJsonMixin): | ||||
|  | @ -99,30 +250,42 @@ class Target(Manifest): | |||
|     tools: Tools = field(default_factory=dict) | ||||
|     routing: dict[str, str] = field(default_factory=dict) | ||||
| 
 | ||||
|     @property | ||||
|     def hashid(self) -> str: | ||||
|         return utils.hash((self.props, [v.to_dict() for k, v in self.tools.items()])) | ||||
| 
 | ||||
|     @property | ||||
|     def builddir(self) -> str: | ||||
|         return os.path.join(const.BUILD_DIR, f"{self.id}-{self.hashid[:8]}") | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def use(args: cli.Args) -> "Target": | ||||
|         registry = Registry.use(args) | ||||
|         targetSpec = str(args.consumeOpt("target", "host-" + shell.uname().machine)) | ||||
|         return registry.ensure(targetSpec, Target) | ||||
| 
 | ||||
|     def route(self, componentSpec: str): | ||||
|         """ | ||||
|         Route a component spec to a target specific component spec | ||||
|         """ | ||||
|         return ( | ||||
|             self.routing[componentSpec] | ||||
|             if componentSpec in self.routing | ||||
|             else componentSpec | ||||
|         ) | ||||
| 
 | ||||
|     def cdefs(self) -> list[str]: | ||||
|         defines: list[str] = [] | ||||
| 
 | ||||
|         def sanatize(s: str) -> str: | ||||
|             return s.lower().replace(" ", "_").replace("-", "_").replace(".", "_") | ||||
| # --- Component -------------------------------------------------------------- # | ||||
| 
 | ||||
|         for key in self.props: | ||||
|             prop = self.props[key] | ||||
|             propStr = str(prop) | ||||
|             if isinstance(prop, bool): | ||||
|                 if prop: | ||||
|                     defines += [f"-D__ck_{sanatize(key)}__"] | ||||
|             else: | ||||
|                 defines += [f"-D__ck_{sanatize(key)}_{sanatize(propStr)}__"] | ||||
|                 defines += [f"-D__ck_{sanatize(key)}_value={propStr}"] | ||||
| 
 | ||||
|         return defines | ||||
| @dataclass | ||||
| class Resolved: | ||||
|     reason: Optional[str] = None | ||||
|     resolved: list[str] = field(default_factory=list) | ||||
| 
 | ||||
|     @property | ||||
|     def enabled(self) -> bool: | ||||
|         return self.reason is None | ||||
| 
 | ||||
| 
 | ||||
| @dataclass | ||||
|  | @ -134,6 +297,8 @@ class Component(Manifest): | |||
|     requires: list[str] = field(default_factory=list) | ||||
|     provides: list[str] = field(default_factory=list) | ||||
|     subdirs: list[str] = field(default_factory=list) | ||||
|     injects: list[str] = field(default_factory=list) | ||||
|     resolved: dict[str, Resolved] = field(default_factory=dict) | ||||
| 
 | ||||
|     def isEnabled(self, target: Target) -> tuple[bool, str]: | ||||
|         for k, v in self.enableIf.items(): | ||||
|  | @ -160,3 +325,290 @@ KINDS: dict[Kind, Type[Manifest]] = { | |||
|     Kind.LIB: Component, | ||||
|     Kind.EXE: Component, | ||||
| } | ||||
| 
 | ||||
| # --- Dependency resolution -------------------------------------------------- # | ||||
| 
 | ||||
| 
 | ||||
| @dataclass | ||||
| class Resolver: | ||||
|     _registry: "Registry" | ||||
|     _target: Target | ||||
|     _mappings: dict[str, list[Component]] = field(default_factory=dict) | ||||
|     _cache: dict[str, Resolved] = field(default_factory=dict) | ||||
|     _baked = False | ||||
| 
 | ||||
|     def _bake(self): | ||||
|         """ | ||||
|         Bake the resolver by building a mapping of all | ||||
|         components that provide a given spec. | ||||
|         """ | ||||
| 
 | ||||
|         if self._baked: | ||||
|             return | ||||
| 
 | ||||
|         for c in self._registry.iter(Component): | ||||
|             for p in c.provides + [c.id]: | ||||
|                 if p not in self._mappings and [0]: | ||||
|                     self._mappings[p] = [] | ||||
|                 self._mappings[p].append(c) | ||||
| 
 | ||||
|         # Overide with target routing since it has priority | ||||
|         # over component provides and id | ||||
|         for k, v in self._target.routing.items(): | ||||
|             component = self._registry.lookup(v, Component) | ||||
|             self._mappings[k] = [component] if component else [] | ||||
| 
 | ||||
|         self._baked = True | ||||
| 
 | ||||
|     def _provider(self, spec: str) -> tuple[Optional[str], str]: | ||||
|         """ | ||||
|         Returns the provider for a given spec. | ||||
|         """ | ||||
|         result = self._mappings.get(spec, []) | ||||
| 
 | ||||
|         if len(result) == 1: | ||||
|             enabled, reason = result[0].isEnabled(self._target) | ||||
|             if not enabled: | ||||
|                 return (None, reason) | ||||
| 
 | ||||
|         def checkIsEnabled(c: Component) -> bool: | ||||
|             enabled, reason = c.isEnabled(self._target) | ||||
|             if not enabled: | ||||
|                 _logger.info(f"Component {c.id} cannot provide '{spec}': {reason}") | ||||
|             return enabled | ||||
| 
 | ||||
|         result = list(filter(checkIsEnabled, result)) | ||||
| 
 | ||||
|         if result == []: | ||||
|             return (None, f"No provider for '{spec}'") | ||||
| 
 | ||||
|         if len(result) > 1: | ||||
|             ids = list(map(lambda x: x.id, result)) | ||||
|             return (None, f"Multiple providers for '{spec}': {','.join(ids)}") | ||||
| 
 | ||||
|         return (result[0].id, "") | ||||
| 
 | ||||
|     def resolve(self, what: str, stack: list[str] = []) -> Resolved: | ||||
|         """ | ||||
|         Resolve a given spec to a list of components. | ||||
|         """ | ||||
|         self._bake() | ||||
| 
 | ||||
|         if what in self._cache: | ||||
|             return self._cache[what] | ||||
| 
 | ||||
|         keep, unresolvedReason = self._provider(what) | ||||
| 
 | ||||
|         if not keep: | ||||
|             _logger.error(f"Dependency '{what}' not found: {unresolvedReason}") | ||||
|             self._cache[what] = Resolved(reason=unresolvedReason) | ||||
|             return self._cache[what] | ||||
| 
 | ||||
|         if keep in self._cache: | ||||
|             return self._cache[keep] | ||||
| 
 | ||||
|         if keep in stack: | ||||
|             raise RuntimeError( | ||||
|                 f"Dependency loop while resolving '{what}': {stack} -> {keep}" | ||||
|             ) | ||||
| 
 | ||||
|         stack.append(keep) | ||||
| 
 | ||||
|         component = self._registry.lookup(keep, Component) | ||||
|         if not component: | ||||
|             return Resolved(reason="No provider for 'myembed'") | ||||
| 
 | ||||
|         result: list[str] = [] | ||||
| 
 | ||||
|         for req in component.requires: | ||||
|             reqResolved = self.resolve(req, stack) | ||||
|             if reqResolved.reason: | ||||
|                 stack.pop() | ||||
| 
 | ||||
|                 self._cache[keep] = Resolved(reason=reqResolved.reason) | ||||
|                 return self._cache[keep] | ||||
| 
 | ||||
|             result.extend(reqResolved.resolved) | ||||
| 
 | ||||
|         stack.pop() | ||||
|         result.insert(0, keep) | ||||
|         self._cache[keep] = Resolved(resolved=utils.uniq(result)) | ||||
|         return self._cache[keep] | ||||
| 
 | ||||
| 
 | ||||
| # --- Registry --------------------------------------------------------------- # | ||||
| 
 | ||||
| _registry: Optional["Registry"] = None | ||||
| 
 | ||||
| 
 | ||||
| @dataclass | ||||
| class Registry(DataClassJsonMixin): | ||||
|     project: Project | ||||
|     manifests: dict[str, Manifest] = field(default_factory=dict) | ||||
| 
 | ||||
|     def _append(self, m: Manifest): | ||||
|         """ | ||||
|         Append a manifest to the model | ||||
|         """ | ||||
|         if m.id in self.manifests: | ||||
|             raise RuntimeError( | ||||
|                 f"Duplicated manifest '{m.id}' at '{m.path}' already loaded from '{self.manifests[m.id].path}'" | ||||
|             ) | ||||
| 
 | ||||
|         self.manifests[m.id] = m | ||||
| 
 | ||||
|     def iter(self, type: Type[utils.T]) -> Generator[utils.T, None, None]: | ||||
|         """ | ||||
|         Iterate over all manifests of a given type | ||||
|         """ | ||||
| 
 | ||||
|         for m in self.manifests.values(): | ||||
|             if isinstance(m, type): | ||||
|                 yield m | ||||
| 
 | ||||
|     def iterEnabled(self, target: Target) -> Generator[Component, None, None]: | ||||
|         for c in self.iter(Component): | ||||
|             resolve = c.resolved[target.id] | ||||
|             if resolve.enabled: | ||||
|                 yield c | ||||
| 
 | ||||
|     def lookup(self, name: str, type: Type[utils.T]) -> Optional[utils.T]: | ||||
|         """ | ||||
|         Lookup a manifest of a given type by name | ||||
|         """ | ||||
| 
 | ||||
|         if name in self.manifests: | ||||
|             m = self.manifests[name] | ||||
|             if isinstance(m, type): | ||||
|                 return m | ||||
| 
 | ||||
|         return None | ||||
| 
 | ||||
|     def ensure(self, name: str, type: Type[utils.T]) -> utils.T: | ||||
|         """ | ||||
|         Ensure that a manifest of a given type exists | ||||
|         and return it. | ||||
|         """ | ||||
| 
 | ||||
|         m = self.lookup(name, type) | ||||
|         if not m: | ||||
|             raise RuntimeError(f"Could not find {type.__name__} '{name}'") | ||||
|         return m | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def use(args: cli.Args) -> "Registry": | ||||
|         global _registry | ||||
| 
 | ||||
|         if _registry is not None: | ||||
|             return _registry | ||||
| 
 | ||||
|         project = Project.use(args) | ||||
|         mixins = str(args.consumeOpt("mixins", "")).split(",") | ||||
|         if mixins == [""]: | ||||
|             mixins = [] | ||||
|         props = cast(dict[str, str], args.consumePrefix("prop:")) | ||||
| 
 | ||||
|         _registry = Registry.load(project, mixins, props) | ||||
|         return _registry | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def load(project: Project, mixins: list[str], props: Props) -> "Registry": | ||||
|         registry = Registry(project) | ||||
|         registry._append(project) | ||||
| 
 | ||||
|         # Lookup and load all extern projects | ||||
|         for externDir in project.externDirs: | ||||
|             projectPath = os.path.join(externDir, "project.json") | ||||
|             manifestPath = os.path.join(externDir, "manifest.json") | ||||
| 
 | ||||
|             if os.path.exists(projectPath): | ||||
|                 registry._append(Manifest.load(Path(projectPath)).ensureType(Project)) | ||||
|             elif os.path.exists(manifestPath): | ||||
|                 # For simple library allow to have a manifest.json instead of a project.json | ||||
|                 registry._append( | ||||
|                     Manifest.load(Path(manifestPath)).ensureType(Component) | ||||
|                 ) | ||||
|             else: | ||||
|                 _logger.warn( | ||||
|                     "Extern project does not have a project.json or manifest.json" | ||||
|                 ) | ||||
| 
 | ||||
|         # Load all manifests from projects | ||||
|         for project in list(registry.iter(Project)): | ||||
|             targetDir = os.path.join(project.dirname(), const.TARGETS_DIR) | ||||
|             targetFiles = shell.find(targetDir, ["*.json"]) | ||||
| 
 | ||||
|             for targetFile in targetFiles: | ||||
|                 registry._append(Manifest.load(Path(targetFile)).ensureType(Target)) | ||||
| 
 | ||||
|             componentDir = os.path.join(project.dirname(), const.SRC_DIR) | ||||
|             rootComponent = os.path.join(project.dirname(), "manifest.json") | ||||
|             componentFiles = shell.find(componentDir, ["manifest.json"]) | ||||
| 
 | ||||
|             if os.path.exists(rootComponent): | ||||
|                 componentFiles += [rootComponent] | ||||
| 
 | ||||
|             for componentFile in componentFiles: | ||||
|                 registry._append( | ||||
|                     Manifest.load(Path(componentFile)).ensureType(Component) | ||||
|                 ) | ||||
| 
 | ||||
|         # Resolve all dependencies for all targets | ||||
|         for target in registry.iter(Target): | ||||
|             target.props |= props | ||||
|             resolver = Resolver(registry, target) | ||||
| 
 | ||||
|             # Apply injects | ||||
|             for c in registry.iter(Component): | ||||
|                 if c.isEnabled(target)[0]: | ||||
|                     for inject in c.injects: | ||||
|                         victim = registry.lookup(inject, Component) | ||||
|                         if not victim: | ||||
|                             raise RuntimeError(f"Cannot find component '{inject}'") | ||||
|                         victim.requires += [c.id] | ||||
| 
 | ||||
|             # Resolve all components | ||||
|             for c in registry.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 | ||||
|             tools: Tools = target.tools | ||||
|             from . import mixins as mxs | ||||
| 
 | ||||
|             for mix in mixins: | ||||
|                 mixin = mxs.byId(mix) | ||||
|                 tools = mixin(target, tools) | ||||
| 
 | ||||
|             # Apply tooling from components | ||||
|             for c in registry.iter(Component): | ||||
|                 if c.resolved[target.id].enabled: | ||||
|                     for k, v in c.tools.items(): | ||||
|                         tools[k].args += v.args | ||||
| 
 | ||||
|         return registry | ||||
| 
 | ||||
| 
 | ||||
| @cli.command("l", "list", "List all components and targets") | ||||
| def listCmd(args: cli.Args): | ||||
|     registry = Registry.use(args) | ||||
| 
 | ||||
|     components = list(registry.iter(Component)) | ||||
|     targets = list(registry.iter(Target)) | ||||
| 
 | ||||
|     vt100.title("Components") | ||||
|     if len(components) == 0: | ||||
|         print(vt100.p("(No components available)")) | ||||
|     else: | ||||
|         print(vt100.p(", ".join(map(lambda m: m.id, components)))) | ||||
|     print() | ||||
| 
 | ||||
|     vt100.title("Targets") | ||||
| 
 | ||||
|     if len(targets) == 0: | ||||
|         print(vt100.p("(No targets available)")) | ||||
|     else: | ||||
|         print(vt100.p(", ".join(map(lambda m: m.id, targets)))) | ||||
|     print() | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import os | ||||
| import logging | ||||
| 
 | ||||
| from . import shell, model, const, context | ||||
| from . import shell, model, const | ||||
| 
 | ||||
| import importlib.util as importlib | ||||
| 
 | ||||
|  | @ -23,19 +23,19 @@ def load(path: str): | |||
| def loadAll(): | ||||
|     _logger.info("Loading plugins...") | ||||
| 
 | ||||
|     projectRoot = model.Project.root() | ||||
|     root = model.Project.root() | ||||
| 
 | ||||
|     if projectRoot is None: | ||||
|     if root is None: | ||||
|         _logger.info("Not in project, skipping plugin loading") | ||||
|         return | ||||
| 
 | ||||
|     pj = context.loadProject(projectRoot) | ||||
|     paths = list(map(lambda e: os.path.join(const.EXTERN_DIR, e), pj.extern.keys())) + [ | ||||
|         "." | ||||
|     ] | ||||
|     project = model.Project.at(root) | ||||
|     paths = list( | ||||
|         map(lambda e: os.path.join(const.EXTERN_DIR, e), project.extern.keys()) | ||||
|     ) + ["."] | ||||
| 
 | ||||
|     for dirname in paths: | ||||
|         pluginDir = os.path.join(projectRoot, dirname, const.META_DIR, "plugins") | ||||
|         pluginDir = os.path.join(root, dirname, const.META_DIR, "plugins") | ||||
| 
 | ||||
|         for files in shell.readdir(pluginDir): | ||||
|             if files.endswith(".py"): | ||||
|  |  | |||
|  | @ -9,7 +9,15 @@ class Rule: | |||
|     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): | ||||
|     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 | ||||
|  | @ -19,16 +27,22 @@ class Rule: | |||
| 
 | ||||
| 
 | ||||
| rules: dict[str, Rule] = { | ||||
|     "cc": Rule("cc", ["*.c"], ["*.o"], "-c -o $out $in -MD -MF $out.d $flags $cincs $cdefs", ["-std=gnu2x", | ||||
|                                                                                               "-Wall", | ||||
|                                                                                               "-Wextra", | ||||
|                                                                                               "-Werror"], "$out.d"), | ||||
|     "cxx": Rule("cxx", ["*.cpp", "*.cc", "*.cxx"], ["*.o"], "-c -o $out $in -MD -MF $out.d $flags $cincs $cdefs", ["-std=gnu++2b", | ||||
|                                                                                                                    "-Wall", | ||||
|                                                                                                                    "-Wextra", | ||||
|                                                                                                                    "-Werror", | ||||
|                                                                                                                    "-fno-exceptions", | ||||
|                                                                                                                    "-fno-rtti"], "$out.d"), | ||||
|     "cc": Rule( | ||||
|         "cc", | ||||
|         ["*.c"], | ||||
|         ["*.o"], | ||||
|         "-c -o $out $in -MD -MF $out.d $flags $cincs $cdefs", | ||||
|         ["-std=gnu2x", "-Wall", "-Wextra", "-Werror"], | ||||
|         "$out.d", | ||||
|     ), | ||||
|     "cxx": Rule( | ||||
|         "cxx", | ||||
|         ["*.cpp", "*.cc", "*.cxx"], | ||||
|         ["*.o"], | ||||
|         "-c -o $out $in -MD -MF $out.d $flags $cincs $cdefs", | ||||
|         ["-std=gnu++2b", "-Wall", "-Wextra", "-Werror", "-fno-exceptions", "-fno-rtti"], | ||||
|         "$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"), | ||||
|  |  | |||
|  | @ -270,7 +270,7 @@ def latest(cmd: str) -> str: | |||
|     return chosen | ||||
| 
 | ||||
| 
 | ||||
| def which(cmd: str) -> str | None: | ||||
| def which(cmd: str) -> Optional[str]: | ||||
|     """ | ||||
|     Find the path of a command | ||||
|     """ | ||||
|  |  | |||
|  | @ -28,10 +28,6 @@ CROSSED = "\033[9m" | |||
| RESET = "\033[0m" | ||||
| 
 | ||||
| 
 | ||||
| def title(text: str): | ||||
|     print(f"{BOLD}{text}{RESET}:") | ||||
| 
 | ||||
| 
 | ||||
| def wordwrap(text: str, width: int = 60, newline: str = "\n") -> str: | ||||
|     result = "" | ||||
|     curr = 0 | ||||
|  | @ -49,3 +45,11 @@ def wordwrap(text: str, width: int = 60, newline: str = "\n") -> str: | |||
| 
 | ||||
| def indent(text: str, indent: int = 4) -> str: | ||||
|     return " " * indent + text.replace("\n", "\n" + " " * indent) | ||||
| 
 | ||||
| 
 | ||||
| def title(text: str): | ||||
|     print(f"{BOLD}{text}{RESET}:") | ||||
| 
 | ||||
| 
 | ||||
| def p(text: str): | ||||
|     return indent(wordwrap(text)) | ||||
|  |  | |||
							
								
								
									
										90
									
								
								tests/test_resolver.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								tests/test_resolver.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,90 @@ | |||
| from cutekit import model | ||||
| 
 | ||||
| 
 | ||||
| def test_direct_deps(): | ||||
|     r = model.Registry("") | ||||
|     r._append(model.Component("myapp", requires=["mylib"])) | ||||
|     r._append(model.Component("mylib")) | ||||
|     t = model.Target("host") | ||||
|     res = model.Resolver(r, t) | ||||
| 
 | ||||
|     resolved = res.resolve("myapp") | ||||
|     assert resolved.reason is None | ||||
|     assert resolved.resolved == ["myapp", "mylib"] | ||||
| 
 | ||||
| 
 | ||||
| def test_indirect_deps(): | ||||
|     r = model.Registry("") | ||||
|     r._append(model.Component("myapp", requires=["mylib"])) | ||||
|     r._append(model.Component("mylib", requires=["myembed"])) | ||||
|     r._append(model.Component("myimpl", provides=["myembed"])) | ||||
|     t = model.Target("host") | ||||
|     res = model.Resolver(r, t) | ||||
|     assert res.resolve("myapp").resolved == ["myapp", "mylib", "myimpl"] | ||||
| 
 | ||||
| 
 | ||||
| def test_deps_routing(): | ||||
|     r = model.Registry("") | ||||
|     r._append(model.Component("myapp", requires=["mylib"])) | ||||
|     r._append(model.Component("mylib", requires=["myembed"])) | ||||
|     r._append(model.Component("myimplA", provides=["myembed"])) | ||||
|     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"] | ||||
| 
 | ||||
|     t = model.Target("host", routing={"myembed": "myimplA"}) | ||||
|     res = model.Resolver(r, t) | ||||
|     assert res.resolve("myapp").resolved == ["myapp", "mylib", "myimplA"] | ||||
| 
 | ||||
|     t = model.Target("host", routing={"myembed": "myimplC"}) | ||||
|     res = model.Resolver(r, t) | ||||
|     assert res.resolve("myapp").reason == "No provider for 'myembed'" | ||||
| 
 | ||||
| 
 | ||||
| def test_deps_routing_with_props(): | ||||
|     r = model.Registry("") | ||||
|     r._append(model.Component("myapp", requires=["mylib"])) | ||||
|     r._append(model.Component("mylib", requires=["myembed"])) | ||||
|     r._append( | ||||
|         model.Component("myimplA", provides=["myembed"], enableIf={"myprop": ["a"]}) | ||||
|     ) | ||||
|     r._append( | ||||
|         model.Component("myimplB", provides=["myembed"], enableIf={"myprop": ["b"]}) | ||||
|     ) | ||||
|     t = model.Target("host", routing={"myembed": "myimplB"}, props={"myprop": "b"}) | ||||
|     res = model.Resolver(r, t) | ||||
|     assert res.resolve("myapp").resolved == ["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"] | ||||
| 
 | ||||
|     t = model.Target("host", routing={"myembed": "myimplC"}, props={"myprop": "c"}) | ||||
|     res = model.Resolver(r, t) | ||||
| 
 | ||||
|     resolved = res.resolve("myapp") | ||||
|     assert resolved.reason == "No provider for 'myembed'" | ||||
| 
 | ||||
| 
 | ||||
| def test_deps_routing_with_props_and_requires(): | ||||
|     r = model.Registry("") | ||||
|     r._append(model.Component("myapp", requires=["mylib"])) | ||||
|     r._append(model.Component("mylib", requires=["myembed"])) | ||||
|     r._append( | ||||
|         model.Component("myimplA", provides=["myembed"], enableIf={"myprop": ["a"]}) | ||||
|     ) | ||||
|     r._append( | ||||
|         model.Component("myimplB", provides=["myembed"], enableIf={"myprop": ["b"]}) | ||||
|     ) | ||||
|     t = model.Target("host", routing={"myembed": "myimplB"}, props={"myprop": "b"}) | ||||
|     res = model.Resolver(r, t) | ||||
|     assert res.resolve("myapp").resolved == ["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"] | ||||
| 
 | ||||
|     t = model.Target("host", routing={"myembed": "myimplC"}, props={"myprop": "c"}) | ||||
|     res = model.Resolver(r, t) | ||||
|     assert res.resolve("myapp").reason == "No provider for 'myembed'" | ||||
		Loading…
	
	Add table
		
		Reference in a new issue