Cleanups imports and got ride of cutekit.project

This commit is contained in:
Sleepy Monax 2023-11-11 16:44:40 +01:00
parent 3a78537dff
commit 8d1ca3095a
13 changed files with 224 additions and 222 deletions

View file

@ -2,7 +2,7 @@ import sys
import os import os
import logging import logging
from cutekit import const, project, vt100, plugins, cmds, cli from . import const, model, vt100, plugins, cmds, cli
def setupLogger(verbose: bool): def setupLogger(verbose: bool):
@ -13,7 +13,7 @@ def setupLogger(verbose: bool):
datefmt="%Y-%m-%d %H:%M:%S", datefmt="%Y-%m-%d %H:%M:%S",
) )
else: else:
projectRoot = project.root() projectRoot = model.Project.root()
logFile = const.GLOBAL_LOG_FILE logFile = const.GLOBAL_LOG_FILE
if projectRoot is not None: if projectRoot is not None:
logFile = os.path.join(projectRoot, const.PROJECT_LOG_FILE) logFile = os.path.join(projectRoot, const.PROJECT_LOG_FILE)

View file

@ -2,16 +2,13 @@ import os
import logging import logging
from typing import TextIO from typing import TextIO
from cutekit.model import Props from . import shell, rules, model, ninja, context
from cutekit.ninja import Writer
from cutekit.context import ComponentInstance, Context, contextFor
from cutekit import shell, rules
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
def gen(out: TextIO, context: Context): def gen(out: TextIO, context: context.Context):
writer = Writer(out) writer = ninja.Writer(out)
target = context.target target = context.target
@ -102,16 +99,18 @@ def gen(out: TextIO, context: Context):
writer.default("all") writer.default("all")
def build(componentSpec: str, targetSpec: str, props: Props = {}) -> ComponentInstance: def build(
context = contextFor(targetSpec, props) componentSpec: str, targetSpec: str, props: model.Props = {}
) -> context.ComponentInstance:
ctx = context.contextFor(targetSpec, props)
shell.mkdir(context.builddir()) shell.mkdir(ctx.builddir())
ninjaPath = os.path.join(context.builddir(), "build.ninja") ninjaPath = os.path.join(ctx.builddir(), "build.ninja")
with open(ninjaPath, "w") as f: with open(ninjaPath, "w") as f:
gen(f, context) gen(f, ctx)
instance = context.componentByName(componentSpec) instance = ctx.componentByName(componentSpec)
if instance is None: if instance is None:
raise RuntimeError(f"Component {componentSpec} not found") raise RuntimeError(f"Component {componentSpec} not found")
@ -137,32 +136,32 @@ class Paths:
self.obj = obj self.obj = obj
def buildAll(targetSpec: str, props: Props = {}) -> Context: def buildAll(targetSpec: str, props: model.Props = {}) -> context.Context:
context = contextFor(targetSpec, props) ctx = context.contextFor(targetSpec, props)
shell.mkdir(context.builddir()) shell.mkdir(ctx.builddir())
ninjaPath = os.path.join(context.builddir(), "build.ninja") ninjaPath = os.path.join(ctx.builddir(), "build.ninja")
with open(ninjaPath, "w") as f: with open(ninjaPath, "w") as f:
gen(f, context) gen(f, ctx)
shell.exec("ninja", "-v", "-f", ninjaPath) shell.exec("ninja", "-v", "-f", ninjaPath)
return context return ctx
def testAll(targetSpec: str): def testAll(targetSpec: str):
context = contextFor(targetSpec) ctx = context.contextFor(targetSpec)
shell.mkdir(context.builddir()) shell.mkdir(ctx.builddir())
ninjaPath = os.path.join(context.builddir(), "build.ninja") ninjaPath = os.path.join(ctx.builddir(), "build.ninja")
with open(ninjaPath, "w") as f: with open(ninjaPath, "w") as f:
gen(f, context) gen(f, ctx)
shell.exec("ninja", "-v", "-f", ninjaPath, "all") shell.exec("ninja", "-v", "-f", ninjaPath, "all")
for instance in context.enabledInstances(): for instance in ctx.enabledInstances():
if instance.isLib(): if instance.isLib():
continue continue

View file

@ -3,13 +3,12 @@ import os
import sys import sys
from cutekit import ( from . import (
context, context,
shell, shell,
const, const,
vt100, vt100,
builder, builder,
project,
cli, cli,
model, model,
jexpr, jexpr,
@ -21,7 +20,7 @@ _logger = logging.getLogger(__name__)
@cli.command("p", "project", "Show project information") @cli.command("p", "project", "Show project information")
def runCmd(args: cli.Args): def runCmd(args: cli.Args):
project.chdir() model.Project.chdir()
targetSpec = str(args.consumeOpt("target", "host-" + shell.uname().machine)) targetSpec = str(args.consumeOpt("target", "host-" + shell.uname().machine))
props = args.consumePrefix("prop:") props = args.consumePrefix("prop:")
@ -42,7 +41,7 @@ def runCmd(args: cli.Args):
@cli.command("t", "test", "Run all test targets") @cli.command("t", "test", "Run all test targets")
def testCmd(args: cli.Args): def testCmd(args: cli.Args):
project.chdir() model.Project.chdir()
targetSpec = str(args.consumeOpt("target", "host-" + shell.uname().machine)) targetSpec = str(args.consumeOpt("target", "host-" + shell.uname().machine))
builder.testAll(targetSpec) builder.testAll(targetSpec)
@ -50,7 +49,7 @@ def testCmd(args: cli.Args):
@cli.command("d", "debug", "Debug a component") @cli.command("d", "debug", "Debug a component")
def debugCmd(args: cli.Args): def debugCmd(args: cli.Args):
project.chdir() model.Project.chdir()
targetSpec = str(args.consumeOpt("target", "host-" + shell.uname().machine)) targetSpec = str(args.consumeOpt("target", "host-" + shell.uname().machine))
props = args.consumePrefix("prop:") props = args.consumePrefix("prop:")
@ -71,7 +70,7 @@ def debugCmd(args: cli.Args):
@cli.command("b", "build", "Build a component or all components") @cli.command("b", "build", "Build a component or all components")
def buildCmd(args: cli.Args): def buildCmd(args: cli.Args):
project.chdir() model.Project.chdir()
targetSpec = str(args.consumeOpt("target", "host-" + shell.uname().machine)) targetSpec = str(args.consumeOpt("target", "host-" + shell.uname().machine))
props = args.consumePrefix("prop:") props = args.consumePrefix("prop:")
@ -85,7 +84,7 @@ def buildCmd(args: cli.Args):
@cli.command("l", "list", "List all components and targets") @cli.command("l", "list", "List all components and targets")
def listCmd(args: cli.Args): def listCmd(args: cli.Args):
project.chdir() model.Project.chdir()
components = context.loadAllComponents() components = context.loadAllComponents()
targets = context.loadAllTargets() targets = context.loadAllTargets()
@ -109,13 +108,13 @@ def listCmd(args: cli.Args):
@cli.command("c", "clean", "Clean build files") @cli.command("c", "clean", "Clean build files")
def cleanCmd(args: cli.Args): def cleanCmd(args: cli.Args):
project.chdir() model.Project.chdir()
shell.rmrf(const.BUILD_DIR) shell.rmrf(const.BUILD_DIR)
@cli.command("n", "nuke", "Clean all build files and caches") @cli.command("n", "nuke", "Clean all build files and caches")
def nukeCmd(args: cli.Args): def nukeCmd(args: cli.Args):
project.chdir() model.Project.chdir()
shell.rmrf(const.PROJECT_CK_DIR) shell.rmrf(const.PROJECT_CK_DIR)
@ -170,8 +169,7 @@ def grabExtern(extern: dict[str, model.Extern]):
@cli.command("i", "install", "Install required external packages") @cli.command("i", "install", "Install required external packages")
def installCmd(args: cli.Args): def installCmd(args: cli.Args):
project.chdir() model.Project.chdir()
pj = context.loadProject(".") pj = context.loadProject(".")
grabExtern(pj.extern) grabExtern(pj.extern)

View file

@ -4,22 +4,14 @@ from pathlib import Path
import os import os
import logging import logging
from cutekit.model import (
Project, from . import const, shell, jexpr, utils, rules, mixins, project, model
Target,
Component,
Props,
Type,
Tool,
Tools,
)
from cutekit import const, shell, jexpr, utils, rules, mixins, project
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
class IContext(Protocol): class IContext(Protocol):
target: Target target: model.Target
def builddir(self) -> str: def builddir(self) -> str:
... ...
@ -28,7 +20,7 @@ class IContext(Protocol):
class ComponentInstance: class ComponentInstance:
enabled: bool = True enabled: bool = True
disableReason = "" disableReason = ""
manifest: Component manifest: model.Component
sources: list[str] = [] sources: list[str] = []
res: list[str] = [] res: list[str] = []
resolved: list[str] = [] resolved: list[str] = []
@ -38,7 +30,7 @@ class ComponentInstance:
self, self,
enabled: bool, enabled: bool,
disableReason: str, disableReason: str,
manifest: Component, manifest: model.Component,
sources: list[str], sources: list[str],
res: list[str], res: list[str],
resolved: list[str], resolved: list[str],
@ -54,7 +46,7 @@ class ComponentInstance:
return self.manifest.id return self.manifest.id
def isLib(self): def isLib(self):
return self.manifest.type == Type.LIB return self.manifest.type == model.Type.LIB
def objdir(self) -> str: def objdir(self) -> str:
return os.path.join(self.context.builddir(), f"{self.manifest.id}/obj") return os.path.join(self.context.builddir(), f"{self.manifest.id}/obj")
@ -96,22 +88,25 @@ class ComponentInstance:
def cinclude(self) -> str: def cinclude(self) -> str:
if "cpp-root-include" in self.manifest.props: if "cpp-root-include" in self.manifest.props:
return self.manifest.dirname() return self.manifest.dirname()
elif self.manifest.type == Type.LIB: elif self.manifest.type == model.Type.LIB:
return str(Path(self.manifest.dirname()).parent) return str(Path(self.manifest.dirname()).parent)
else: else:
return "" return ""
class Context(IContext): class Context(IContext):
target: Target target: model.Target
instances: list[ComponentInstance] instances: list[ComponentInstance]
tools: Tools tools: model.Tools
def enabledInstances(self) -> Iterable[ComponentInstance]: def enabledInstances(self) -> Iterable[ComponentInstance]:
return filter(lambda x: x.enabled, self.instances) return filter(lambda x: x.enabled, self.instances)
def __init__( def __init__(
self, target: Target, instances: list[ComponentInstance], tools: Tools self,
target: model.Target,
instances: list[ComponentInstance],
tools: model.Tools,
): ):
self.target = target self.target = target
self.instances = instances self.instances = instances
@ -143,8 +138,8 @@ class Context(IContext):
return os.path.join(const.BUILD_DIR, f"{self.target.id}-{self.hashid()[:8]}") return os.path.join(const.BUILD_DIR, f"{self.target.id}-{self.hashid()[:8]}")
def loadAllTargets() -> list[Target]: def loadAllTargets() -> list[model.Target]:
projectRoot = project.root() projectRoot = model.Project.root()
if projectRoot is None: if projectRoot is None:
return [] return []
@ -159,40 +154,42 @@ def loadAllTargets() -> list[Target]:
ret = [] ret = []
for entry in paths: for entry in paths:
files = shell.find(entry, ["*.json"]) files = shell.find(entry, ["*.json"])
ret += list(map(lambda path: Target(jexpr.evalRead(path), path), files)) ret += list(map(lambda path: model.Target(jexpr.evalRead(path), path), files))
return ret return ret
def loadProject(path: str) -> Project: def loadProject(path: str) -> model.Project:
path = os.path.join(path, "project.json") path = os.path.join(path, "project.json")
return Project(jexpr.evalRead(path), path) return model.Project(jexpr.evalRead(path), path)
def loadTarget(id: str) -> Target: def loadTarget(id: str) -> model.Target:
try: try:
return next(filter(lambda t: t.id == id, loadAllTargets())) return next(filter(lambda t: t.id == id, loadAllTargets()))
except StopIteration: except StopIteration:
raise RuntimeError(f"Target '{id}' not found") raise RuntimeError(f"Target '{id}' not found")
def loadAllComponents() -> list[Component]: def loadAllComponents() -> list[model.Component]:
files = shell.find(const.SRC_DIR, ["manifest.json"]) files = shell.find(const.SRC_DIR, ["manifest.json"])
files += shell.find(const.EXTERN_DIR, ["manifest.json"]) files += shell.find(const.EXTERN_DIR, ["manifest.json"])
return list(map(lambda path: Component(jexpr.evalRead(path), path), files)) return list(map(lambda path: model.Component(jexpr.evalRead(path), path), files))
def filterDisabled( def filterDisabled(
components: list[Component], target: Target components: list[model.Component], target: model.Target
) -> tuple[list[Component], list[Component]]: ) -> tuple[list[model.Component], list[model.Component]]:
return list(filter(lambda c: c.isEnabled(target)[0], components)), list( return list(filter(lambda c: c.isEnabled(target)[0], components)), list(
filter(lambda c: not c.isEnabled(target)[0], components) filter(lambda c: not c.isEnabled(target)[0], components)
) )
def providerFor(what: str, components: list[Component]) -> tuple[Optional[str], str]: def providerFor(
result: list[Component] = list(filter(lambda c: c.id == what, components)) 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: if len(result) == 0:
# Try to find a provider # Try to find a provider
@ -211,7 +208,7 @@ def providerFor(what: str, components: list[Component]) -> tuple[Optional[str],
def resolveDeps( def resolveDeps(
componentSpec: str, components: list[Component], target: Target componentSpec: str, components: list[model.Component], target: model.Target
) -> tuple[bool, str, list[str]]: ) -> tuple[bool, str, list[str]]:
mapping = dict(map(lambda c: (c.id, c), components)) mapping = dict(map(lambda c: (c.id, c), components))
@ -249,7 +246,7 @@ def resolveDeps(
def instanciate( def instanciate(
componentSpec: str, components: list[Component], target: Target componentSpec: str, components: list[model.Component], target: model.Target
) -> Optional[ComponentInstance]: ) -> Optional[ComponentInstance]:
manifest = next(filter(lambda c: c.id == componentSpec, components)) manifest = next(filter(lambda c: c.id == componentSpec, components))
wildcards = set(chain(*map(lambda rule: rule.fileIn, rules.rules.values()))) wildcards = set(chain(*map(lambda rule: rule.fileIn, rules.rules.values())))
@ -264,7 +261,9 @@ def instanciate(
) )
def instanciateDisabled(component: Component, target: Target) -> ComponentInstance: def instanciateDisabled(
component: model.Component, target: model.Target
) -> ComponentInstance:
return ComponentInstance( return ComponentInstance(
enabled=False, enabled=False,
disableReason=component.isEnabled(target)[1], disableReason=component.isEnabled(target)[1],
@ -278,7 +277,7 @@ def instanciateDisabled(component: Component, target: Target) -> ComponentInstan
context: dict[str, Context] = {} context: dict[str, Context] = {}
def contextFor(targetSpec: str, props: Props = {}) -> Context: def contextFor(targetSpec: str, props: model.Props = {}) -> Context:
if targetSpec in context: if targetSpec in context:
return context[targetSpec] return context[targetSpec]
@ -295,12 +294,12 @@ def contextFor(targetSpec: str, props: Props = {}) -> Context:
components = loadAllComponents() components = loadAllComponents()
components, disabled = filterDisabled(components, target) components, disabled = filterDisabled(components, target)
tools: Tools = {} tools: model.Tools = {}
for toolSpec in target.tools: for toolSpec in target.tools:
tool = target.tools[toolSpec] tool = target.tools[toolSpec]
tools[toolSpec] = Tool( tools[toolSpec] = model.Tool(
strict=False, cmd=tool.cmd, args=tool.args, files=tool.files strict=False, cmd=tool.cmd, args=tool.args, files=tool.files
) )

View file

@ -1,7 +1,7 @@
import os import os
from typing import cast from typing import cast
from . import vt100, context, project, cli, shell from . import vt100, context, cli, shell, model
def view( def view(
@ -10,7 +10,7 @@ def view(
showExe: bool = True, showExe: bool = True,
showDisabled: bool = False, showDisabled: bool = False,
): ):
from graphviz import Digraph from graphviz import Digraph # type: ignore
g = Digraph(context.target.id, filename="graph.gv") g = Digraph(context.target.id, filename="graph.gv")
@ -83,7 +83,7 @@ def view(
@cli.command("g", "graph", "Show the dependency graph") @cli.command("g", "graph", "Show the dependency graph")
def graphCmd(args: cli.Args): def graphCmd(args: cli.Args):
project.chdir() model.Project.chdir()
targetSpec = str(args.consumeOpt("target", "host-" + shell.uname().machine)) targetSpec = str(args.consumeOpt("target", "host-" + shell.uname().machine))

View file

@ -1,9 +1,8 @@
import os import os
from typing import Any, cast, Callable, Final
import json import json
import cutekit.shell as shell from typing import Any, cast, Callable, Final
from cutekit.compat import ensureSupportedManifest from . import shell, compat
Json = Any Json = Any
Builtin = Callable[..., Json] Builtin = Callable[..., Json]
@ -61,5 +60,5 @@ def read(path: str) -> Json:
def evalRead(path: str, compatibilityCheck: bool = True) -> Json: def evalRead(path: str, compatibilityCheck: bool = True) -> Json:
data = read(path) data = read(path)
if compatibilityCheck: if compatibilityCheck:
ensureSupportedManifest(data, path) compat.ensureSupportedManifest(data, path)
return eval(data, path) return eval(data, path)

View file

@ -1,25 +1,26 @@
from typing import Callable from typing import Callable
from cutekit.model import Target, Tools
Mixin = Callable[[Target, Tools], Tools] from . import model
Mixin = Callable[[model.Target, model.Tools], model.Tools]
def patchToolArgs(tools: Tools, toolSpec: str, args: list[str]): def patchToolArgs(tools: model.Tools, toolSpec: str, args: list[str]):
tools[toolSpec].args += args tools[toolSpec].args += args
def prefixToolCmd(tools: Tools, toolSpec: str, prefix: str): def prefixToolCmd(tools: model.Tools, toolSpec: str, prefix: str):
tools[toolSpec].cmd = prefix + " " + tools[toolSpec].cmd tools[toolSpec].cmd = prefix + " " + tools[toolSpec].cmd
def mixinCache(target: Target, tools: Tools) -> Tools: def mixinCache(target: model.Target, tools: model.Tools) -> model.Tools:
prefixToolCmd(tools, "cc", "ccache") prefixToolCmd(tools, "cc", "ccache")
prefixToolCmd(tools, "cxx", "ccache") prefixToolCmd(tools, "cxx", "ccache")
return tools return tools
def makeMixinSan(san: str) -> Mixin: def makeMixinSan(san: str) -> Mixin:
def mixinSan(target: Target, tools: Tools) -> Tools: def mixinSan(target: model.Target, tools: model.Tools) -> model.Tools:
patchToolArgs(tools, "cc", [f"-fsanitize={san}"]) patchToolArgs(tools, "cc", [f"-fsanitize={san}"])
patchToolArgs(tools, "cxx", [f"-fsanitize={san}"]) patchToolArgs(tools, "cxx", [f"-fsanitize={san}"])
patchToolArgs(tools, "ld", [f"-fsanitize={san}"]) patchToolArgs(tools, "ld", [f"-fsanitize={san}"])
@ -30,7 +31,7 @@ def makeMixinSan(san: str) -> Mixin:
def makeMixinOptimize(level: str) -> Mixin: def makeMixinOptimize(level: str) -> Mixin:
def mixinOptimize(target: Target, tools: Tools) -> Tools: def mixinOptimize(target: model.Target, tools: model.Tools) -> model.Tools:
patchToolArgs(tools, "cc", [f"-O{level}"]) patchToolArgs(tools, "cc", [f"-O{level}"])
patchToolArgs(tools, "cxx", [f"-O{level}"]) patchToolArgs(tools, "cxx", [f"-O{level}"])
@ -39,7 +40,7 @@ def makeMixinOptimize(level: str) -> Mixin:
return mixinOptimize return mixinOptimize
def mixinDebug(target: Target, tools: Tools) -> Tools: def mixinDebug(target: model.Target, tools: model.Tools) -> model.Tools:
patchToolArgs(tools, "cc", ["-g", "-gdwarf-4"]) patchToolArgs(tools, "cc", ["-g", "-gdwarf-4"])
patchToolArgs(tools, "cxx", ["-g", "-gdwarf-4"]) patchToolArgs(tools, "cxx", ["-g", "-gdwarf-4"])
@ -47,7 +48,7 @@ def mixinDebug(target: Target, tools: Tools) -> Tools:
def makeMixinTune(tune: str) -> Mixin: def makeMixinTune(tune: str) -> Mixin:
def mixinTune(target: Target, tools: Tools) -> Tools: def mixinTune(target: model.Target, tools: model.Tools) -> model.Tools:
patchToolArgs(tools, "cc", [f"-mtune={tune}"]) patchToolArgs(tools, "cc", [f"-mtune={tune}"])
patchToolArgs(tools, "cxx", [f"-mtune={tune}"]) patchToolArgs(tools, "cxx", [f"-mtune={tune}"])

View file

@ -1,9 +1,10 @@
import os import os
from enum import Enum
from typing import Any
import logging import logging
from cutekit.jexpr import Json from enum import Enum
from typing import Any
from pathlib import Path
from . import jexpr
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -25,7 +26,11 @@ class Manifest:
path: str = "" path: str = ""
def __init__( def __init__(
self, json: Json = None, path: str = "", strict: bool = True, **kwargs: Any self,
json: jexpr.Json = None,
path: str = "",
strict: bool = True,
**kwargs: Any,
): ):
if json is not None: if json is not None:
if "id" not in json: if "id" not in json:
@ -45,7 +50,7 @@ class Manifest:
for key in kwargs: for key in kwargs:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])
def toJson(self) -> Json: def toJson(self) -> jexpr.Json:
return {"id": self.id, "type": self.type.value, "path": self.path} return {"id": self.id, "type": self.type.value, "path": self.path}
def __str__(self): def __str__(self):
@ -62,7 +67,7 @@ class Extern:
git: str = "" git: str = ""
tag: str = "" tag: str = ""
def __init__(self, json: Json = None, strict: bool = True, **kwargs: Any): def __init__(self, json: jexpr.Json = None, strict: bool = True, **kwargs: Any):
if json is not None: if json is not None:
if "git" not in json and strict: if "git" not in json and strict:
raise RuntimeError("Missing git") raise RuntimeError("Missing git")
@ -79,7 +84,7 @@ class Extern:
for key in kwargs: for key in kwargs:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])
def toJson(self) -> Json: def toJson(self) -> jexpr.Json:
return {"git": self.git, "tag": self.tag} return {"git": self.git, "tag": self.tag}
def __str__(self): def __str__(self):
@ -94,7 +99,11 @@ class Project(Manifest):
extern: dict[str, Extern] = {} extern: dict[str, Extern] = {}
def __init__( def __init__(
self, json: Json = None, path: str = "", strict: bool = True, **kwargs: Any self,
json: jexpr.Json = None,
path: str = "",
strict: bool = True,
**kwargs: Any,
): ):
if json is not None: if json is not None:
if "description" not in json and strict: if "description" not in json and strict:
@ -108,7 +117,7 @@ class Project(Manifest):
super().__init__(json, path, strict, **kwargs) super().__init__(json, path, strict, **kwargs)
def toJson(self) -> Json: def toJson(self) -> jexpr.Json:
return { return {
**super().toJson(), **super().toJson(),
"description": self.description, "description": self.description,
@ -121,13 +130,31 @@ class Project(Manifest):
def __repr__(self): def __repr__(self):
return f"ProjectManifest({self.id})" return f"ProjectManifest({self.id})"
@staticmethod
def root() -> str | None:
cwd = Path.cwd()
while str(cwd) != cwd.root:
if (cwd / "project.json").is_file():
return str(cwd)
cwd = cwd.parent
return None
@staticmethod
def chdir() -> None:
path = Project.root()
if path is None:
raise RuntimeError(
"No project.json found in this directory or any parent directory"
)
os.chdir(path)
class Tool: class Tool:
cmd: str = "" cmd: str = ""
args: list[str] = [] args: list[str] = []
files: list[str] = [] files: list[str] = []
def __init__(self, json: Json = None, strict: bool = True, **kwargs: Any): def __init__(self, json: jexpr.Json = None, strict: bool = True, **kwargs: Any):
if json is not None: if json is not None:
if "cmd" not in json and strict: if "cmd" not in json and strict:
raise RuntimeError("Missing cmd") raise RuntimeError("Missing cmd")
@ -146,7 +173,7 @@ class Tool:
for key in kwargs: for key in kwargs:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])
def toJson(self) -> Json: def toJson(self) -> jexpr.Json:
return {"cmd": self.cmd, "args": self.args, "files": self.files} return {"cmd": self.cmd, "args": self.args, "files": self.files}
def __str__(self): def __str__(self):
@ -165,7 +192,11 @@ class Target(Manifest):
routing: dict[str, str] routing: dict[str, str]
def __init__( def __init__(
self, json: Json = None, path: str = "", strict: bool = True, **kwargs: Any self,
json: jexpr.Json = None,
path: str = "",
strict: bool = True,
**kwargs: Any,
): ):
if json is not None: if json is not None:
if "props" not in json and strict: if "props" not in json and strict:
@ -182,7 +213,7 @@ class Target(Manifest):
super().__init__(json, path, strict, **kwargs) super().__init__(json, path, strict, **kwargs)
def toJson(self) -> Json: def toJson(self) -> jexpr.Json:
return { return {
**super().toJson(), **super().toJson(),
"props": self.props, "props": self.props,
@ -229,7 +260,11 @@ class Component(Manifest):
subdirs: list[str] = [] subdirs: list[str] = []
def __init__( def __init__(
self, json: Json = None, path: str = "", strict: bool = True, **kwargs: Any self,
json: jexpr.Json = None,
path: str = "",
strict: bool = True,
**kwargs: Any,
): ):
if json is not None: if json is not None:
self.decription = json.get("description", self.decription) self.decription = json.get("description", self.decription)
@ -249,7 +284,7 @@ class Component(Manifest):
super().__init__(json, path, strict, **kwargs) super().__init__(json, path, strict, **kwargs)
def toJson(self) -> Json: def toJson(self) -> jexpr.Json:
return { return {
**super().toJson(), **super().toJson(),
"description": self.decription, "description": self.decription,

View file

@ -23,13 +23,13 @@ use Python.
""" """
import textwrap import textwrap
from typing import TextIO, Union
from cutekit.utils import asList from typing import TextIO, Union
from . import utils
def escapePath(word: str) -> str: def escapePath(word: str) -> str:
return word.replace('$ ', '$$ ').replace(' ', '$ ').replace(':', '$:') return word.replace("$ ", "$$ ").replace(" ", "$ ").replace(":", "$:")
VarValue = Union[int, str, list[str], None] VarValue = Union[int, str, list[str], None]
@ -42,92 +42,98 @@ class Writer(object):
self.width = width self.width = width
def newline(self) -> None: def newline(self) -> None:
self.output.write('\n') self.output.write("\n")
def comment(self, text: str) -> None: def comment(self, text: str) -> None:
for line in textwrap.wrap(text, self.width - 2, break_long_words=False, for line in textwrap.wrap(
break_on_hyphens=False): text, self.width - 2, break_long_words=False, break_on_hyphens=False
self.output.write('# ' + line + '\n') ):
self.output.write("# " + line + "\n")
def separator(self, text : str) -> None: def separator(self, text: str) -> None:
self.output.write(f"# --- {text} ---" + '-' * self.output.write(
(self.width - 10 - len(text)) + " #\n\n") f"# --- {text} ---" + "-" * (self.width - 10 - len(text)) + " #\n\n"
)
def variable(self, key: str, value: VarValue, indent: int = 0) -> None: def variable(self, key: str, value: VarValue, indent: int = 0) -> None:
if value is None: if value is None:
return return
if isinstance(value, list): if isinstance(value, list):
value = ' '.join(filter(None, value)) # Filter out empty strings. value = " ".join(filter(None, value)) # Filter out empty strings.
self._line('%s = %s' % (key, value), indent) self._line("%s = %s" % (key, value), indent)
def pool(self, name: str, depth: int) -> None: def pool(self, name: str, depth: int) -> None:
self._line('pool %s' % name) self._line("pool %s" % name)
self.variable('depth', depth, indent=1) self.variable("depth", depth, indent=1)
def rule(self, def rule(
name: str, self,
command: VarValue, name: str,
description: Union[str, None] = None, command: VarValue,
depfile: VarValue = None, description: Union[str, None] = None,
generator: VarValue = False, depfile: VarValue = None,
pool: VarValue = None, generator: VarValue = False,
restat: bool = False, pool: VarValue = None,
rspfile: VarValue = None, restat: bool = False,
rspfile_content: VarValue = None, rspfile: VarValue = None,
deps: VarValue = None) -> None: rspfile_content: VarValue = None,
self._line('rule %s' % name) deps: VarValue = None,
self.variable('command', command, indent=1) ) -> None:
self._line("rule %s" % name)
self.variable("command", command, indent=1)
if description: if description:
self.variable('description', description, indent=1) self.variable("description", description, indent=1)
if depfile: if depfile:
self.variable('depfile', depfile, indent=1) self.variable("depfile", depfile, indent=1)
if generator: if generator:
self.variable('generator', '1', indent=1) self.variable("generator", "1", indent=1)
if pool: if pool:
self.variable('pool', pool, indent=1) self.variable("pool", pool, indent=1)
if restat: if restat:
self.variable('restat', '1', indent=1) self.variable("restat", "1", indent=1)
if rspfile: if rspfile:
self.variable('rspfile', rspfile, indent=1) self.variable("rspfile", rspfile, indent=1)
if rspfile_content: if rspfile_content:
self.variable('rspfile_content', rspfile_content, indent=1) self.variable("rspfile_content", rspfile_content, indent=1)
if deps: if deps:
self.variable('deps', deps, indent=1) self.variable("deps", deps, indent=1)
def build(self, def build(
outputs: Union[str, list[str]], self,
rule: str, outputs: Union[str, list[str]],
inputs: Union[VarPath, None], rule: str,
implicit: VarPath = None, inputs: Union[VarPath, None],
order_only: VarPath = None, implicit: VarPath = None,
variables: Union[dict[str, str], None] = None, order_only: VarPath = None,
implicit_outputs: VarPath = None, variables: Union[dict[str, str], None] = None,
pool: Union[str, None] = None, implicit_outputs: VarPath = None,
dyndep: Union[str, None] = None) -> list[str]: pool: Union[str, None] = None,
outputs = asList(outputs) dyndep: Union[str, None] = None,
) -> list[str]:
outputs = utils.asList(outputs)
out_outputs = [escapePath(x) for x in outputs] out_outputs = [escapePath(x) for x in outputs]
all_inputs = [escapePath(x) for x in asList(inputs)] all_inputs = [escapePath(x) for x in utils.asList(inputs)]
if implicit: if implicit:
implicit = [escapePath(x) for x in asList(implicit)] implicit = [escapePath(x) for x in utils.asList(implicit)]
all_inputs.append('|') all_inputs.append("|")
all_inputs.extend(implicit) all_inputs.extend(implicit)
if order_only: if order_only:
order_only = [escapePath(x) for x in asList(order_only)] order_only = [escapePath(x) for x in utils.asList(order_only)]
all_inputs.append('||') all_inputs.append("||")
all_inputs.extend(order_only) all_inputs.extend(order_only)
if implicit_outputs: if implicit_outputs:
implicit_outputs = [escapePath(x) implicit_outputs = [escapePath(x) for x in utils.asList(implicit_outputs)]
for x in asList(implicit_outputs)] out_outputs.append("|")
out_outputs.append('|')
out_outputs.extend(implicit_outputs) out_outputs.extend(implicit_outputs)
self._line('build %s: %s' % (' '.join(out_outputs), self._line(
' '.join([rule] + all_inputs))) "build %s: %s" % (" ".join(out_outputs), " ".join([rule] + all_inputs))
)
if pool is not None: if pool is not None:
self._line(' pool = %s' % pool) self._line(" pool = %s" % pool)
if dyndep is not None: if dyndep is not None:
self._line(' dyndep = %s' % dyndep) self._line(" dyndep = %s" % dyndep)
if variables: if variables:
iterator = iter(variables.items()) iterator = iter(variables.items())
@ -138,58 +144,59 @@ class Writer(object):
return outputs return outputs
def include(self, path: str) -> None: def include(self, path: str) -> None:
self._line('include %s' % path) self._line("include %s" % path)
def subninja(self, path: str) -> None: def subninja(self, path: str) -> None:
self._line('subninja %s' % path) self._line("subninja %s" % path)
def default(self, paths: VarPath) -> None: def default(self, paths: VarPath) -> None:
self._line('default %s' % ' '.join(asList(paths))) self._line("default %s" % " ".join(utils.asList(paths)))
def _count_dollars_before_index(self, s: str, i: int) -> int: def _count_dollars_before_index(self, s: str, i: int) -> int:
"""Returns the number of '$' characters right in front of s[i].""" """Returns the number of '$' characters right in front of s[i]."""
dollar_count = 0 dollar_count = 0
dollar_index = i - 1 dollar_index = i - 1
while dollar_index > 0 and s[dollar_index] == '$': while dollar_index > 0 and s[dollar_index] == "$":
dollar_count += 1 dollar_count += 1
dollar_index -= 1 dollar_index -= 1
return dollar_count return dollar_count
def _line(self, text: str, indent: int = 0) -> None: def _line(self, text: str, indent: int = 0) -> None:
"""Write 'text' word-wrapped at self.width characters.""" """Write 'text' word-wrapped at self.width characters."""
leading_space = ' ' * indent leading_space = " " * indent
while len(leading_space) + len(text) > self.width: while len(leading_space) + len(text) > self.width:
# The text is too wide; wrap if possible. # The text is too wide; wrap if possible.
# Find the rightmost space that would obey our width constraint and # Find the rightmost space that would obey our width constraint and
# that's not an escaped space. # that's not an escaped space.
available_space = self.width - len(leading_space) - len(' $') available_space = self.width - len(leading_space) - len(" $")
space = available_space space = available_space
while True: while True:
space = text.rfind(' ', 0, space) space = text.rfind(" ", 0, space)
if (space < 0 or if space < 0 or self._count_dollars_before_index(text, space) % 2 == 0:
self._count_dollars_before_index(text, space) % 2 == 0):
break break
if space < 0: if space < 0:
# No such space; just use the first unescaped space we can find. # No such space; just use the first unescaped space we can find.
space = available_space - 1 space = available_space - 1
while True: while True:
space = text.find(' ', space + 1) space = text.find(" ", space + 1)
if (space < 0 or if (
self._count_dollars_before_index(text, space) % 2 == 0): space < 0
or self._count_dollars_before_index(text, space) % 2 == 0
):
break break
if space < 0: if space < 0:
# Give up on breaking. # Give up on breaking.
break break
self.output.write(leading_space + text[0:space] + ' $\n') self.output.write(leading_space + text[0:space] + " $\n")
text = text[space+1:] text = text[space + 1 :]
# Subsequent lines are continuations, so indent them. # Subsequent lines are continuations, so indent them.
leading_space = ' ' * (indent+2) leading_space = " " * (indent + 2)
self.output.write(leading_space + text + '\n') self.output.write(leading_space + text + "\n")
def close(self) -> None: def close(self) -> None:
self.output.close() self.output.close()
@ -198,6 +205,6 @@ class Writer(object):
def escape(string: str) -> str: def escape(string: str) -> str:
"""Escape a string such that it can be embedded into a Ninja file without """Escape a string such that it can be embedded into a Ninja file without
further interpretation.""" further interpretation."""
assert '\n' not in string, 'Ninja syntax does not allow newlines' assert "\n" not in string, "Ninja syntax does not allow newlines"
# We only have one special metacharacter: '$'. # We only have one special metacharacter: '$'.
return string.replace('$', '$$') return string.replace("$", "$$")

View file

@ -1,7 +1,7 @@
import os import os
import logging import logging
from cutekit import shell, project, const, context from . import shell, model, const, context
import importlib.util as importlib import importlib.util as importlib
@ -23,7 +23,7 @@ def load(path: str):
def loadAll(): def loadAll():
_logger.info("Loading plugins...") _logger.info("Loading plugins...")
projectRoot = project.root() projectRoot = model.Project.root()
if projectRoot is None: if projectRoot is None:
_logger.info("Not in project, skipping plugin loading") _logger.info("Not in project, skipping plugin loading")

View file

@ -1,22 +0,0 @@
import os
from pathlib import Path
def root() -> str | None:
cwd = Path.cwd()
while str(cwd) != cwd.root:
if (cwd / "project.json").is_file():
return str(cwd)
cwd = cwd.parent
return None
def chdir() -> None:
projectRoot = root()
if projectRoot is None:
raise RuntimeError(
"No project.json found in this directory or any parent directory"
)
os.chdir(projectRoot)

View file

@ -13,7 +13,7 @@ import tempfile
from typing import Optional from typing import Optional
from cutekit import const from . import const
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)

View file

@ -1,5 +1,4 @@
# Extending cutekit
# Extending cutekit
By writing custom Python plugins, you can extend Cutekit to do whatever you want. By writing custom Python plugins, you can extend Cutekit to do whatever you want.
@ -9,24 +8,11 @@ Then you can import cutekit and change/add whatever you want.
For example you can add a new command to the CLI: For example you can add a new command to the CLI:
```python ```python
import os from cutekit import cli
import json
import magic
import logging
from pathlib import Path
@cli.command("h", "hello", "Print hello world")
from cutekit import shell, builder, const, project def bootCmd(args: cli.Args) -> None:
from cutekit.cmds import Cmd, append
from cutekit.args import Args
from typing import Callable
def bootCmd(args: Args) -> None:
project.chdir()
print("Hello world!") print("Hello world!")
append(Cmd("h", "hello", "Print hello world", bootCmd))
``` ```
This feature is used - for example - by [SkiftOS](https://github.com/skift-org/skift/blob/main/meta/plugins/start-cmd.py) to add the `start` command, that build packages and run a virtual machine. This feature is used - for example - by [SkiftOS](https://github.com/skift-org/skift/blob/main/meta/plugins/start-cmd.py) to add the `start` command, that build packages and run a virtual machine.