From aae3ccd06e05451b243ab3a38923d51341844890 Mon Sep 17 00:00:00 2001 From: VAN BOSSUYT Nicolas Date: Mon, 12 Jun 2023 19:09:17 +0200 Subject: [PATCH] wip --- cutekit/__init__.py | 10 +- cutekit/args2.py | 651 ++++++++++++++++++++++++++++++++++++++++++++ cutekit/builder.py | 21 +- cutekit/cmds.py | 473 ++++++++++++++++++++------------ cutekit/context.py | 4 +- 5 files changed, 973 insertions(+), 186 deletions(-) create mode 100644 cutekit/args2.py diff --git a/cutekit/__init__.py b/cutekit/__init__.py index 124251a..1df2e47 100644 --- a/cutekit/__init__.py +++ b/cutekit/__init__.py @@ -3,7 +3,7 @@ import os import logging from cutekit import const, project, vt100, plugins, cmds -from cutekit.args import parse + def setupLogger(verbose: bool): if verbose: @@ -31,12 +31,12 @@ def setupLogger(verbose: bool): datefmt="%Y-%m-%d %H:%M:%S", ) + def main() -> int: try: - a = parse(sys.argv[1:]) - setupLogger(a.consumeOpt("verbose", False) == True) + setupLogger(False) plugins.loadAll() - cmds.exec(a) + cmds.exec(sys.argv) print() return 0 except RuntimeError as e: @@ -47,4 +47,4 @@ def main() -> int: return 1 except KeyboardInterrupt: print() - return 1 \ No newline at end of file + return 1 diff --git a/cutekit/args2.py b/cutekit/args2.py new file mode 100644 index 0000000..51d09f5 --- /dev/null +++ b/cutekit/args2.py @@ -0,0 +1,651 @@ +from typing import TypeVar, Generic, Optional, Callable, Any +import sys +from cutekit import vt100 + +T = TypeVar('T') + + +# --- Base ------------------------------------------------------------------- # + +class Scan: + _argv: list[str] + _off: int = 0 + + def __init__(self, argv: list[str]): + self._argv = argv + + def any(self) -> bool: + return self._off < len(self._argv) + + def peek(self) -> str: + return self._argv[self._off] + + def tryPeek(self, default: str = "") -> str: + if self.any(): + return self.peek() + return default + + def isHelp(self) -> bool: + return self.tryPeek() == "-h" or self.tryPeek() == "--help" + + def next(self) -> str: + result = self._argv[self._off] + self._off += 1 + return result + + def consumed(self) -> list[str]: + return self._argv[:self._off] + + +class Emit: + _buf: str = "" + _ident: int = 0 + _newline: int = 0 + + def __init__(self) -> None: + pass + + def ident(self) -> None: + self._ident += 1 + + def unident(self) -> None: + self._ident -= 1 + + def emit(self, text: str) -> None: + for c in text: + if c == "\n": + self._newline += 1 + continue + + if c != "\n" and self._newline: + self.flushNewline() + self._buf += " " * self._ident + + self._buf += c + + def newline(self) -> None: + self._newline += 1 + + def flushNewline(self) -> None: + if self._newline: + self._buf += "\n" * min(2, self._newline) + self._newline = 0 + + def finish(self) -> str: + self._newline = min(1, self._newline) + self.flushNewline() + if self._ident: + raise RuntimeError("Unbalanced ident") + return self._buf + + +class Node: + def eval(self, s: Scan, args: Any) -> bool: + raise NotImplementedError() + + def usage(self, e: Emit) -> None: + raise NotImplementedError() + + def help(self, e: Emit) -> None: + raise NotImplementedError() + + +# --- Quatifiers ------------------------------------------------------------ # + +class AllOf(Node): + parts: list[Node] + + def __init__(self, *parts: Node): + self.parts = list(parts) + + def eval(self, s: Scan, args: Any) -> bool: + if s.isHelp(): + e = Emit() + self.help(e) + print(e.finish()) + raise SystemExit(0) + + for part in self.parts: + part.eval(s, args) + + return True + + def usage(self, e: Emit) -> None: + if len(self.parts) == 1: + self.parts[0].usage(e) + return + + e.emit("(") + first = True + for part in self.parts: + if not first: + e.emit(" ") + first = False + part.usage(e) + e.emit(")") + + def help(self, e: Emit) -> None: + for part in self.parts: + part.help(e) + + +class OneOf(Node): + parts: list[Node] + + def __init__(self, *parts: Node, title: Optional[str] = None): + self.parts = list(parts) + + def eval(self, s: Scan, args: Any) -> bool: + if s.isHelp(): + e = Emit() + self.help(e) + print(e.finish()) + raise SystemExit(0) + + for part in self.parts: + if part.eval(s, args): + return True + + e = Emit() + self.usage(e) + if not s.any(): + raise RuntimeError( + f"Unexpected end of input, expected one of " + e.finish()) + + raise RuntimeError( + f"Unexpected {s.peek()}, expected one of " + e.finish()) + + def usage(self, e: Emit) -> None: + e.emit("(") + first = True + for part in self.parts: + if not first: + e.emit(" | ") + first = False + part.usage(e) + e.emit(")") + + def help(self, e: Emit) -> None: + for part in self.parts: + part.help(e) + + +class ZeroOrMoreOf(Node): + operands: list[Node] + + def __init__(self, *operands: Node): + self.operands = list(operands) + + def eval(self, s: Scan, args: Any) -> bool: + if s.isHelp(): + e = Emit() + self.help(e) + print(e.finish()) + raise SystemExit(0) + + any = True + while s.any() and any: + any = False + for part in self.operands: + if not part.eval(s, args): + break + + return True + + def usage(self, e: Emit) -> None: + if len(self.operands) == 1: + e.emit("[") + self.operands[0].usage(e) + e.emit("...]") + return + + e.emit("[") + first = True + for part in self.operands: + if not first: + e.emit(" | ") + first = False + part.usage(e) + e.emit("]+") + + def help(self, e: Emit) -> None: + for part in self.operands: + part.help(e) + + +class OneOrMoreOf(Node): + operands: list[Node] + + def __init__(self, *operands: Node): + self.operands = list(operands) + + def eval(self, s: Scan, args: Any) -> bool: + if s.isHelp(): + e = Emit() + self.help(e) + print(e.finish()) + raise SystemExit(0) + + any = False + while s.any(): + for operand in self.operands: + if operand.eval(s, args): + any = True + break + + if not any: + break + + if not any: + e = Emit() + self.usage(e) + if not s.any(): + raise RuntimeError( + f"Unexpected end of input, expected one or more {vt100.BOLD + vt100.GREEN}{e.finish()}{vt100.RESET}") + + raise RuntimeError( + f"Unexpected {vt100.BOLD + vt100.BROWN}{s.peek()}{vt100.RESET}, expected one or more {vt100.BOLD + vt100.GREEN}{e.finish()}{vt100.RESET}") + + return True + + def usage(self, e: Emit) -> None: + if len(self.operands) == 1: + e.emit("") + self.operands[0].usage(e) + e.emit("+") + return + + e.emit("[") + first = True + for operand in self.operands: + if not first: + e.emit(" | ") + first = False + e.emit("[") + operand.usage(e) + e.emit("]") + e.emit("]+") + + def help(self, e: Emit) -> None: + for operand in self.operands: + operand.help(e) + + +# --- Keywords, Options and Operands ---------------------------------------- # + +class Keyword(Node): + name: str + shorthand: str + description: str + syntax: Optional[Node] + run: Optional[Callable[[Any], None]] + + def __init__(self, name: str, shorthand: str = "", description: str = "", syntax: Optional[Node] = None, run: Optional[Callable[[Any], None]] = None): + self.name = name + self.shorthand = shorthand + self.description = description + self.syntax = syntax + self.run = run + + def eval(self, s: Scan, args: Any) -> bool: + if s.any() and (s.peek() == self.name or s.peek() == self.shorthand): + s.next() + if self.syntax: + try: + self.syntax.eval(s, args) + except RuntimeError as e: + print(f"{vt100.RED}Error:{vt100.RESET} {e}") + print("Try: --help for more information") + raise SystemExit(1) + + if self.run: + self.run(args) + return True + return False + + def usage(self, e: Emit) -> None: + if self.shorthand: + e.emit(f'({self.shorthand} | ') + + e.emit(self.name) + + if self.shorthand: + e.emit(")") + + if self.syntax: + e.emit(" ") + self.syntax.usage(e) + + def help(self, e: Emit) -> None: + if self.shorthand: + e.emit(f" {vt100.BOLD + vt100.GREEN}{self.shorthand}{vt100.RESET} ") + else: + e.emit(" ") + e.emit(f"{self.name} -") + e.emit(f" {self.description}\n") + + e.ident() + e.newline() + e.emit(f"{vt100.BOLD}Usage:{vt100.RESET}") + e.newline() + self.usage(e) + e.newline() + + if self.syntax: + self.syntax.help(e) + e.unident() + e.newline() + + +class Option(Node, Generic[T]): + name: str + key: str + shorthand: str + description: str + default: Optional[T] + + def __init__(self, name: str, key: Optional[str] = None, shorthand: str = "", description: str = "", default: Optional[T] = None): + self.name = name + self.key = key or name + self.shorthand = shorthand + self.description = description + self.default = default + + def eval(self, s: Scan, args: Any) -> bool: + if not s.any(): + return False + + opt = s.peek() + if s.peek().startswith('--' + self.name) or s.peek().startswith('-' + self.shorthand): + s.next() + if '=' in opt: + value = opt.split('=')[1] + else: + if not s.any(): + raise RuntimeError( + f"Missing value for {vt100.BOLD + vt100.BROWN}{opt}{vt100.RESET}") + value = s.next() + args[self.key] = value + return True + return False + + def usage(self, e: Emit) -> None: + e.emit("(") + if self.shorthand: + e.emit(f'(-{self.shorthand} | ') + + e.emit(f'--{self.name}') + + if self.shorthand: + e.emit(")") + + if self.default: + e.emit(f'=<{self.key}>') + + e.emit(")") + + def help(self, e: Emit) -> None: + if self.shorthand: + e.emit(f"-{self.shorthand}, ") + else: + e.emit(" ") + e.emit(f"--{self.name}") + e.emit(f" {self.description}") + + if self.default: + e.emit(f" (default: {self.default})") + e.emit("\n") + + +class Operand(Node, Generic[T]): + name: str + key: str + description: str + default: Optional[T] + + def __init__(self, name: str, key: Optional[str] = None, description: str = "", default: Optional[T] = None): + self.name = name + self.key = key or name + self.description = description + self.default = default + + def eval(self, s: Scan, args: Any) -> bool: + if s.any() and not s.peek().startswith('-'): + args[self.key] = s.next() + return True + return False + + def usage(self, e: Emit) -> None: + e.emit(f"<{self.key}>") + + def help(self, e: Emit) -> None: + e.emit(f"{vt100.WHITE}<{self.name}>{vt100.RESET}") + e.emit(f" {self.description}\n") + if self.default: + e.emit(f" (default: {self.default})") + + +class Operands(Node, Generic[T]): + name: str + key: str + description: str + default: list[T] + + def __init__(self, name: str, key: Optional[str] = None, description: str = "", default: list[T] = []): + self.name = name + self.key = key or name + self.description = description + self.default = default + + def eval(self, s: Scan, args: Any) -> bool: + if self.key not in args: + args[self.key] = [] + + any = False + while s.any() and not s.peek().startswith('-'): + args[self.key] += [s.next()] + any = True + + if not any and len(args[self.key]) == 0: + args[self.key] += self.default + + return True + + def usage(self, e: Emit) -> None: + e.emit(f"<{self.key}...>") + + def help(self, e: Emit) -> None: + e.emit(f"{vt100.WHITE}<{self.name}>{vt100.RESET}") + e.emit(f" {self.description}\n") + if self.default: + e.emit(f" (default: {self.default})") + + +class Sink(Node): + name: str + key: str + description: str + + def __init__(self, name: str, key: Optional[str] = None, description: str = ""): + self.name = name + self.key = key or name + self.description = description + + def eval(self, s: Scan, args: Any) -> bool: + if self.key not in args: + args[self.key] = [] + while s.any(): + args[self.key].append(s.next()) + return True + + def usage(self, e: Emit) -> None: + e.emit(f"<{self.name}>...") + + def help(self, e: Emit) -> None: + e.emit(f"{vt100.WHITE}<{self.name}...>{vt100.RESET}") + e.emit(f" {self.description}\n") + e.emit("\n") + +# --- Help ------------------------------------------------------------------- # + + +class Title(Node): + title: str + + def __init__(self, title: str): + self.title = title + + def eval(self, s: Scan, args: Any) -> bool: + return False + + def usage(self, e: Emit) -> None: + pass + + def help(self, e: Emit) -> None: + e.emit(f"{vt100.BOLD}{self.title}{vt100.RESET}\n") + + +class Heading(Node): + title: str + + def __init__(self, title: str): + self.title = title + + def eval(self, s: Scan, args: Any) -> bool: + return False + + def usage(self, e: Emit) -> None: + pass + + def help(self, e: Emit) -> None: + e.emit(f"{vt100.BOLD}{self.title}:{vt100.RESET}\n") + + +class Paragraph(Node): + _text: str + + def __init__(self, text: str): + self._text = text + + def eval(self, s: Scan, args: Any) -> bool: + return False + + def usage(self, e: Emit) -> None: + pass + + def help(self, e: Emit) -> None: + e.emit(f"{self._text}\n") + + +class Section(Node): + _inner: Node + + def __init__(self, inner: Node): + self._inner = inner + + def eval(self, s: Scan, args: Any) -> bool: + return self._inner.eval(s, args) + + def usage(self, e: Emit) -> None: + self._inner.usage(e) + + def help(self, e: Emit) -> None: + e.newline() + self._inner.help(e) + e.newline() + + +def Options(*nodes: Node) -> Node: + return Section(ZeroOrMoreOf( + Heading("Options"), + Paragraph("Options can be specified in any order."), + ZeroOrMoreOf( + *nodes, + ), + )) + + +""" +cmds = None + + +def helpCmd(args): + global cmds + if cmds: + e = Emit() + cmds.help(e) + print(e.finish()) + +cmds = Section(OneOf( + Title("CuteKit"), + Paragraph( + "A build system and package manager for low-level software development"), + + Section( + ZeroOrMoreOf( + Heading("Usage"), + Paragraph("ck [args...]") + ) + ), + + Section( + OneOf( + Heading("Commands"), + Keyword( + "help", + shorthand="h", + description="Show help", + syntax=AllOf( + Operand("command", description="Command to show help for"), + ), + run=helpCmd, + ), + Keyword( + "build", + shorthand="b", + description="Build the project", + syntax=AllOf( + Options( + Option[str]( + "target", + shorthand="t", + description="Build target", + default="default", + ), + ), + Operands( + "components", + description="Components to build", + ), + ), + run=lambda args: print("build", args), + ), + Keyword( + "run", + shorthand="r", + description="Run the project", + syntax=AllOf( + Options( + Option[str]( + "target", + shorthand="t", + description="Build target", + default="default", + ), + ), + Operand("component", description="Component to run"), + Sink("args", description="Arguments to pass to the component") + ), + run=lambda args: print("run", args), + ), + ) + ) +)) + +try: + cmds.eval(Scan(sys.argv[1:]), {}) +except RuntimeError as e: + print(f"{vt100.RED}Error:{vt100.RESET} {e}") + print("Try: --help for more information") + raise SystemExit(1) +""" diff --git a/cutekit/builder.py b/cutekit/builder.py index f505ec0..b25f12b 100644 --- a/cutekit/builder.py +++ b/cutekit/builder.py @@ -93,7 +93,7 @@ def gen(out: TextIO, context: Context): writer.default("all") -def build(componentSpec: str, targetSpec: str, props: Props = {}) -> ComponentInstance: +def buildMany(componentSpec: list[str], targetSpec: str, props: Props = {}) -> list[ComponentInstance]: context = contextFor(targetSpec, props) shell.mkdir(context.builddir()) @@ -102,18 +102,21 @@ def build(componentSpec: str, targetSpec: str, props: Props = {}) -> ComponentIn with open(ninjaPath, "w") as f: gen(f, context) - instance = context.componentByName(componentSpec) + instances = map(lambda i: context.componentByName(i), componentSpec) - if instance is None: - raise RuntimeError(f"Component {componentSpec} not found") + for instance in instances: + if not instance.enabled: + raise RuntimeError( + f"Component {componentSpec} is disabled: {instance.disableReason}") - if not instance.enabled: - raise RuntimeError( - f"Component {componentSpec} is disabled: {instance.disableReason}") + shell.exec(f"ninja", "-v", "-f", ninjaPath, + *map(lambda i: i.outfile(), instances)) - shell.exec(f"ninja", "-v", "-f", ninjaPath, instance.outfile()) + return [instance] - return instance + +def build(componentSpec: str, targetSpec: str, props: Props = {}) -> ComponentInstance: + return buildMany([componentSpec], targetSpec, props)[0] class Paths: diff --git a/cutekit/cmds.py b/cutekit/cmds.py index 812c0c8..d5f2a6b 100644 --- a/cutekit/cmds.py +++ b/cutekit/cmds.py @@ -2,212 +2,328 @@ import os import logging import requests import sys +import git -from typing import Callable, cast, Optional, NoReturn +from typing import cast, Optional, Any -from cutekit import context, shell, const, vt100, builder, graph, project -from cutekit.args import Args -from cutekit.jexpr import Json +from cutekit import context, shell, const, vt100, builder, graph, project, args2 from cutekit.model import Extern from cutekit.context import contextFor -Callback = Callable[[Args], None] + +# === Commons ================================================================ # + +TARGET_OPTION = args2.Option[str]( + "target", + shorthand="t", + description="Build target", + default="host-" + shell.uname().machine +) logger = logging.getLogger(__name__) - -class Cmd: - shortName: Optional[str] - longName: str - helpText: str - callback: Callable[[Args], NoReturn] - isPlugin: bool = False - - def __init__(self, shortName: Optional[str], longName: str, helpText: str, callback: Callable[[Args], NoReturn]): - self.shortName = shortName - self.longName = longName - self.helpText = helpText - self.callback = callback +root: Optional[args2.Node] = None +cmds: list[args2.Node] = [] -cmds: list[Cmd] = [] - - -def append(cmd: Cmd): - cmd.isPlugin = True +def append(cmd: args2.Node): + global cmds cmds.append(cmd) - cmds.sort(key=lambda c: c.shortName or c.longName) -def runCmd(args: Args): +def usage(): + print(f"Usage: {const.ARGV0} [args...]") + + +def error(msg: str) -> None: + print(f"{vt100.RED}Error:{vt100.RESET} {msg}\n", file=sys.stderr) + + +def exec(args: list[str]): + global root, cmds + + if root is None: + root = args2.Section(args2.OneOf( + args2.Title("CuteKit"), + args2.Paragraph( + "A build system and package manager for low-level software development"), + + args2.Section( + args2.ZeroOrMoreOf( + args2.Heading("Usage"), + args2.Paragraph("ck [args...]") + ) + ), + + args2.Section( + args2.OneOf( + args2.Heading("Commands"), + *cmds + ) + ) + )) + + try: + root.eval(args2.Scan(args[1:]), {}) + except RuntimeError as e: + print(f"{vt100.RED}Error:{vt100.RESET} {e}") + print("Try: --help for more information") + raise SystemExit(1) + + +# === Commands =============================================================== # + +# --- Help Command ----------------------------------------------------------- # + +def helpCmd(args: Any): + global root + if root: + e = args.Emit() + root.help(e) + print(e.finish()) + + +append( + args2.Keyword( + "help", + shorthand="h", + description="Show help", + run=helpCmd, + ) +) + +# --- Run Command ------------------------------------------------------------ # + + +def runCmd(args: Any): project.chdir() - - targetSpec = cast(str, args.consumeOpt( - "target", "host-" + shell.uname().machine)) - - componentSpec = args.consumeArg() - - if componentSpec is None: - raise RuntimeError("Component not specified") - - component = builder.build(componentSpec, targetSpec) - + component = builder.build(args['component'], args['target']) os.environ["CK_TARGET"] = component.context.target.id os.environ["CK_COMPONENT"] = component.id() os.environ["CK_BUILDDIR"] = component.context.builddir() - - shell.exec(component.outfile(), *args.args) + shell.exec(component.outfile(), *args["args"]) -cmds += [Cmd("r", "run", "Run the target", runCmd)] +append( + args2.Keyword( + "run", + shorthand="r", + description="Run the target", + run=runCmd, + syntax=args2.AllOf( + args2.Options( + TARGET_OPTION + ), + args2.Operand("component", description="Component to run"), + args2.Sink("args", description="Arguments to pass to the component") + ), + ), +) -def testCmd(args: Args): +# --- Test Command ----------------------------------------------------------- # + +def testCmd(args: Any): project.chdir() - - targetSpec = cast(str, args.consumeOpt( - "target", "host-" + shell.uname().machine)) - builder.testAll(targetSpec) + builder.testAll(args['target']) -cmds += [Cmd("t", "test", "Run all test targets", testCmd)] +append( + args2.Keyword( + "test", + shorthand="t", + description="Run all test", + run=testCmd, + syntax=args2.AllOf( + args2.Options( + TARGET_OPTION + ) + ), + ), +) -def debugCmd(args: Args): +# --- Debug Command ---------------------------------------------------------- # + +def debugCmd(args: Any): project.chdir() - - targetSpec = cast(str, args.consumeOpt( - "target", "host-" + shell.uname().machine)) - - componentSpec = args.consumeArg() - - if componentSpec is None: - raise RuntimeError("Component not specified") - - component = builder.build(componentSpec, targetSpec) - + component = builder.build(args['component'], args['target']) os.environ["CK_TARGET"] = component.context.target.id os.environ["CK_COMPONENT"] = component.id() os.environ["CK_BUILDDIR"] = component.context.builddir() - - shell.exec("lldb", "-o", "run", component.outfile(), *args.args) + shell.exec("lldb", "-o", "run", component.outfile(), *args["args"]) -cmds += [Cmd("d", "debug", "Debug the target", debugCmd)] +append( + args2.Keyword( + "debug", + shorthand="d", + description="Run a component in the debugger", + run=debugCmd, + syntax=args2.AllOf( + args2.Options( + TARGET_OPTION + ), + args2.Operand("component", description="Component to debug"), + args2.Sink("args", description="Arguments to pass to the component") + ), + ), +) -def buildCmd(args: Args): +# --- Build Command ---------------------------------------------------------- # + +def buildCmd(args: Any): project.chdir() - - targetSpec = cast(str, args.consumeOpt( - "target", "host-" + shell.uname().machine)) - - componentSpec = args.consumeArg() - - if componentSpec is None: - builder.buildAll(targetSpec) + if len(args['components']) == 0: + builder.buildAll(args['target']) else: - builder.build(componentSpec, targetSpec) + builder.buildMany(args['components'], args['target']) -cmds += [Cmd("b", "build", "Build the target", buildCmd)] +append( + args2.Keyword( + "build", + shorthand="b", + description="Build specified component or all components", + run=buildCmd, + syntax=args2.AllOf( + args2.Options( + TARGET_OPTION + ), + args2.Operands( + "components", description="Components to build", default=[]) + ), + ), +) -def listCmd(args: Args): - project.chdir() +# --- List Command ----------------------------------------------------------- # - components = context.loadAllComponents() - targets = context.loadAllTargets() - - vt100.title("Components") - if len(components) == 0: - print(f" (No components available)") +def listWithTitle(title: str, elements: list[str]): + vt100.title(title + " (" + str(len(elements)) + ")") + if len(elements) == 0: + print(f" (No {title.lower()} available)") else: print(vt100.indent(vt100.wordwrap( - ", ".join(map(lambda m: m.id, components))))) - print() - - vt100.title("Targets") - - if len(targets) == 0: - print(f" (No targets available)") - else: - print(vt100.indent(vt100.wordwrap(", ".join(map(lambda m: m.id, targets))))) - + ", ".join(elements)))) print() -cmds += [Cmd("l", "list", "List the targets", listCmd)] +def listCmd(args: Any): + project.chdir() + components = context.loadAllComponents() + targets = context.loadAllTargets() + listWithTitle("Components", list(map(lambda m: m.id, components))) + listWithTitle("Targets", list(map(lambda m: m.id, targets))) -def cleanCmd(args: Args): +append( + args2.Keyword( + "list", + shorthand="l", + description="List all targets and components", + run=listCmd, + ), +) + +# --- Clean Command ---------------------------------------------------------- # + + +def cleanCmd(args: Any): project.chdir() shell.rmrf(const.BUILD_DIR) -cmds += [Cmd("c", "clean", "Clean the build directory", cleanCmd)] +append( + args2.Keyword( + "clean", + shorthand="c", + description="Clean the build directory", + run=cleanCmd, + ), +) + +# --- Nuke Command ---------------------------------------------------------- # -def nukeCmd(args: Args): +def nukeCmd(args: Any): project.chdir() shell.rmrf(const.PROJECT_CK_DIR) -cmds += [Cmd("n", "nuke", "Clean the build directory and cache", nukeCmd)] +append( + args2.Keyword( + "nuke", + shorthand="n", + description="Clean the build and cache directories", + run=nukeCmd, + ), +) -def helpCmd(args: Args): - usage() - - print() - - vt100.title("Description") - print(f" {const.DESCRIPTION}") - - print() - vt100.title("Commands") - for cmd in cmds: - pluginText = "" - if cmd.isPlugin: - pluginText = f"{vt100.CYAN}(plugin){vt100.RESET}" - - print( - f" {vt100.GREEN}{cmd.shortName or ' '}{vt100.RESET} {cmd.longName} - {cmd.helpText} {pluginText}") - - print() - vt100.title("Logging") - print(f" Logs are stored in:") - print(f" - {const.PROJECT_LOG_FILE}") - print(f" - {const.GLOBAL_LOG_FILE}") +# --- Version Command ---------------------------------------------------------- # -cmds += [Cmd("h", "help", "Show this help message", helpCmd)] - - -def versionCmd(args: Args): +def versionCmd(args: Any): print(f"CuteKit v{const.VERSION_STR}\n") -cmds += [Cmd("v", "version", "Show current version", versionCmd)] +append( + args2.Keyword( + "version", + shorthand="v", + description="Show current version", + run=versionCmd, + ), +) + +# --- Version Command ---------------------------------------------------------- # -def graphCmd(args: Args): +def graphCmd(args: Any): project.chdir() - - targetSpec = cast(str, args.consumeOpt( - "target", "host-" + shell.uname().machine)) - - scope: Optional[str] = cast(Optional[str], args.tryConsumeOpt("scope")) - onlyLibs: bool = args.consumeOpt("only-libs", False) == True - showDisabled: bool = args.consumeOpt("show-disabled", False) == True - - context = contextFor(targetSpec) - - graph.view(context, scope=scope, showExe=not onlyLibs, - showDisabled=showDisabled) + context = contextFor(args["target"]) + graph.view( + context, + scope=args["scope"], + showExe=not args["only-libs"], + showDisabled=args["show-disabled"]) -cmds += [Cmd("g", "graph", "Show dependency graph", graphCmd)] +append( + args2.Keyword( + "graph", + shorthand="g", + description="Show dependency graph", + run=graphCmd, + syntax=args2.AllOf( + args2.Options( + TARGET_OPTION, + args2.Option[str]( + "scope", + shorthand="s", + description="Scope to show (default: all)", + default=None, + ), + args2.Option[bool]( + "only-libs", + shorthand="l", + description="Only show libraries", + default=False, + ), + args2.Option[bool]( + "show-disabled", + shorthand="d", + description="Show disabled components", + default=False, + ) + ) + ), + ), +) + +# --- Install Command ---------------------------------------------------------- # def grabExtern(extern: dict[str, Extern]): @@ -225,22 +341,30 @@ def grabExtern(extern: dict[str, Extern]): grabExtern(context.loadProject(extPath).extern) -def installCmd(args: Args): +def installCmd(args: Any): project.chdir() - - pj = context.loadProject(".") - grabExtern(pj.extern) + p = context.loadProject(".") + grabExtern(p.extern) -cmds += [Cmd("i", "install", "Install all the external packages", installCmd)] +append( + args2.Keyword( + "install", + shorthand="i", + description="Install external dependencies", + run=installCmd, + ), +) + +# --- Init Command ---------------------------------------------------------- # -def initCmd(args: Args): - repo = args.consumeOpt('repo', const.DEFAULT_REPO_TEMPLATES) - list = args.consumeOpt('list') +def initCmd(args: Any): + repo = args['repo'] + list = args['list'] - template = args.consumeArg() - name = args.consumeArg() + template = args['template'] + name = args['name'] logger.info("Fetching registry...") r = requests.get( @@ -261,7 +385,7 @@ def initCmd(args: Args): raise RuntimeError('Failed to fetch registry') print('\n'.join( - f"* {entry['id']} - {entry['description']}" for entry in json.loads(r.text))) + f"* {entry['id']} - {entry['description']}" for entry in registry)) return if not template: @@ -284,26 +408,35 @@ def initCmd(args: Args): f" {vt100.GREEN}cutekit build{vt100.BRIGHT_BLACK} # Build the project{vt100.RESET}") -cmds += [Cmd("I", "init", "Initialize a new project", initCmd)] - - -def usage(): - print(f"Usage: {const.ARGV0} [args...]") - - -def error(msg: str) -> None: - print(f"{vt100.RED}Error:{vt100.RESET} {msg}\n", file=sys.stderr) - - -def exec(args: Args): - cmd = args.consumeArg() - - if cmd is None: - raise RuntimeError("No command specified") - - for c in cmds: - if c.shortName == cmd or c.longName == cmd: - c.callback(args) - return - - raise RuntimeError(f"Unknown command {cmd}") +append( + args2.Keyword( + "init", + shorthand="I", + description="Initialize a new project", + run=installCmd, + syntax=args2.AllOf( + args2.Options( + args2.Option[str]( + "repo", + shorthand="r", + description="Repository to fetch templates from", + default=const.DEFAULT_REPO_TEMPLATES, + ), + args2.Option[bool]( + "list", + shorthand="l", + description="List available templates", + default=False, + ), + ), + args2.Operand[str]( + "template", + description="Template to use", + ), + args2.Operand[str]( + "name", + description="Name of the project", + ) + ), + ) +) diff --git a/cutekit/context.py b/cutekit/context.py index 10e98c2..ce87f00 100644 --- a/cutekit/context.py +++ b/cutekit/context.py @@ -95,10 +95,10 @@ class Context(IContext): self.instances = instances self.tools = tools - def componentByName(self, name: str) -> Optional[ComponentInstance]: + def componentByName(self, name: str) -> ComponentInstance: result = list(filter(lambda x: x.manifest.id == name, self.instances)) if len(result) == 0: - return None + raise RuntimeError(f"Component '{name}' not found") return result[0] def cincls(self) -> list[str]: