diff --git a/cutekit/builder.py b/cutekit/builder.py index e5516f0..a4b357e 100644 --- a/cutekit/builder.py +++ b/cutekit/builder.py @@ -5,7 +5,7 @@ from pathlib import Path import platform -from typing import Callable, Literal, TextIO, Union +from typing import Callable, cast, Literal, TextIO, Union from . import cli, shell, rules, model, ninja, const @@ -323,6 +323,22 @@ def _(args: CxxDyndepArgs): print() +# MARK: Port ------------------------------------------------------------------ + +@cli.command("tools/port", "Execute and build port") +def _(args: model.PortArgs): + registry = model.Registry.use(args) + scope = model.PortScope.use(args) + + target = registry.lookup(args.target, model.Target) + assert target is not None, f"Target {args.target} not found." + + scope.component.fetch() + scope.component.prepare(scope) + scope.component.build(scope) + scope.component.package(scope) + + def compileSrcs( w: ninja.Writer | None, scope: ComponentScope, rule: rules.Rule, srcs: list[str] ) -> list[str]: @@ -384,12 +400,12 @@ def compileSrcs( def compileObjs( w: ninja.Writer | None, scope: ComponentScope ) -> tuple[list[str], list[str]]: - objs = [] - ddi = [] + objs: list[str] = [] + ddi: list[str] = [] for rule in rules.rules.values(): if rule.id == "cxx-scan": ddi += compileSrcs(w, scope, rule, srcs=scope.wilcard(rule.fileIn)) - elif rule.id not in ["cp", "ld", "ar", "cxx-collect", "cxx-modmap"]: + elif rule.id not in ["cp", "ld", "ar", "cxx-collect", "cxx-modmap", "ck-port"]: objs += compileSrcs(w, scope, rule, srcs=scope.wilcard(rule.fileIn)) return objs, ddi @@ -440,7 +456,9 @@ def outfile(scope: ComponentScope) -> str: staticExt = "a" exeExt = "out" - if scope.component.type == model.Kind.LIB: + if scope.component.type == model.Kind.LIB or \ + (scope.component.type == model.Kind.PORT and \ + cast(model.Port, scope.component).subtype == model.Kind.LIB): if scope.component.props.get("shared", False): return str(scope.buildpath(f"__lib__/{scope.component.id}.{sharedExt}")) else: @@ -459,7 +477,8 @@ def collectLibs( if r == scope.component.id: continue - if not req.type == model.Kind.LIB: + if req.type != model.Kind.LIB and \ + (not (isinstance(req, model.Port) and req.subtype == model.Kind.LIB)): raise RuntimeError(f"Component {r} is not a library") res.append(outfile(scope.openComponentScope(req))) @@ -474,7 +493,8 @@ def collectInjectedObjs(scope: ComponentScope) -> list[str]: if r == scope.component.id: continue - if not req.type == model.Kind.LIB: + if req.type != model.Kind.LIB and \ + (not (isinstance(req, model.Port) and req.subtype == model.Kind.LIB)): raise RuntimeError(f"Component {r} is not a library") objs, _ = compileObjs(None, scope.openComponentScope(req)) @@ -521,7 +541,7 @@ def link( "ck_component": scope.component.id, }, ) - else: + elif scope.component.type == model.Kind.EXE: injectedObjs = collectInjectedObjs(scope) libs = collectLibs(scope) w.build( @@ -536,6 +556,16 @@ def link( }, implicit=res, ) + else: + w.build( + out, + "ck-port", + [], + variables={ + "ck_target": scope.target.id, + "ck_component": scope.component.id, + } + ) return out, ddi @@ -545,10 +575,14 @@ def link( def all(w: ninja.Writer, scope: TargetScope) -> list[str]: all: list[str] = [] ddis: list[str] = [] + ports: list[str] = [] for c in scope.registry.iterEnabled(scope.target): out, ddi = link(w, scope.openComponentScope(c)) ddis.extend(ddi) - all.append(out) + if c.type == model.Kind.PORT: + ports.append(out) + else: + all.append(out) modulesDdi = str(scope.buildpath("modules.ddi")) w.build(modulesDdi, "cxx-collect", ddis) @@ -565,6 +599,7 @@ def all(w: ninja.Writer, scope: TargetScope) -> list[str]: all = [modulesDd] + all + w.build("ports", "phony", ports) w.build("all", "phony", all) w.default("all") return all @@ -647,10 +682,14 @@ def build( if not r.enabled: raise RuntimeError(f"Component {c.id} is disabled: {r.reason}") - products.append(s.openProductScope(Path(outfile(scope.openComponentScope(c))))) + product = s.openProductScope(Path(outfile(scope.openComponentScope(c)))) + products.append(product) outs = list(map(lambda p: str(p.path), products)) + # Build ports first + shell.popen("ninja", "-f", ninjaPath, "ports") + ninjaCmd = [ "ninja", "-f", diff --git a/cutekit/model.py b/cutekit/model.py index e39e21e..17f5afa 100644 --- a/cutekit/model.py +++ b/cutekit/model.py @@ -28,6 +28,7 @@ class Kind(StrEnum): TARGET = "target" LIB = "lib" EXE = "exe" + PORT = "port" # MARK: Manifest --------------------------------------------------------------- @@ -586,34 +587,6 @@ def use() -> "Project": return _project -@cli.command("model", "Manage the model") -def _(): - """ - Manage the CuteKit model. - """ - pass - - -class InstallArgs: - """ - Arguments for the install command. - """ - - update: bool = cli.arg( - None, "update", "Pull latest versions of externs and refresh the lockfile" - ) - - -@cli.command("install", "Install required external packages") -def _(args: InstallArgs): - """ - Install required external packages for the project. - """ - project = Project.use() - assert project.lockfile is not None - project.fetchExterns(project.lockfile, args.update) - project.lockfile.save() - # MARK: Target ----------------------------------------------------------------- @@ -644,6 +617,7 @@ class Tool(DataClassJsonMixin): "cxx-collect": Tool("jq"), "cxx-modmap": Tool("ck --safemode tools cxx-modmap"), "cxx-dyndep": Tool("ck --safemode tools cxx-dyndep"), + "ck-port": Tool("ck --safemode tools port"), } """Default tools available in all projects.""" @@ -835,11 +809,144 @@ def isEnabled(self, target: Target) -> tuple[bool, str]: return True, "" +# MARK: Port ------------------------------------------------------------------ + +class PortArgs(TargetArgs): + component: str = cli.arg(None, "component", "Name of the component to port") + out: str = cli.arg(None, "out", "Output path") + + +@dt.dataclass +class Port(Component): + _subtype: Kind = dt.field(default=Kind.UNKNOWN) + ctx: Optional[dict[str, Any]] = dt.field(default=None) + + @property + def subtype(self) -> Kind: + if self._subtype == Kind.UNKNOWN: + self.parseBuild() + return self._subtype + + def parseBuild(self): + with self.subpath("build.py").open("r") as f: + globals: dict[str, Any] = {} + code = compile(f.read(), str(f" "PortScope": + registry = Registry.use(args) + component = registry.ensure(args.component, Port) + + assert isinstance(component, Port), "Component is not a Port" + + return PortScope( + srcDir=Path(const.EXTERN_DIR) / args.component, + destDir=Path(args.out).parent, + destFile=Path(args.out), + cwd=Path(component.dirname()), + target=Target.use(args), + component=component, + + #FIXME: this is a temporary hack for includes + includeDir=Path(const.GENERATED_DIR) / args.component + ) + + KINDS: dict[Kind, Type[Manifest]] = { Kind.PROJECT: Project, Kind.TARGET: Target, Kind.LIB: Component, Kind.EXE: Component, + Kind.PORT: Port, } """Mapping of manifest kinds to their corresponding classes.""" @@ -1298,6 +1405,35 @@ def load(project: Project, mixins: list[str], props: Props) -> "Registry": return r +@cli.command("model", "Manage the model") +def _(): + """ + Manage the CuteKit model. + """ + pass + + +class InstallArgs(RegistryArgs): + """ + Arguments for the install command. + """ + + update: bool = cli.arg( + None, "update", "Pull latest versions of externs and refresh the lockfile" + ) + + +@cli.command("install", "Install required external packages") +def _(args: InstallArgs): + """ + Install required external packages for the project. + """ + project = Project.use() + assert project.lockfile is not None + project.fetchExterns(project.lockfile, args.update) + project.lockfile.save() + + @cli.command("model/list", "List all components and targets") def _(args: TargetArgs): """ diff --git a/cutekit/project.py b/cutekit/project.py index 39bbc3c..e8ae559 100644 --- a/cutekit/project.py +++ b/cutekit/project.py @@ -35,20 +35,20 @@ def init_manifest(args: InitArgs): """ Each type of kind """ match model.KINDS[args.kind]: # Init a Component or a Target - case model.Component | model.Target: + case model.Component | model.Target | model.Port: if project is None: raise RuntimeError( f"can't create '{args.kind}' without a project") filename = "manifest" - if model.KINDS[args.kind] == model.Component: + if model.KINDS[args.kind] in [model.Component, model.Port]: schema = model.COMPONENT_SCHEMA manifest = model.Component( **manifest.__dict__ ) if args.description: manifest.description = args.description - else: + elif model.KINDS[args.kind] == model.Target: schema = model.TARGET_SCHEMA manifest = model.Target( **manifest.__dict__ @@ -89,11 +89,23 @@ def init_manifest(args: InitArgs): if args.description: manifest.description = args.description + case _: + raise RuntimeError(f"Unknown kind: {args.kind}") # Avoid having manifests in different format if model.Manifest.tryLoad(Path.cwd() / filename): raise RuntimeError("Your Manifest already exist.") + if model.KINDS[args.kind] == model.Port: + with open("build.py", "w", encoding="utf-8") as f: + f.writelines([ + "import cutekit as ck\n\n", + "kind = \"lib\"", + "def prepare(scope: ck.model.PortScope):\n ...\n\n" + "def build(scope: ck.model.PortScope):\n ...\n\n", + "def package(scope: ck.model.PortScope):\n ...\n" + ]) + filename += f".{args.format}" try: # Filter empty variable, and protected variables from Manifest. diff --git a/cutekit/rules.py b/cutekit/rules.py index 1b6c785..7cb38c2 100644 --- a/cutekit/rules.py +++ b/cutekit/rules.py @@ -15,6 +15,7 @@ class Rule: rules: dict[str, Rule] = { "cp": Rule("cp", ["*"], "*", "$in $out"), + "ck-port": Rule("ck-port", ["*"], "*", "--component=$ck_component --out=$out --target=$ck_target"), "cc": Rule( "cc", ["*.c"],