This commit is contained in:
Sleepy Monax 2024-01-19 15:01:12 +01:00
parent 6649488b0b
commit c974d319a7
8 changed files with 224 additions and 114 deletions

View file

@ -28,8 +28,14 @@ def ensure(version: tuple[int, int, int]):
) )
def setupLogger(verbose: bool): class LoggerArgs:
if verbose: verbose = cli.Arg[bool]("v", "verbose", "Enable verbose logging", default=False)
class logger:
@staticmethod
def setup(args: LoggerArgs):
if args.verbose:
logging.basicConfig( logging.basicConfig(
level=logging.DEBUG, level=logging.DEBUG,
format=f"{vt100.CYAN}%(asctime)s{vt100.RESET} {vt100.YELLOW}%(levelname)s{vt100.RESET} %(name)s: %(message)s", format=f"{vt100.CYAN}%(asctime)s{vt100.RESET} {vt100.YELLOW}%(levelname)s{vt100.RESET} %(name)s: %(message)s",
@ -52,16 +58,22 @@ def setupLogger(verbose: bool):
) )
def main() -> int: class MainArgs(pods.PodArgs, plugins.PluginArgs, LoggerArgs):
try: pass
shell.mkdir(const.GLOBAL_CK_DIR)
args = cli.parse(sys.argv[1:], cli.CutekitArgs)
setupLogger(args.verbose)
@cli.command(None, "/", const.DESCRIPTION)
def _(args: MainArgs):
shell.mkdir(const.GLOBAL_CK_DIR)
logger.setup(args)
const.setup() const.setup()
plugins.setup(args) plugins.setup(args)
pods.setup(args, sys.argv[1:]) pods.setup(args, sys.argv[1:])
cli.exec(args.cmd, sys.argv[1:])
def main() -> int:
try:
cli.exec(sys.argv)
return 0 return 0
except RuntimeError as e: except RuntimeError as e:

View file

@ -350,25 +350,24 @@ def _(args: Any):
build(scope, component if component is not None else "all")[0] build(scope, component if component is not None else "all")[0]
class RunArgs: class RunArgs(model.RegistryArgs, shell.DebuggerArgs):
debug: cli.Arg[bool] = cli.Arg("d", "debug", "Enable debug mode", default=False) debug = cli.Arg[bool]("d", "debug", "Enable debug mode", default=False)
profile: cli.Arg[bool] = cli.Arg("p", "profile", "Enable profiling", default=False) profile = cli.Arg[bool]("p", "profile", "Enable profiling", default=False)
wait: cli.Arg[bool] = cli.Arg("w", "wait", "Wait for debugger to attach", default=False) component = cli.FreeFormArg(
debugger: cli.Arg[str] = cli.Arg("g", "debugger", "Debugger to use", default="lldb") "component", "Component to run", default="__main__"
mixins: cli.Arg[str] = cli.Arg("m", "mixins", "Mixins to apply", default="") )
componentSpec: cli.FreeFormArg[str] = cli.FreeFormArg("Component to run", default="__main__")
extra: cli.RawArg extra: cli.RawArg
@cli.command("r", "builder/run", "Run a component") @cli.command("r", "builder/run", "Run a component")
def runCmd(args: RunArgs): def runCmd(args: RunArgs):
scope = TargetScope.use(args, {"debug": args.debug}) scope = TargetScope.use(args, {"debug": args.debug})
component = scope.registry.lookup( component = scope.registry.lookup(
args.componentSpec, model.Component, includeProvides=True args.component, model.Component, includeProvides=True
) )
if component is None: if component is None:
raise RuntimeError(f"Component {args.componentSpec} not found") raise RuntimeError(f"Component {args.component} not found")
product = build(scope, component)[0] product = build(scope, component)[0]
@ -393,8 +392,7 @@ def runCmd(args: RunArgs):
def _(args: RunArgs): def _(args: RunArgs):
# This is just a wrapper around the `run` command that try # This is just a wrapper around the `run` command that try
# to run a special hook component named __tests__. # to run a special hook component named __tests__.
args.component = "__tests__"
args.componentSpec = "__tests__"
runCmd(args) runCmd(args)

View file

@ -13,6 +13,7 @@ from typing import (
Union, Union,
Callable, Callable,
Generic, Generic,
cast,
get_origin, get_origin,
get_args, get_args,
) )
@ -40,6 +41,9 @@ class Arg(Generic[utils.T]):
return instance.__dict__.get(self.longName, self.default) return instance.__dict__.get(self.longName, self.default)
def __set__(self, instance, value: utils.T):
instance.__dict__[self.longName] = value
@dt.dataclass @dt.dataclass
class FreeFormArg(Generic[utils.T]): class FreeFormArg(Generic[utils.T]):
@ -53,23 +57,30 @@ class FreeFormArg(Generic[utils.T]):
return instance.__dict__.get(self.longName, self.default) return instance.__dict__.get(self.longName, self.default)
def __set__(self, instance, value: utils.T):
instance.__dict__[self.longName] = value
@dt.dataclass
class RawArg:
longName: str
description: str
def __get__(self, instance, owner) -> list[str]:
if instance is None:
return self # type: ignore
return instance.__dict__.get(self.longName, [])
def __set__(self, instance, value):
instance.__dict__[self.longName] = value
class ParserState(enum.Enum): class ParserState(enum.Enum):
FreeForm = enum.auto() FreeForm = enum.auto()
ShortArg = enum.auto() ShortArg = enum.auto()
RawArg = NewType("RawArg", str)
class CutekitArgs:
cmd: FreeFormArg[str] = FreeFormArg("Command to execute")
verbose: Arg[bool] = Arg("v", "verbose", "Enable verbose logging")
safemode: Arg[bool] = Arg("s", "safe", "Enable safe mode")
pod: Arg[bool] = Arg("p", "enable-pod", "Enable pod", default=False)
podName: Arg[str] = Arg("n", "pod-name", "The name of the pod", default="")
def parse(argv: list[str], argType: type[utils.T]) -> utils.T: def parse(argv: list[str], argType: type[utils.T]) -> utils.T:
def set_value(options: dict[str, Any], name: str, value: Any): def set_value(options: dict[str, Any], name: str, value: Any):
if name is not options: if name is not options:
@ -183,6 +194,7 @@ def parse(argv: list[str], argType: type[utils.T]) -> utils.T:
raise RuntimeError(f"Missing arguments: {', '.join(missing)}") raise RuntimeError(f"Missing arguments: {', '.join(missing)}")
for key, value in options.items(): for key, value in options.items():
# print(getattr(argType, key).type())
field_type = get_args(argType.__annotations__[key])[0] field_type = get_args(argType.__annotations__[key])[0]
setattr(result, key, field_type(value)) setattr(result, key, field_type(value))
@ -210,33 +222,55 @@ class Command:
subcommands: dict[str, "Command"] = dt.field(default_factory=dict) subcommands: dict[str, "Command"] = dt.field(default_factory=dict)
def help(self):
print(f"{self.longName} - {self.helpText}")
print()
self.usage()
def usage(self):
usage = f"Usage: {self.longName}"
if self.argType is not None:
usage += " [args...]"
if len(self.subcommands) > 0:
usage += " <command> [args...]"
print(usage)
commands: dict[str, Command] = {} commands: dict[str, Command] = {}
rootCommand: Optional[Command] = None
def command(shortName: Optional[str], longName: str, helpText: str): def command(shortName: Optional[str], longName: str, helpText: str):
curframe = inspect.currentframe() curframe = inspect.currentframe()
calframe = inspect.getouterframes(curframe, 2) calframe = inspect.getouterframes(curframe, 2)
def wrap(fn: Callback): def wrap(fn: utils.T) -> utils.T:
_logger.debug(f"Registering command {longName}") _logger.debug(f"Registering command {longName}")
if len(fn.__annotations__) == 0: if len(fn.__annotations__) == 0:
argType = None argType = None
else: else:
argType = list(fn.__annotations__.values())[0] argType = list(fn.__annotations__.values())[0]
path = longName.split("/") path = longName.split("/")
command = Command(
shortName,
path[-1] if longName != "/" else "/",
helpText,
Path(calframe[1].filename).parent != Path(__file__).parent,
cast(Callback, fn),
argType,
)
if longName == "/":
global rootCommand
rootCommand = command
else:
parent = commands parent = commands
for p in path[:-1]: for p in path[:-1]:
parent = parent[p].subcommands parent = parent[p].subcommands
parent[path[-1]] = Command( parent[path[-1]] = command
shortName,
path[-1],
helpText,
Path(calframe[1].filename).parent != Path(__file__).parent,
fn,
argType,
)
return fn return fn
@ -327,17 +361,49 @@ def versionCmd():
print(f"CuteKit v{const.VERSION_STR}") print(f"CuteKit v{const.VERSION_STR}")
def exec(cmd: str, args: list[str], cmds: dict[str, Command] = commands): def _exec(args: list[str], cmd: Command):
for c in cmds.values(): # let's slice the arguments for this command and the sub command
if c.shortName == cmd or c.longName == cmd: # [-a -b] [sub-cmd -c -d]
if len(c.subcommands) > 0:
exec(args[0], args[1:], c.subcommands) selfArgs = []
return if len(cmd.subcommands) > 0:
while len(args) > 0 and args[0].startswith("-"):
selfArgs.append(args.pop(0))
else: else:
if c.argType is not None: selfArgs = args
c.callback(parse(args[1:], c.argType)) # type: ignore
else: if "-h" in selfArgs or "--help" in selfArgs:
c.callback() # type: ignore cmd.help()
return return
raise RuntimeError(f"Unknown command {cmd}") if "-u" in selfArgs or "--usage" in selfArgs:
cmd.usage()
return
if cmd.argType is not None:
parsedArgs = parse(selfArgs, cmd.argType)
cmd.callback(parsedArgs) # type: ignore
else:
cmd.callback() # type: ignore
if cmd.subcommands:
if len(args) == 0:
error("Missing subcommand")
cmd.usage()
return
for c in cmd.subcommands.values():
if c.shortName == args[0] or c.longName == args[0]:
_exec(args, c)
return
raise RuntimeError(f"Unknown command {args[0]}")
def exec(args: list[str]):
if not rootCommand:
raise RuntimeError("No root command")
rootCommand.longName = Path(args[0]).name
rootCommand.subcommands = commands
_exec(args[1:], rootCommand)

View file

@ -83,15 +83,20 @@ def view(
g.view(filename=os.path.join(target.builddir, "graph.gv")) g.view(filename=os.path.join(target.builddir, "graph.gv"))
class GraphCmd: class GraphCmd(model.TargetArgs):
mixins: cli.Arg[str] = cli.Arg("m", "mixins", "Mixins to apply", default="") scope = cli.Arg("s", "scope", "Scope to show", default="")
scope: cli.Arg[str] = cli.Arg("s", "scope", "Scope to show", default="") onlyLibs = cli.Arg("l", "only-libs", "Only show libraries", default=False)
onlyLibs: cli.Arg[bool] = cli.Arg("l", "only-libs", "Only show libraries", default=False) showDisabled = cli.Arg(
showDisabled: cli.Arg[bool] = cli.Arg("d", "show-disabled", "Show disabled components", default=False) "d", "show-disabled", "Show disabled components", default=False
)
@cli.command("g", "graph", "Show the dependency graph") @cli.command("g", "graph", "Show the dependency graph")
def _(args: GraphCmd): def _(args: GraphCmd):
registry = model.Registry.use(args) view(
target = model.Target.use(args) model.Registry.use(args),
model.Target.use(args),
view(registry, target, scope=args.scope, showExe=not args.onlyLibs, showDisabled=args.showDisabled) scope=args.scope,
showExe=not args.onlyLibs,
showDisabled=args.showDisabled,
)

View file

@ -190,18 +190,15 @@ def _():
class InitArgs: class InitArgs:
repo: cli.Arg[str] = cli.Arg( repo = cli.Arg(
"r", "r",
"repo", "repo",
"Repository to use for templates", "Repository to use for templates",
default=const.DEFAULT_REPO_TEMPLATES, default=const.DEFAULT_REPO_TEMPLATES,
) )
list: cli.Arg[bool] = cli.Arg( list = cli.Arg("l", "list", "List available templates", default=False)
"l", "list", "List available templates", default=False template = cli.FreeFormArg("template", "Template to use")
) name = cli.FreeFormArg("name", "Name of the project")
template: cli.FreeFormArg[str] = cli.FreeFormArg("Template to use")
name: cli.FreeFormArg[Optional[str]] = cli.FreeFormArg("Name of the project")
@cli.command("I", "model/init", "Initialize a new project") @cli.command("I", "model/init", "Initialize a new project")
@ -274,8 +271,8 @@ DEFAULT_TOOLS: Tools = {
class RegistryArgs: class RegistryArgs:
mixins: cli.Arg[str] = cli.Arg("m", "mixins", "Mixins to apply", default="") mixins = cli.Arg[list[str]]("m", "mixins", "Mixins to apply", default=[])
# props: cli.Arg[dict[str]] = cli.Arg("p", "props", "Properties to set", default="") # props = cli.Arg[dict[str]]("p", "props", "Properties to set", default="")
class TargetArgs(RegistryArgs): class TargetArgs(RegistryArgs):

View file

@ -51,6 +51,10 @@ def loadAll():
load(os.path.join(pluginDir, files)) load(os.path.join(pluginDir, files))
def setup(args: cli.CutekitArgs): class PluginArgs:
safemode = cli.Arg[bool]("s", "safemode", "Skip loading plugins", default=False)
def setup(args: PluginArgs):
if not args.safemode: if not args.safemode:
loadAll() loadAll()

View file

@ -77,7 +77,12 @@ IMAGES: dict[str, Image] = {
} }
def setup(args: cli.CutekitArgs, argv: list[str]): class PodArgs:
pod = cli.Arg[bool]("p", "enable-pod", "Enable pod", default=False)
podName = cli.Arg[str]("n", "pod-name", "The name of the pod", default="")
def setup(args: PodArgs, argv: list[str]):
""" """
Reincarnate cutekit within a docker container, this is Reincarnate cutekit within a docker container, this is
useful for cross-compiling useful for cross-compiling
@ -118,12 +123,15 @@ def tryDecode(data: Optional[bytes], default: str = "") -> str:
return data.decode() return data.decode()
class PodArgs: class PodCreateArgs:
name: cli.Arg[str] = cli.Arg("n", "name", "The name of the pod to use", default=defaultPodName) name: cli.Arg[str] = cli.Arg(
image = cli.Arg("i", "image", "The image to use", default=defaultPodImage) "n", "name", "The name of the pod to use", default="default"
)
image = cli.Arg[str]("i", "image", "The image to use", default=defaultPodImage)
@cli.command("c", "pod/create", "Create a new pod") @cli.command("c", "pod/create", "Create a new pod")
def _(args: PodArgs): def _(args: PodCreateArgs):
""" """
Create a new development pod with cutekit installed and the current Create a new development pod with cutekit installed and the current
project mounted at /project project mounted at /project
@ -177,7 +185,10 @@ def _(args: PodArgs):
class KillPodArgs: class KillPodArgs:
name: cli.Arg[str] = cli.Arg("n", "name", "The name of the pod to kill", default=defaultPodName) name: cli.Arg[str] = cli.Arg(
"n", "name", "The name of the pod to kill", default=defaultPodName
)
@cli.command("k", "pod/kill", "Stop and remove a pod") @cli.command("k", "pod/kill", "Stop and remove a pod")
def _(args: KillPodArgs): def _(args: KillPodArgs):
@ -211,9 +222,10 @@ def _():
class PodExecArgs: class PodExecArgs:
name: cli.Arg[str] = cli.Arg("n", "name", "The name of the pod to use", default=defaultPodName) name = cli.Arg("n", "name", "The name of the pod to use", default=defaultPodName)
cmd: cli.FreeFormArg[str] = cli.FreeFormArg("The command to execute") cmd = cli.FreeFormArg("cmd", "The command to execute", default="/bin/bash")
extra: cli.RawArg args = cli.FreeFormArg("args", "The arguments to pass to the command")
@cli.command("e", "pod/exec", "Execute a command in a pod") @cli.command("e", "pod/exec", "Execute a command in a pod")
def podExecCmd(args: PodExecArgs): def podExecCmd(args: PodExecArgs):
@ -223,7 +235,7 @@ def podExecCmd(args: PodExecArgs):
name = args.name name = args.name
try: try:
shell.exec("docker", "exec", "-it", name, args.cmd, *args.extra) shell.exec("docker", "exec", "-it", name, args.cmd, *args.args)
except Exception: except Exception:
raise RuntimeError(f"Pod '{name[len(podPrefix):]}' does not exist") raise RuntimeError(f"Pod '{name[len(podPrefix):]}' does not exist")

View file

@ -395,35 +395,51 @@ def compress(path: str, dest: Optional[str] = None, format: str = "zstd") -> str
@cli.command("s", "scripts", "Manage scripts") @cli.command("s", "scripts", "Manage scripts")
def _(args: cli.Args): def _():
pass pass
class DebugArgs: class DebuggerArgs:
debugger = cli.Arg[str]( debugger = cli.Arg[str](
"d", "debugger", "Debugger to use (lldb, gdb)", default="lldb" "d", "debugger", "Debugger to use (lldb, gdb)", default="lldb"
) )
wait = cli.Arg[bool]("w", "wait", "Wait for debugger to attach") wait = cli.Arg[bool]("w", "wait", "Wait for debugger to attach")
extra = cli.RawArg
class DebugArgs(DebuggerArgs):
cmd = cli.FreeFormArg[str]("cmd", "Command to debug", default="a.out")
args = cli.RawArg("args", "Arguments to pass to the command")
@cli.command("d", "debug", "Debug a program") @cli.command("d", "debug", "Debug a program")
def _(args: DebugArgs): def _(args: DebugArgs):
wait = args.consumeOpt("wait", False) is True command = [args.cmd, *args.args]
debugger = args.consumeOpt("debugger", "lldb") debug(command, args.debugger, args.wait)
command = [str(args.consumeArg()), *args.extra]
debug(command, debugger=debugger, wait=wait)
class ProfilerArgs:
pass
class ProfileArgs:
cmd = cli.FreeFormArg[str]("cmd", "Command to profile", default="a.out")
extra: cli.RawArg
@cli.command("p", "profile", "Profile a program") @cli.command("p", "profile", "Profile a program")
def _(args: cli.Args): def _(args: ProfileArgs):
command = [str(args.consumeArg()), *args.extra] command = [args.cmd, *args.extra]
profile(command) profile(command)
class CompressArgs:
dest = cli.Arg[str]("d", "dest", "Destination file")
format = cli.Arg[str](
"f", "format", "Compression format (zip, zstd, gzip)", default="zstd"
)
path = cli.FreeFormArg[str]("path", "Path to compress")
@cli.command("c", "compress", "Compress a file or directory") @cli.command("c", "compress", "Compress a file or directory")
def _(args: cli.Args): def _(args: CompressArgs):
path = str(args.consumeArg()) compress(args.path, args.dest, args.format)
dest = args.consumeOpt("dest", None)
format = args.consumeOpt("format", "zstd")
compress(path, dest, format)