Skip to content

Commit cba95dc

Browse files
keyboard-slayerruxap0
authored andcommitted
feat: port system
1 parent 9c838f4 commit cba95dc

4 files changed

Lines changed: 229 additions & 41 deletions

File tree

cutekit/builder.py

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from pathlib import Path
77
import platform
8-
from typing import Callable, Literal, TextIO, Union
8+
from typing import Callable, cast, Literal, TextIO, Union
99

1010
from . import cli, shell, rules, model, ninja, const
1111

@@ -323,6 +323,22 @@ def _(args: CxxDyndepArgs):
323323
print()
324324

325325

326+
# MARK: Port ------------------------------------------------------------------
327+
328+
@cli.command("tools/port", "Execute and build port")
329+
def _(args: model.PortArgs):
330+
registry = model.Registry.use(args)
331+
scope = model.PortScope.use(args)
332+
333+
target = registry.lookup(args.target, model.Target)
334+
assert target is not None, f"Target {args.target} not found."
335+
336+
scope.component.fetch()
337+
scope.component.prepare(scope)
338+
scope.component.build(scope)
339+
scope.component.package(scope)
340+
341+
326342
def compileSrcs(
327343
w: ninja.Writer | None, scope: ComponentScope, rule: rules.Rule, srcs: list[str]
328344
) -> list[str]:
@@ -384,12 +400,12 @@ def compileSrcs(
384400
def compileObjs(
385401
w: ninja.Writer | None, scope: ComponentScope
386402
) -> tuple[list[str], list[str]]:
387-
objs = []
388-
ddi = []
403+
objs: list[str] = []
404+
ddi: list[str] = []
389405
for rule in rules.rules.values():
390406
if rule.id == "cxx-scan":
391407
ddi += compileSrcs(w, scope, rule, srcs=scope.wilcard(rule.fileIn))
392-
elif rule.id not in ["cp", "ld", "ar", "cxx-collect", "cxx-modmap"]:
408+
elif rule.id not in ["cp", "ld", "ar", "cxx-collect", "cxx-modmap", "ck-port"]:
393409
objs += compileSrcs(w, scope, rule, srcs=scope.wilcard(rule.fileIn))
394410
return objs, ddi
395411

@@ -440,7 +456,9 @@ def outfile(scope: ComponentScope) -> str:
440456
staticExt = "a"
441457
exeExt = "out"
442458

443-
if scope.component.type == model.Kind.LIB:
459+
if scope.component.type == model.Kind.LIB or \
460+
(scope.component.type == model.Kind.PORT and \
461+
cast(model.Port, scope.component).subtype == model.Kind.LIB):
444462
if scope.component.props.get("shared", False):
445463
return str(scope.buildpath(f"__lib__/{scope.component.id}.{sharedExt}"))
446464
else:
@@ -459,7 +477,8 @@ def collectLibs(
459477

460478
if r == scope.component.id:
461479
continue
462-
if not req.type == model.Kind.LIB:
480+
if req.type != model.Kind.LIB and \
481+
(not (isinstance(req, model.Port) and req.subtype == model.Kind.LIB)):
463482
raise RuntimeError(f"Component {r} is not a library")
464483
res.append(outfile(scope.openComponentScope(req)))
465484

@@ -474,7 +493,8 @@ def collectInjectedObjs(scope: ComponentScope) -> list[str]:
474493

475494
if r == scope.component.id:
476495
continue
477-
if not req.type == model.Kind.LIB:
496+
if req.type != model.Kind.LIB and \
497+
(not (isinstance(req, model.Port) and req.subtype == model.Kind.LIB)):
478498
raise RuntimeError(f"Component {r} is not a library")
479499

480500
objs, _ = compileObjs(None, scope.openComponentScope(req))
@@ -521,7 +541,7 @@ def link(
521541
"ck_component": scope.component.id,
522542
},
523543
)
524-
else:
544+
elif scope.component.type == model.Kind.EXE:
525545
injectedObjs = collectInjectedObjs(scope)
526546
libs = collectLibs(scope)
527547
w.build(
@@ -536,6 +556,16 @@ def link(
536556
},
537557
implicit=res,
538558
)
559+
else:
560+
w.build(
561+
out,
562+
"ck-port",
563+
[],
564+
variables={
565+
"ck_target": scope.target.id,
566+
"ck_component": scope.component.id,
567+
}
568+
)
539569
return out, ddi
540570

541571

@@ -545,10 +575,14 @@ def link(
545575
def all(w: ninja.Writer, scope: TargetScope) -> list[str]:
546576
all: list[str] = []
547577
ddis: list[str] = []
578+
ports: list[str] = []
548579
for c in scope.registry.iterEnabled(scope.target):
549580
out, ddi = link(w, scope.openComponentScope(c))
550581
ddis.extend(ddi)
551-
all.append(out)
582+
if c.type == model.Kind.PORT:
583+
ports.append(out)
584+
else:
585+
all.append(out)
552586

553587
modulesDdi = str(scope.buildpath("modules.ddi"))
554588
w.build(modulesDdi, "cxx-collect", ddis)
@@ -565,6 +599,7 @@ def all(w: ninja.Writer, scope: TargetScope) -> list[str]:
565599

566600
all = [modulesDd] + all
567601

602+
w.build("ports", "phony", ports)
568603
w.build("all", "phony", all)
569604
w.default("all")
570605
return all
@@ -647,10 +682,14 @@ def build(
647682
if not r.enabled:
648683
raise RuntimeError(f"Component {c.id} is disabled: {r.reason}")
649684

650-
products.append(s.openProductScope(Path(outfile(scope.openComponentScope(c)))))
685+
product = s.openProductScope(Path(outfile(scope.openComponentScope(c))))
686+
products.append(product)
651687

652688
outs = list(map(lambda p: str(p.path), products))
653689

690+
# Build ports first
691+
shell.popen("ninja", "-f", ninjaPath, "ports")
692+
654693
ninjaCmd = [
655694
"ninja",
656695
"-f",

cutekit/model.py

Lines changed: 164 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class Kind(StrEnum):
2828
TARGET = "target"
2929
LIB = "lib"
3030
EXE = "exe"
31+
PORT = "port"
3132

3233

3334
# MARK: Manifest ---------------------------------------------------------------
@@ -586,34 +587,6 @@ def use() -> "Project":
586587
return _project
587588

588589

589-
@cli.command("model", "Manage the model")
590-
def _():
591-
"""
592-
Manage the CuteKit model.
593-
"""
594-
pass
595-
596-
597-
class InstallArgs:
598-
"""
599-
Arguments for the install command.
600-
"""
601-
602-
update: bool = cli.arg(
603-
None, "update", "Pull latest versions of externs and refresh the lockfile"
604-
)
605-
606-
607-
@cli.command("install", "Install required external packages")
608-
def _(args: InstallArgs):
609-
"""
610-
Install required external packages for the project.
611-
"""
612-
project = Project.use()
613-
assert project.lockfile is not None
614-
project.fetchExterns(project.lockfile, args.update)
615-
project.lockfile.save()
616-
617590

618591
# MARK: Target -----------------------------------------------------------------
619592

@@ -644,6 +617,7 @@ class Tool(DataClassJsonMixin):
644617
"cxx-collect": Tool("jq"),
645618
"cxx-modmap": Tool("ck --safemode tools cxx-modmap"),
646619
"cxx-dyndep": Tool("ck --safemode tools cxx-dyndep"),
620+
"ck-port": Tool("ck --safemode tools port"),
647621
}
648622
"""Default tools available in all projects."""
649623

@@ -835,11 +809,144 @@ def isEnabled(self, target: Target) -> tuple[bool, str]:
835809
return True, ""
836810

837811

812+
# MARK: Port ------------------------------------------------------------------
813+
814+
class PortArgs(TargetArgs):
815+
component: str = cli.arg(None, "component", "Name of the component to port")
816+
out: str = cli.arg(None, "out", "Output path")
817+
818+
819+
@dt.dataclass
820+
class Port(Component):
821+
_subtype: Kind = dt.field(default=Kind.UNKNOWN)
822+
ctx: Optional[dict[str, Any]] = dt.field(default=None)
823+
824+
@property
825+
def subtype(self) -> Kind:
826+
if self._subtype == Kind.UNKNOWN:
827+
self.parseBuild()
828+
return self._subtype
829+
830+
def parseBuild(self):
831+
with self.subpath("build.py").open("r") as f:
832+
globals: dict[str, Any] = {}
833+
code = compile(f.read(), str(f"<PORT {self.id}"), "exec")
834+
835+
exec(code, globals)
836+
del globals['__builtins__']
837+
838+
self.ctx = globals
839+
840+
assert 'kind' in self.ctx, "Port context must have a 'kind' field"
841+
self._subtype = Kind(self.ctx['kind'])
842+
843+
def fetch(self):
844+
if self.ctx is None:
845+
self.parseBuild()
846+
847+
assert self.ctx is not None
848+
849+
srcDir = Path(Project.use().dirname()) / const.EXTERN_DIR / self.id
850+
if srcDir.exists():
851+
_logger.debug(f"Port {self.id} already fetched at {srcDir}")
852+
return
853+
854+
if self.ctx.get('git_url') is not None:
855+
print(f"Installing {self.id} from {self.ctx['git_url']}...")
856+
_logger.info(f"Cloning {self.ctx['git_url']}...")
857+
cmd = [
858+
"git",
859+
"clone",
860+
"--quiet",
861+
"--depth=1",
862+
self.ctx['git_url'],
863+
str(srcDir),
864+
]
865+
866+
if self.ctx.get('commit') is not None:
867+
cmd.append(f"--revision={self.ctx.get('commit')}")
868+
869+
if self.ctx.get('branch') is not None:
870+
cmd.append(f"--branch={self.ctx.get('branch')}")
871+
872+
shell.exec(*cmd, quiet=True)
873+
874+
def prepare(self, port: "PortScope"):
875+
if self.ctx is None:
876+
self.parseBuild()
877+
878+
assert self.ctx is not None
879+
880+
if 'patches' in self.ctx:
881+
if not (port.srcDir / ".git").exists():
882+
raise RuntimeError(f"Port {self.ctx['name']} is not a git repository, cannot apply patches.")
883+
884+
for patch in self.ctx['patches']:
885+
try:
886+
shell.exec(
887+
*["git", "-C", str(port.srcDir), "apply", "--check", str(port.cwd / patch)],
888+
quiet=True
889+
)
890+
shell.exec(*["git", "-C", str(port.srcDir), "apply", str(port.cwd / patch)])
891+
except Exception as e:
892+
_logger.warning(f"Could not apply patch {patch} to port {self.ctx['name']}: {e}")
893+
894+
if 'prepare' in self.ctx:
895+
self.ctx['prepare'](port)
896+
897+
def build(self, port: "PortScope"):
898+
assert self.ctx is not None
899+
900+
if 'build' in self.ctx:
901+
self.ctx['build'](port)
902+
903+
def package(self, port: "PortScope"):
904+
assert self.ctx is not None
905+
906+
if 'package' in self.ctx:
907+
self.ctx['package'](port)
908+
909+
if not port.destFile.exists():
910+
with open(port.destFile, "w") as f:
911+
f.write("")
912+
913+
914+
@dt.dataclass
915+
class PortScope:
916+
srcDir: Path
917+
destDir: Path
918+
destFile: Path
919+
cwd: Path
920+
target: Target
921+
component: Port
922+
includeDir: Path
923+
924+
@staticmethod
925+
def use(args: PortArgs) -> "PortScope":
926+
registry = Registry.use(args)
927+
component = registry.ensure(args.component, Port)
928+
929+
assert isinstance(component, Port), "Component is not a Port"
930+
931+
return PortScope(
932+
srcDir=Path(const.EXTERN_DIR) / args.component,
933+
destDir=Path(args.out).parent,
934+
destFile=Path(args.out),
935+
cwd=Path(component.dirname()),
936+
target=Target.use(args),
937+
component=component,
938+
939+
#FIXME: this is a temporary hack for includes
940+
includeDir=Path(const.GENERATED_DIR) / args.component
941+
)
942+
943+
838944
KINDS: dict[Kind, Type[Manifest]] = {
839945
Kind.PROJECT: Project,
840946
Kind.TARGET: Target,
841947
Kind.LIB: Component,
842948
Kind.EXE: Component,
949+
Kind.PORT: Port,
843950
}
844951
"""Mapping of manifest kinds to their corresponding classes."""
845952

@@ -1298,6 +1405,35 @@ def load(project: Project, mixins: list[str], props: Props) -> "Registry":
12981405
return r
12991406

13001407

1408+
@cli.command("model", "Manage the model")
1409+
def _():
1410+
"""
1411+
Manage the CuteKit model.
1412+
"""
1413+
pass
1414+
1415+
1416+
class InstallArgs(RegistryArgs):
1417+
"""
1418+
Arguments for the install command.
1419+
"""
1420+
1421+
update: bool = cli.arg(
1422+
None, "update", "Pull latest versions of externs and refresh the lockfile"
1423+
)
1424+
1425+
1426+
@cli.command("install", "Install required external packages")
1427+
def _(args: InstallArgs):
1428+
"""
1429+
Install required external packages for the project.
1430+
"""
1431+
project = Project.use()
1432+
assert project.lockfile is not None
1433+
project.fetchExterns(project.lockfile, args.update)
1434+
project.lockfile.save()
1435+
1436+
13011437
@cli.command("model/list", "List all components and targets")
13021438
def _(args: TargetArgs):
13031439
"""

0 commit comments

Comments
 (0)