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 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)

View file

@ -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

View file

@ -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)

View file

@ -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
)

View file

@ -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))

View file

@ -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)

View file

@ -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}"])

View file

@ -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,

View file

@ -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,29 +42,32 @@ 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,
def rule(
self,
name: str,
command: VarValue,
description: Union[str, None] = None,
@ -74,27 +77,29 @@ class Writer(object):
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)
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,
def build(
self,
outputs: Union[str, list[str]],
rule: str,
inputs: Union[VarPath, None],
@ -103,31 +108,32 @@ class Writer(object):
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)
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("$", "$$")

View file

@ -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")

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 cutekit import const
from . import const
_logger = logging.getLogger(__name__)

View file

@ -1,4 +1,3 @@
# 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.