Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 49 additions & 10 deletions cutekit/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand All @@ -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)))

Expand All @@ -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))
Expand Down Expand Up @@ -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(
Expand All @@ -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


Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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",
Expand Down
192 changes: 164 additions & 28 deletions cutekit/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class Kind(StrEnum):
TARGET = "target"
LIB = "lib"
EXE = "exe"
PORT = "port"


# MARK: Manifest ---------------------------------------------------------------
Expand Down Expand Up @@ -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 -----------------------------------------------------------------

Expand Down Expand Up @@ -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."""

Expand Down Expand Up @@ -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"<PORT {self.id}"), "exec")

exec(code, globals)
del globals['__builtins__']

self.ctx = globals

assert 'kind' in self.ctx, "Port context must have a 'kind' field"
self._subtype = Kind(self.ctx['kind'])

def fetch(self):
if self.ctx is None:
self.parseBuild()

assert self.ctx is not None

srcDir = Path(Project.use().dirname()) / const.EXTERN_DIR / self.id
if srcDir.exists():
_logger.debug(f"Port {self.id} already fetched at {srcDir}")
return

if self.ctx.get('git_url') is not None:
print(f"Installing {self.id} from {self.ctx['git_url']}...")
_logger.info(f"Cloning {self.ctx['git_url']}...")
cmd = [
"git",
"clone",
"--quiet",
"--depth=1",
self.ctx['git_url'],
str(srcDir),
]

if self.ctx.get('commit') is not None:
cmd.append(f"--revision={self.ctx.get('commit')}")

if self.ctx.get('branch') is not None:
cmd.append(f"--branch={self.ctx.get('branch')}")

shell.exec(*cmd, quiet=True)

def prepare(self, port: "PortScope"):
if self.ctx is None:
self.parseBuild()

assert self.ctx is not None

if 'patches' in self.ctx:
if not (port.srcDir / ".git").exists():
raise RuntimeError(f"Port {self.ctx['name']} is not a git repository, cannot apply patches.")

for patch in self.ctx['patches']:
try:
shell.exec(
*["git", "-C", str(port.srcDir), "apply", "--check", str(port.cwd / patch)],
quiet=True
)
shell.exec(*["git", "-C", str(port.srcDir), "apply", str(port.cwd / patch)])
except Exception as e:
_logger.warning(f"Could not apply patch {patch} to port {self.ctx['name']}: {e}")

if 'prepare' in self.ctx:
self.ctx['prepare'](port)

def build(self, port: "PortScope"):
assert self.ctx is not None

if 'build' in self.ctx:
self.ctx['build'](port)

def package(self, port: "PortScope"):
assert self.ctx is not None

if 'package' in self.ctx:
self.ctx['package'](port)

if not port.destFile.exists():
with open(port.destFile, "w") as f:
f.write("")


@dt.dataclass
class PortScope:
srcDir: Path
destDir: Path
destFile: Path
cwd: Path
target: Target
component: Port
includeDir: Path

@staticmethod
def use(args: PortArgs) -> "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."""

Expand Down Expand Up @@ -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):
"""
Expand Down
Loading