diff --git a/cutekit/__init__.py b/cutekit/__init__.py index e787119..f0d4909 100644 --- a/cutekit/__init__.py +++ b/cutekit/__init__.py @@ -2,7 +2,7 @@ import sys import os import logging -from cutekit import const, project, vt100, plugins, cmds, cli +from . import const, model, vt100, plugins, cmds, cli def setupLogger(verbose: bool): @@ -13,7 +13,7 @@ def setupLogger(verbose: bool): datefmt="%Y-%m-%d %H:%M:%S", ) else: - projectRoot = project.root() + projectRoot = model.Project.root() logFile = const.GLOBAL_LOG_FILE if projectRoot is not None: logFile = os.path.join(projectRoot, const.PROJECT_LOG_FILE) diff --git a/cutekit/builder.py b/cutekit/builder.py index 7874af9..84ad9b3 100644 --- a/cutekit/builder.py +++ b/cutekit/builder.py @@ -2,16 +2,13 @@ import os import logging from typing import TextIO -from cutekit.model import Props -from cutekit.ninja import Writer -from cutekit.context import ComponentInstance, Context, contextFor -from cutekit import shell, rules +from . import shell, rules, model, ninja, context _logger = logging.getLogger(__name__) -def gen(out: TextIO, context: Context): - writer = Writer(out) +def gen(out: TextIO, context: context.Context): + writer = ninja.Writer(out) target = context.target @@ -102,16 +99,18 @@ def gen(out: TextIO, context: Context): writer.default("all") -def build(componentSpec: str, targetSpec: str, props: Props = {}) -> ComponentInstance: - context = contextFor(targetSpec, props) +def build( + componentSpec: str, targetSpec: str, props: model.Props = {} +) -> context.ComponentInstance: + ctx = context.contextFor(targetSpec, props) - shell.mkdir(context.builddir()) - ninjaPath = os.path.join(context.builddir(), "build.ninja") + shell.mkdir(ctx.builddir()) + ninjaPath = os.path.join(ctx.builddir(), "build.ninja") with open(ninjaPath, "w") as f: - gen(f, context) + gen(f, ctx) - instance = context.componentByName(componentSpec) + instance = ctx.componentByName(componentSpec) if instance is None: raise RuntimeError(f"Component {componentSpec} not found") @@ -137,32 +136,32 @@ class Paths: self.obj = obj -def buildAll(targetSpec: str, props: Props = {}) -> Context: - context = contextFor(targetSpec, props) +def buildAll(targetSpec: str, props: model.Props = {}) -> context.Context: + ctx = context.contextFor(targetSpec, props) - shell.mkdir(context.builddir()) - ninjaPath = os.path.join(context.builddir(), "build.ninja") + shell.mkdir(ctx.builddir()) + ninjaPath = os.path.join(ctx.builddir(), "build.ninja") with open(ninjaPath, "w") as f: - gen(f, context) + gen(f, ctx) shell.exec("ninja", "-v", "-f", ninjaPath) - return context + return ctx def testAll(targetSpec: str): - context = contextFor(targetSpec) + ctx = context.contextFor(targetSpec) - shell.mkdir(context.builddir()) - ninjaPath = os.path.join(context.builddir(), "build.ninja") + shell.mkdir(ctx.builddir()) + ninjaPath = os.path.join(ctx.builddir(), "build.ninja") with open(ninjaPath, "w") as f: - gen(f, context) + gen(f, ctx) shell.exec("ninja", "-v", "-f", ninjaPath, "all") - for instance in context.enabledInstances(): + for instance in ctx.enabledInstances(): if instance.isLib(): continue diff --git a/cutekit/cmds.py b/cutekit/cmds.py index 9c09d2b..1b2b857 100644 --- a/cutekit/cmds.py +++ b/cutekit/cmds.py @@ -3,13 +3,12 @@ import os import sys -from cutekit import ( +from . import ( context, shell, const, vt100, builder, - project, cli, model, jexpr, @@ -21,7 +20,7 @@ _logger = logging.getLogger(__name__) @cli.command("p", "project", "Show project information") def runCmd(args: cli.Args): - project.chdir() + model.Project.chdir() targetSpec = str(args.consumeOpt("target", "host-" + shell.uname().machine)) props = args.consumePrefix("prop:") @@ -42,7 +41,7 @@ def runCmd(args: cli.Args): @cli.command("t", "test", "Run all test targets") def testCmd(args: cli.Args): - project.chdir() + model.Project.chdir() targetSpec = str(args.consumeOpt("target", "host-" + shell.uname().machine)) builder.testAll(targetSpec) @@ -50,7 +49,7 @@ def testCmd(args: cli.Args): @cli.command("d", "debug", "Debug a component") def debugCmd(args: cli.Args): - project.chdir() + model.Project.chdir() targetSpec = str(args.consumeOpt("target", "host-" + shell.uname().machine)) props = args.consumePrefix("prop:") @@ -71,7 +70,7 @@ def debugCmd(args: cli.Args): @cli.command("b", "build", "Build a component or all components") def buildCmd(args: cli.Args): - project.chdir() + model.Project.chdir() targetSpec = str(args.consumeOpt("target", "host-" + shell.uname().machine)) props = args.consumePrefix("prop:") @@ -85,7 +84,7 @@ def buildCmd(args: cli.Args): @cli.command("l", "list", "List all components and targets") def listCmd(args: cli.Args): - project.chdir() + model.Project.chdir() components = context.loadAllComponents() targets = context.loadAllTargets() @@ -109,13 +108,13 @@ def listCmd(args: cli.Args): @cli.command("c", "clean", "Clean build files") def cleanCmd(args: cli.Args): - project.chdir() + model.Project.chdir() shell.rmrf(const.BUILD_DIR) @cli.command("n", "nuke", "Clean all build files and caches") def nukeCmd(args: cli.Args): - project.chdir() + model.Project.chdir() 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") def installCmd(args: cli.Args): - project.chdir() - + model.Project.chdir() pj = context.loadProject(".") grabExtern(pj.extern) diff --git a/cutekit/context.py b/cutekit/context.py index d72e65c..5780a3c 100644 --- a/cutekit/context.py +++ b/cutekit/context.py @@ -4,22 +4,14 @@ from pathlib import Path import os import logging -from cutekit.model import ( - Project, - Target, - Component, - Props, - Type, - Tool, - Tools, -) -from cutekit import const, shell, jexpr, utils, rules, mixins, project + +from . import const, shell, jexpr, utils, rules, mixins, project, model _logger = logging.getLogger(__name__) class IContext(Protocol): - target: Target + target: model.Target def builddir(self) -> str: ... @@ -28,7 +20,7 @@ class IContext(Protocol): class ComponentInstance: enabled: bool = True disableReason = "" - manifest: Component + manifest: model.Component sources: list[str] = [] res: list[str] = [] resolved: list[str] = [] @@ -38,7 +30,7 @@ class ComponentInstance: self, enabled: bool, disableReason: str, - manifest: Component, + manifest: model.Component, sources: list[str], res: list[str], resolved: list[str], @@ -54,7 +46,7 @@ class ComponentInstance: return self.manifest.id def isLib(self): - return self.manifest.type == Type.LIB + return self.manifest.type == model.Type.LIB def objdir(self) -> str: return os.path.join(self.context.builddir(), f"{self.manifest.id}/obj") @@ -96,22 +88,25 @@ class ComponentInstance: def cinclude(self) -> str: if "cpp-root-include" in self.manifest.props: return self.manifest.dirname() - elif self.manifest.type == Type.LIB: + elif self.manifest.type == model.Type.LIB: return str(Path(self.manifest.dirname()).parent) else: return "" class Context(IContext): - target: Target + target: model.Target instances: list[ComponentInstance] - tools: Tools + tools: model.Tools def enabledInstances(self) -> Iterable[ComponentInstance]: return filter(lambda x: x.enabled, self.instances) def __init__( - self, target: Target, instances: list[ComponentInstance], tools: Tools + self, + target: model.Target, + instances: list[ComponentInstance], + tools: model.Tools, ): self.target = target self.instances = instances @@ -143,8 +138,8 @@ class Context(IContext): return os.path.join(const.BUILD_DIR, f"{self.target.id}-{self.hashid()[:8]}") -def loadAllTargets() -> list[Target]: - projectRoot = project.root() +def loadAllTargets() -> list[model.Target]: + projectRoot = model.Project.root() if projectRoot is None: return [] @@ -159,40 +154,42 @@ def loadAllTargets() -> list[Target]: ret = [] for entry in paths: 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 -def loadProject(path: str) -> Project: +def loadProject(path: str) -> model.Project: 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: return next(filter(lambda t: t.id == id, loadAllTargets())) except StopIteration: 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.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( - components: list[Component], target: Target -) -> tuple[list[Component], list[Component]]: + 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[Component]) -> tuple[Optional[str], str]: - result: list[Component] = list(filter(lambda c: c.id == what, 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 @@ -211,7 +208,7 @@ def providerFor(what: str, components: list[Component]) -> tuple[Optional[str], def resolveDeps( - componentSpec: str, components: list[Component], target: Target + componentSpec: str, components: list[model.Component], target: model.Target ) -> tuple[bool, str, list[str]]: mapping = dict(map(lambda c: (c.id, c), components)) @@ -249,7 +246,7 @@ def resolveDeps( def instanciate( - componentSpec: str, components: list[Component], target: Target + 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()))) @@ -264,7 +261,9 @@ def instanciate( ) -def instanciateDisabled(component: Component, target: Target) -> ComponentInstance: +def instanciateDisabled( + component: model.Component, target: model.Target +) -> ComponentInstance: return ComponentInstance( enabled=False, disableReason=component.isEnabled(target)[1], @@ -278,7 +277,7 @@ def instanciateDisabled(component: Component, target: Target) -> ComponentInstan context: dict[str, Context] = {} -def contextFor(targetSpec: str, props: Props = {}) -> Context: +def contextFor(targetSpec: str, props: model.Props = {}) -> Context: if targetSpec in context: return context[targetSpec] @@ -295,12 +294,12 @@ def contextFor(targetSpec: str, props: Props = {}) -> Context: components = loadAllComponents() components, disabled = filterDisabled(components, target) - tools: Tools = {} + tools: model.Tools = {} for toolSpec in target.tools: tool = target.tools[toolSpec] - tools[toolSpec] = Tool( + tools[toolSpec] = model.Tool( strict=False, cmd=tool.cmd, args=tool.args, files=tool.files ) diff --git a/cutekit/graph.py b/cutekit/graph.py index 6f64eec..19c9b3d 100644 --- a/cutekit/graph.py +++ b/cutekit/graph.py @@ -1,7 +1,7 @@ import os from typing import cast -from . import vt100, context, project, cli, shell +from . import vt100, context, cli, shell, model def view( @@ -10,7 +10,7 @@ def view( showExe: bool = True, showDisabled: bool = False, ): - from graphviz import Digraph + from graphviz import Digraph # type: ignore g = Digraph(context.target.id, filename="graph.gv") @@ -83,7 +83,7 @@ def view( @cli.command("g", "graph", "Show the dependency graph") def graphCmd(args: cli.Args): - project.chdir() + model.Project.chdir() targetSpec = str(args.consumeOpt("target", "host-" + shell.uname().machine)) diff --git a/cutekit/jexpr.py b/cutekit/jexpr.py index 4f5338c..91b727f 100644 --- a/cutekit/jexpr.py +++ b/cutekit/jexpr.py @@ -1,9 +1,8 @@ import os -from typing import Any, cast, Callable, Final import json -import cutekit.shell as shell -from cutekit.compat import ensureSupportedManifest +from typing import Any, cast, Callable, Final +from . import shell, compat Json = Any Builtin = Callable[..., Json] @@ -61,5 +60,5 @@ def read(path: str) -> Json: def evalRead(path: str, compatibilityCheck: bool = True) -> Json: data = read(path) if compatibilityCheck: - ensureSupportedManifest(data, path) + compat.ensureSupportedManifest(data, path) return eval(data, path) diff --git a/cutekit/mixins.py b/cutekit/mixins.py index 499b9d2..c275525 100644 --- a/cutekit/mixins.py +++ b/cutekit/mixins.py @@ -1,25 +1,26 @@ 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 -def prefixToolCmd(tools: Tools, toolSpec: str, prefix: str): +def prefixToolCmd(tools: model.Tools, toolSpec: str, prefix: str): 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, "cxx", "ccache") return tools 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, "cxx", [f"-fsanitize={san}"]) patchToolArgs(tools, "ld", [f"-fsanitize={san}"]) @@ -30,7 +31,7 @@ def makeMixinSan(san: 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, "cxx", [f"-O{level}"]) @@ -39,7 +40,7 @@ def makeMixinOptimize(level: str) -> Mixin: 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, "cxx", ["-g", "-gdwarf-4"]) @@ -47,7 +48,7 @@ def mixinDebug(target: Target, tools: Tools) -> Tools: 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, "cxx", [f"-mtune={tune}"]) diff --git a/cutekit/model.py b/cutekit/model.py index 4db70ce..3bf1e45 100644 --- a/cutekit/model.py +++ b/cutekit/model.py @@ -1,9 +1,10 @@ import os -from enum import Enum -from typing import Any 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__) @@ -25,7 +26,11 @@ class Manifest: path: str = "" 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 "id" not in json: @@ -45,7 +50,7 @@ class Manifest: for key in kwargs: 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} def __str__(self): @@ -62,7 +67,7 @@ class Extern: git: 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 "git" not in json and strict: raise RuntimeError("Missing git") @@ -79,7 +84,7 @@ class Extern: for key in kwargs: setattr(self, key, kwargs[key]) - def toJson(self) -> Json: + def toJson(self) -> jexpr.Json: return {"git": self.git, "tag": self.tag} def __str__(self): @@ -94,7 +99,11 @@ class Project(Manifest): extern: dict[str, Extern] = {} 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 "description" not in json and strict: @@ -108,7 +117,7 @@ class Project(Manifest): super().__init__(json, path, strict, **kwargs) - def toJson(self) -> Json: + def toJson(self) -> jexpr.Json: return { **super().toJson(), "description": self.description, @@ -121,13 +130,31 @@ class Project(Manifest): def __repr__(self): 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: cmd: str = "" args: 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 "cmd" not in json and strict: raise RuntimeError("Missing cmd") @@ -146,7 +173,7 @@ class Tool: for key in kwargs: setattr(self, key, kwargs[key]) - def toJson(self) -> Json: + def toJson(self) -> jexpr.Json: return {"cmd": self.cmd, "args": self.args, "files": self.files} def __str__(self): @@ -165,7 +192,11 @@ class Target(Manifest): routing: dict[str, str] 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 "props" not in json and strict: @@ -182,7 +213,7 @@ class Target(Manifest): super().__init__(json, path, strict, **kwargs) - def toJson(self) -> Json: + def toJson(self) -> jexpr.Json: return { **super().toJson(), "props": self.props, @@ -229,7 +260,11 @@ class Component(Manifest): subdirs: list[str] = [] 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: self.decription = json.get("description", self.decription) @@ -249,7 +284,7 @@ class Component(Manifest): super().__init__(json, path, strict, **kwargs) - def toJson(self) -> Json: + def toJson(self) -> jexpr.Json: return { **super().toJson(), "description": self.decription, diff --git a/cutekit/ninja.py b/cutekit/ninja.py index a24698c..9049b5c 100644 --- a/cutekit/ninja.py +++ b/cutekit/ninja.py @@ -23,13 +23,13 @@ use Python. """ 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: - return word.replace('$ ', '$$ ').replace(' ', '$ ').replace(':', '$:') + return word.replace("$ ", "$$ ").replace(" ", "$ ").replace(":", "$:") VarValue = Union[int, str, list[str], None] @@ -42,92 +42,98 @@ class Writer(object): self.width = width def newline(self) -> None: - self.output.write('\n') + self.output.write("\n") def comment(self, text: str) -> None: - for line in textwrap.wrap(text, self.width - 2, break_long_words=False, - break_on_hyphens=False): - self.output.write('# ' + line + '\n') + for line in textwrap.wrap( + text, self.width - 2, break_long_words=False, break_on_hyphens=False + ): + self.output.write("# " + line + "\n") - def separator(self, text : str) -> None: - self.output.write(f"# --- {text} ---" + '-' * - (self.width - 10 - len(text)) + " #\n\n") + def separator(self, text: str) -> None: + self.output.write( + f"# --- {text} ---" + "-" * (self.width - 10 - len(text)) + " #\n\n" + ) def variable(self, key: str, value: VarValue, indent: int = 0) -> None: if value is None: return if isinstance(value, list): - value = ' '.join(filter(None, value)) # Filter out empty strings. - self._line('%s = %s' % (key, value), indent) + value = " ".join(filter(None, value)) # Filter out empty strings. + self._line("%s = %s" % (key, value), indent) def pool(self, name: str, depth: int) -> None: - self._line('pool %s' % name) - self.variable('depth', depth, indent=1) + self._line("pool %s" % name) + self.variable("depth", depth, indent=1) - def rule(self, - name: str, - command: VarValue, - description: Union[str, None] = None, - depfile: VarValue = None, - generator: VarValue = False, - pool: VarValue = None, - restat: bool = False, - rspfile: VarValue = None, - rspfile_content: VarValue = None, - deps: VarValue = None) -> None: - self._line('rule %s' % name) - self.variable('command', command, indent=1) + def rule( + self, + name: str, + command: VarValue, + description: Union[str, None] = None, + depfile: VarValue = None, + generator: VarValue = False, + pool: VarValue = None, + restat: bool = False, + rspfile: VarValue = None, + rspfile_content: VarValue = None, + deps: VarValue = None, + ) -> None: + self._line("rule %s" % name) + self.variable("command", command, indent=1) if description: - self.variable('description', description, indent=1) + self.variable("description", description, indent=1) if depfile: - self.variable('depfile', depfile, indent=1) + self.variable("depfile", depfile, indent=1) if generator: - self.variable('generator', '1', indent=1) + self.variable("generator", "1", indent=1) if pool: - self.variable('pool', pool, indent=1) + self.variable("pool", pool, indent=1) if restat: - self.variable('restat', '1', indent=1) + self.variable("restat", "1", indent=1) if rspfile: - self.variable('rspfile', rspfile, indent=1) + self.variable("rspfile", rspfile, indent=1) if rspfile_content: - self.variable('rspfile_content', rspfile_content, indent=1) + self.variable("rspfile_content", rspfile_content, indent=1) if deps: - self.variable('deps', deps, indent=1) + self.variable("deps", deps, indent=1) - def build(self, - outputs: Union[str, list[str]], - rule: str, - inputs: Union[VarPath, None], - implicit: VarPath = None, - order_only: VarPath = None, - variables: Union[dict[str, str], None] = None, - implicit_outputs: VarPath = None, - pool: Union[str, None] = None, - dyndep: Union[str, None] = None) -> list[str]: - outputs = asList(outputs) + def build( + self, + outputs: Union[str, list[str]], + rule: str, + inputs: Union[VarPath, None], + implicit: VarPath = None, + order_only: VarPath = None, + variables: Union[dict[str, str], None] = None, + implicit_outputs: VarPath = None, + pool: Union[str, None] = None, + dyndep: Union[str, None] = None, + ) -> list[str]: + outputs = utils.asList(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: - implicit = [escapePath(x) for x in asList(implicit)] - all_inputs.append('|') + implicit = [escapePath(x) for x in utils.asList(implicit)] + all_inputs.append("|") all_inputs.extend(implicit) if order_only: - order_only = [escapePath(x) for x in asList(order_only)] - all_inputs.append('||') + order_only = [escapePath(x) for x in utils.asList(order_only)] + all_inputs.append("||") all_inputs.extend(order_only) if implicit_outputs: - implicit_outputs = [escapePath(x) - for x in asList(implicit_outputs)] - out_outputs.append('|') + implicit_outputs = [escapePath(x) for x in utils.asList(implicit_outputs)] + out_outputs.append("|") out_outputs.extend(implicit_outputs) - self._line('build %s: %s' % (' '.join(out_outputs), - ' '.join([rule] + all_inputs))) + self._line( + "build %s: %s" % (" ".join(out_outputs), " ".join([rule] + all_inputs)) + ) if pool is not None: - self._line(' pool = %s' % pool) + self._line(" pool = %s" % pool) if dyndep is not None: - self._line(' dyndep = %s' % dyndep) + self._line(" dyndep = %s" % dyndep) if variables: iterator = iter(variables.items()) @@ -138,58 +144,59 @@ class Writer(object): return outputs def include(self, path: str) -> None: - self._line('include %s' % path) + self._line("include %s" % path) def subninja(self, path: str) -> None: - self._line('subninja %s' % path) + self._line("subninja %s" % path) 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: """Returns the number of '$' characters right in front of s[i].""" dollar_count = 0 dollar_index = i - 1 - while dollar_index > 0 and s[dollar_index] == '$': + while dollar_index > 0 and s[dollar_index] == "$": dollar_count += 1 dollar_index -= 1 return dollar_count def _line(self, text: str, indent: int = 0) -> None: """Write 'text' word-wrapped at self.width characters.""" - leading_space = ' ' * indent + leading_space = " " * indent while len(leading_space) + len(text) > self.width: # The text is too wide; wrap if possible. # Find the rightmost space that would obey our width constraint and # 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 while True: - space = text.rfind(' ', 0, space) - if (space < 0 or - self._count_dollars_before_index(text, space) % 2 == 0): + space = text.rfind(" ", 0, space) + if space < 0 or self._count_dollars_before_index(text, space) % 2 == 0: break if space < 0: # No such space; just use the first unescaped space we can find. space = available_space - 1 while True: - space = text.find(' ', space + 1) - if (space < 0 or - self._count_dollars_before_index(text, space) % 2 == 0): + space = text.find(" ", space + 1) + if ( + space < 0 + or self._count_dollars_before_index(text, space) % 2 == 0 + ): break if space < 0: # Give up on breaking. break - self.output.write(leading_space + text[0:space] + ' $\n') - text = text[space+1:] + self.output.write(leading_space + text[0:space] + " $\n") + text = text[space + 1 :] # 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: self.output.close() @@ -198,6 +205,6 @@ class Writer(object): def escape(string: str) -> str: """Escape a string such that it can be embedded into a Ninja file without 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: '$'. - return string.replace('$', '$$') + return string.replace("$", "$$") diff --git a/cutekit/plugins.py b/cutekit/plugins.py index 5bcfd17..18ac72d 100644 --- a/cutekit/plugins.py +++ b/cutekit/plugins.py @@ -1,7 +1,7 @@ import os import logging -from cutekit import shell, project, const, context +from . import shell, model, const, context import importlib.util as importlib @@ -23,7 +23,7 @@ def load(path: str): def loadAll(): _logger.info("Loading plugins...") - projectRoot = project.root() + projectRoot = model.Project.root() if projectRoot is None: _logger.info("Not in project, skipping plugin loading") diff --git a/cutekit/project.py b/cutekit/project.py deleted file mode 100644 index fcba449..0000000 --- a/cutekit/project.py +++ /dev/null @@ -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) diff --git a/cutekit/shell.py b/cutekit/shell.py index a76aa40..996cbbd 100644 --- a/cutekit/shell.py +++ b/cutekit/shell.py @@ -13,7 +13,7 @@ import tempfile from typing import Optional -from cutekit import const +from . import const _logger = logging.getLogger(__name__) diff --git a/doc/extends.md b/doc/extends.md index dcea14a..692f605 100644 --- a/doc/extends.md +++ b/doc/extends.md @@ -1,5 +1,4 @@ - -# Extending cutekit +# Extending cutekit 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: ```python -import os -import json -import magic -import logging -from pathlib import Path +from cutekit import cli - -from cutekit import shell, builder, const, project -from cutekit.cmds import Cmd, append -from cutekit.args import Args -from typing import Callable - - -def bootCmd(args: Args) -> None: - project.chdir() +@cli.command("h", "hello", "Print hello world") +def bootCmd(args: cli.Args) -> None: 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.