Compare commits
26 commits
stable
...
dev-argpar
Author | SHA1 | Date | |
---|---|---|---|
Sleepy Monax | aae3ccd06e | ||
Sleepy Monax | 515893de5f | ||
Sleepy Monax | d46d268156 | ||
Sleepy Monax | d9e7be504d | ||
Jordan ⌨️ | c3ead4092c | ||
Jordan ⌨️ | cc8aafb300 | ||
Sleepy Monax | c33e5316bd | ||
Jordan ⌨️ | f3dcfd8bf0 | ||
Jordan ⌨️ | 0396c8165a | ||
Jordan ⌨️ | 0f7a5f6502 | ||
Jordan ⌨️ | edc4119bf7 | ||
Jordan ⌨️ | 2fd545f313 | ||
1c5e369926 | |||
Sleepy Monax | 004a5f4518 | ||
Jordan ⌨️ | f03051df7e | ||
Jordan ⌨️ | 5ef1a586f5 | ||
Sleepy Monax | 10f4a29e89 | ||
Sleepy Monax | 5f6ca201fc | ||
Sleepy Monax | 49bdb4ebad | ||
Sleepy Monax | e6f245d2ad | ||
Sleepy Monax | 175f4e0c3f | ||
Sleepy Monax | 0534c2304a | ||
Sleepy Monax | 745918c003 | ||
Sleepy Monax | 0d4a17fb26 | ||
Sleepy Monax | 782065ec10 | ||
Sleepy Monax | 36108a0fc7 |
|
@ -17,7 +17,6 @@ permissions:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
3
LICENSE
3
LICENSE
|
@ -1,6 +1,7 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2022 skift
|
Copyright (c) 2022, the skiftOS Developers
|
||||||
|
Copyright (c) 2022-2023, Cute Engineering
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
59
README.md
59
README.md
|
@ -1,30 +1,39 @@
|
||||||
# osdk
|
<br/>
|
||||||
|
<br/>
|
||||||
The operating system development kit
|
<br/>
|
||||||
|
<p align="center">
|
||||||
|
<img src="logo.png" width="200" height="200">
|
||||||
|
</p>
|
||||||
|
<h1 align="center">CuteKit</h1>
|
||||||
|
<p align="center">
|
||||||
|
The Cute build system and package manager
|
||||||
|
</p>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
## Table of contents
|
## Table of contents
|
||||||
|
|
||||||
- [osdk](#osdk)
|
- [Table of contents](#table-of-contents)
|
||||||
- [Table of contents](#table-of-contents)
|
- [Macros](#macros)
|
||||||
- [Macros](#macros)
|
- [`@latest`](#latest)
|
||||||
- [`@latest`](#latest)
|
- [`@uname`](#uname)
|
||||||
- [`@uname`](#uname)
|
- [`@include`](#include)
|
||||||
- [`@include`](#include)
|
- [`@join`](#join)
|
||||||
- [`@join`](#join)
|
- [`@concat`](#concat)
|
||||||
- [`@concat`](#concat)
|
- [`@exec`](#exec)
|
||||||
- [`@exec`](#exec)
|
- [Manifest file format](#manifest-file-format)
|
||||||
- [Manifest file format](#manifest-file-format)
|
- [`id`](#id)
|
||||||
- [`id`](#id)
|
- [`type`](#type)
|
||||||
- [`type`](#type)
|
- [`description`](#description)
|
||||||
- [`description`](#description)
|
- [`enabledIf`](#enabledif)
|
||||||
- [`enabledIf`](#enabledif)
|
- [`requires`](#requires)
|
||||||
- [`requires`](#requires)
|
- [`provides`](#provides)
|
||||||
- [`provides`](#provides)
|
- [Target file format](#target-file-format)
|
||||||
- [Target file format](#target-file-format)
|
- [`id`](#id-1)
|
||||||
- [`id`](#id-1)
|
- [`type`](#type-1)
|
||||||
- [`type`](#type-1)
|
- [`props`](#props)
|
||||||
- [`props`](#props)
|
- [`tools`](#tools)
|
||||||
- [`tools`](#tools)
|
|
||||||
|
|
||||||
|
|
||||||
## Macros
|
## Macros
|
||||||
|
@ -197,7 +206,7 @@ Exemple:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Theses values are exposed the translation unit as `__osdk_{prop}__`.
|
Theses values are exposed the translation unit as `__ck_{prop}__`.
|
||||||
|
|
||||||
### `tools`
|
### `tools`
|
||||||
|
|
||||||
|
|
50
cutekit/__init__.py
Normal file
50
cutekit/__init__.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from cutekit import const, project, vt100, plugins, cmds
|
||||||
|
|
||||||
|
|
||||||
|
def setupLogger(verbose: bool):
|
||||||
|
if verbose:
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format=f"{vt100.CYAN}%(asctime)s{vt100.RESET} {vt100.YELLOW}%(levelname)s{vt100.RESET} %(name)s: %(message)s",
|
||||||
|
datefmt="%Y-%m-%d %H:%M:%S",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
projectRoot = project.root()
|
||||||
|
logFile = const.GLOBAL_LOG_FILE
|
||||||
|
if projectRoot is not None:
|
||||||
|
logFile = os.path.join(projectRoot, const.PROJECT_LOG_FILE)
|
||||||
|
|
||||||
|
# create the directory if it doesn't exist
|
||||||
|
logDir = os.path.dirname(logFile)
|
||||||
|
if not os.path.isdir(logDir):
|
||||||
|
os.makedirs(logDir)
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
filename=logFile,
|
||||||
|
filemode="w",
|
||||||
|
format=f"%(asctime)s %(levelname)s %(name)s: %(message)s",
|
||||||
|
datefmt="%Y-%m-%d %H:%M:%S",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
try:
|
||||||
|
setupLogger(False)
|
||||||
|
plugins.loadAll()
|
||||||
|
cmds.exec(sys.argv)
|
||||||
|
print()
|
||||||
|
return 0
|
||||||
|
except RuntimeError as e:
|
||||||
|
logging.exception(e)
|
||||||
|
cmds.error(str(e))
|
||||||
|
cmds.usage()
|
||||||
|
print()
|
||||||
|
return 1
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print()
|
||||||
|
return 1
|
|
@ -1,4 +1,7 @@
|
||||||
Value = str | bool | int
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
|
||||||
|
Value = Union[str, bool, int]
|
||||||
|
|
||||||
|
|
||||||
class Args:
|
class Args:
|
||||||
|
@ -17,23 +20,23 @@ class Args:
|
||||||
del self.opts[key]
|
del self.opts[key]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def consumeOpt(self, key: str, default: Value) -> Value:
|
def consumeOpt(self, key: str, default: Value = False) -> Value:
|
||||||
if key in self.opts:
|
if key in self.opts:
|
||||||
result = self.opts[key]
|
result = self.opts[key]
|
||||||
del self.opts[key]
|
del self.opts[key]
|
||||||
return result
|
return result
|
||||||
return default
|
return default
|
||||||
|
|
||||||
def tryConsumeOpt(self, key: str) -> Value | None:
|
def tryConsumeOpt(self, key: str) -> Optional[Value]:
|
||||||
if key in self.opts:
|
if key in self.opts:
|
||||||
result = self.opts[key]
|
result = self.opts[key]
|
||||||
del self.opts[key]
|
del self.opts[key]
|
||||||
return result
|
return result
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def consumeArg(self) -> str | None:
|
def consumeArg(self, default: Optional[str] = None) -> Optional[str]:
|
||||||
if len(self.args) == 0:
|
if len(self.args) == 0:
|
||||||
return None
|
return default
|
||||||
|
|
||||||
first = self.args[0]
|
first = self.args[0]
|
||||||
del self.args[0]
|
del self.args[0]
|
651
cutekit/args2.py
Normal file
651
cutekit/args2.py
Normal file
|
@ -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 <command> [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)
|
||||||
|
"""
|
|
@ -1,13 +1,13 @@
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
from typing import TextIO
|
from typing import TextIO
|
||||||
|
|
||||||
from osdk.model import ComponentManifest, TargetManifest, Props
|
from cutekit.model import Props
|
||||||
from osdk.ninja import Writer
|
from cutekit.ninja import Writer
|
||||||
from osdk.logger import Logger
|
from cutekit.context import ComponentInstance, Context, contextFor
|
||||||
from osdk.context import Context, contextFor
|
from cutekit import shell, rules
|
||||||
from osdk import shell, rules
|
|
||||||
|
|
||||||
logger = Logger("builder")
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def gen(out: TextIO, context: Context):
|
def gen(out: TextIO, context: Context):
|
||||||
|
@ -28,6 +28,9 @@ def gen(out: TextIO, context: Context):
|
||||||
|
|
||||||
writer.newline()
|
writer.newline()
|
||||||
|
|
||||||
|
writer.rule("cp", "cp $in $out")
|
||||||
|
writer.newline()
|
||||||
|
|
||||||
for i in target.tools:
|
for i in target.tools:
|
||||||
tool = target.tools[i]
|
tool = target.tools[i]
|
||||||
rule = rules.rules[i]
|
rule = rules.rules[i]
|
||||||
|
@ -43,22 +46,26 @@ def gen(out: TextIO, context: Context):
|
||||||
all: list[str] = []
|
all: list[str] = []
|
||||||
|
|
||||||
for instance in context.enabledInstances():
|
for instance in context.enabledInstances():
|
||||||
objects = instance.objsfiles(context)
|
objects = instance.objsfiles()
|
||||||
|
assets = instance.resfiles()
|
||||||
writer.comment(f"Component: {instance.manifest.id}")
|
writer.comment(f"Component: {instance.manifest.id}")
|
||||||
writer.comment(f"Resolved: {', '.join(instance.resolved)}")
|
writer.comment(f"Resolved: {', '.join(instance.resolved)}")
|
||||||
|
|
||||||
for obj in objects:
|
for obj in objects:
|
||||||
r = rules.byFileIn(obj[0])
|
r = rules.byFileIn(obj[0])
|
||||||
if r is None:
|
if r is None:
|
||||||
raise Exception(f"Unknown rule for file {obj[0]}")
|
raise RuntimeError(f"Unknown rule for file {obj[0]}")
|
||||||
t = target.tools[r.id]
|
t = target.tools[r.id]
|
||||||
writer.build(obj[1], r.id, obj[0], order_only=t.files)
|
writer.build(obj[1], r.id, obj[0], order_only=t.files)
|
||||||
|
|
||||||
|
for asset in assets:
|
||||||
|
writer.build(asset[1], "cp", asset[0])
|
||||||
|
|
||||||
writer.newline()
|
writer.newline()
|
||||||
|
|
||||||
if instance.isLib():
|
if instance.isLib():
|
||||||
writer.build(instance.libfile(context), "ar",
|
writer.build(instance.outfile(), "ar",
|
||||||
list(map(lambda o: o[1], objects)))
|
list(map(lambda o: o[1], objects)), implicit=list(map(lambda o: o[1], assets)))
|
||||||
else:
|
else:
|
||||||
libraries: list[str] = []
|
libraries: list[str] = []
|
||||||
|
|
||||||
|
@ -66,17 +73,17 @@ def gen(out: TextIO, context: Context):
|
||||||
reqInstance = context.componentByName(req)
|
reqInstance = context.componentByName(req)
|
||||||
|
|
||||||
if reqInstance is None:
|
if reqInstance is None:
|
||||||
raise Exception(f"Component {req} not found")
|
raise RuntimeError(f"Component {req} not found")
|
||||||
|
|
||||||
if not reqInstance.isLib():
|
if not reqInstance.isLib():
|
||||||
raise Exception(f"Component {req} is not a library")
|
raise RuntimeError(f"Component {req} is not a library")
|
||||||
|
|
||||||
libraries.append(reqInstance.outfile(context))
|
libraries.append(reqInstance.outfile())
|
||||||
|
|
||||||
writer.build(instance.binfile(context), "ld",
|
writer.build(instance.outfile(), "ld", list(
|
||||||
list(map(lambda o: o[1], objects)) + libraries)
|
map(lambda o: o[1], objects)) + libraries, implicit=list(map(lambda o: o[1], assets)))
|
||||||
|
|
||||||
all.append(instance.binfile(context))
|
all.append(instance.outfile())
|
||||||
|
|
||||||
writer.newline()
|
writer.newline()
|
||||||
|
|
||||||
|
@ -86,7 +93,7 @@ def gen(out: TextIO, context: Context):
|
||||||
writer.default("all")
|
writer.default("all")
|
||||||
|
|
||||||
|
|
||||||
def build(componentSpec: str, targetSpec: str, props: Props = {}) -> str:
|
def buildMany(componentSpec: list[str], targetSpec: str, props: Props = {}) -> list[ComponentInstance]:
|
||||||
context = contextFor(targetSpec, props)
|
context = contextFor(targetSpec, props)
|
||||||
|
|
||||||
shell.mkdir(context.builddir())
|
shell.mkdir(context.builddir())
|
||||||
|
@ -95,18 +102,21 @@ def build(componentSpec: str, targetSpec: str, props: Props = {}) -> str:
|
||||||
with open(ninjaPath, "w") as f:
|
with open(ninjaPath, "w") as f:
|
||||||
gen(f, context)
|
gen(f, context)
|
||||||
|
|
||||||
instance = context.componentByName(componentSpec)
|
instances = map(lambda i: context.componentByName(i), componentSpec)
|
||||||
|
|
||||||
if instance is None:
|
for instance in instances:
|
||||||
raise Exception(f"Component {componentSpec} not found")
|
if not instance.enabled:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Component {componentSpec} is disabled: {instance.disableReason}")
|
||||||
|
|
||||||
if not instance.enabled:
|
shell.exec(f"ninja", "-v", "-f", ninjaPath,
|
||||||
raise Exception(
|
*map(lambda i: i.outfile(), instances))
|
||||||
f"Component {componentSpec} is disabled: {instance.disableReason}")
|
|
||||||
|
|
||||||
shell.exec(f"ninja", "-v", "-f", ninjaPath, instance.outfile(context))
|
return [instance]
|
||||||
|
|
||||||
return instance.outfile(context)
|
|
||||||
|
def build(componentSpec: str, targetSpec: str, props: Props = {}) -> ComponentInstance:
|
||||||
|
return buildMany([componentSpec], targetSpec, props)[0]
|
||||||
|
|
||||||
|
|
||||||
class Paths:
|
class Paths:
|
||||||
|
@ -120,9 +130,8 @@ class Paths:
|
||||||
self.obj = obj
|
self.obj = obj
|
||||||
|
|
||||||
|
|
||||||
def buildAll(targetSpec: str) -> Paths:
|
def buildAll(targetSpec: str) -> Context:
|
||||||
context = contextFor(targetSpec)
|
context = contextFor(targetSpec)
|
||||||
target = context.target
|
|
||||||
|
|
||||||
shell.mkdir(context.builddir())
|
shell.mkdir(context.builddir())
|
||||||
ninjaPath = os.path.join(context.builddir(), "build.ninja")
|
ninjaPath = os.path.join(context.builddir(), "build.ninja")
|
||||||
|
@ -132,8 +141,24 @@ def buildAll(targetSpec: str) -> Paths:
|
||||||
|
|
||||||
shell.exec(f"ninja", "-v", "-f", ninjaPath)
|
shell.exec(f"ninja", "-v", "-f", ninjaPath)
|
||||||
|
|
||||||
return Paths(
|
return context
|
||||||
os.path.join(context.builddir(), "bin"),
|
|
||||||
os.path.join(context.builddir(), "lib"),
|
|
||||||
os.path.join(context.builddir(), "obj")
|
def testAll(targetSpec: str):
|
||||||
)
|
context = contextFor(targetSpec)
|
||||||
|
|
||||||
|
shell.mkdir(context.builddir())
|
||||||
|
ninjaPath = os.path.join(context.builddir(), "build.ninja")
|
||||||
|
|
||||||
|
with open(ninjaPath, "w") as f:
|
||||||
|
gen(f, context)
|
||||||
|
|
||||||
|
shell.exec(f"ninja", "-v", "-f", ninjaPath, "all")
|
||||||
|
|
||||||
|
for instance in context.enabledInstances():
|
||||||
|
if instance.isLib():
|
||||||
|
continue
|
||||||
|
|
||||||
|
if instance.id().endswith("-tests"):
|
||||||
|
print(f"Running {instance.id()}")
|
||||||
|
shell.exec(instance.outfile())
|
442
cutekit/cmds.py
Normal file
442
cutekit/cmds.py
Normal file
|
@ -0,0 +1,442 @@
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import sys
|
||||||
|
import git
|
||||||
|
|
||||||
|
from typing import cast, Optional, Any
|
||||||
|
|
||||||
|
from cutekit import context, shell, const, vt100, builder, graph, project, args2
|
||||||
|
from cutekit.model import Extern
|
||||||
|
from cutekit.context import contextFor
|
||||||
|
|
||||||
|
|
||||||
|
# === Commons ================================================================ #
|
||||||
|
|
||||||
|
TARGET_OPTION = args2.Option[str](
|
||||||
|
"target",
|
||||||
|
shorthand="t",
|
||||||
|
description="Build target",
|
||||||
|
default="host-" + shell.uname().machine
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
root: Optional[args2.Node] = None
|
||||||
|
cmds: list[args2.Node] = []
|
||||||
|
|
||||||
|
|
||||||
|
def append(cmd: args2.Node):
|
||||||
|
global cmds
|
||||||
|
cmds.append(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def usage():
|
||||||
|
print(f"Usage: {const.ARGV0} <command> [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 <command> [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()
|
||||||
|
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"])
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Test Command ----------------------------------------------------------- #
|
||||||
|
|
||||||
|
def testCmd(args: Any):
|
||||||
|
project.chdir()
|
||||||
|
builder.testAll(args['target'])
|
||||||
|
|
||||||
|
|
||||||
|
append(
|
||||||
|
args2.Keyword(
|
||||||
|
"test",
|
||||||
|
shorthand="t",
|
||||||
|
description="Run all test",
|
||||||
|
run=testCmd,
|
||||||
|
syntax=args2.AllOf(
|
||||||
|
args2.Options(
|
||||||
|
TARGET_OPTION
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Debug Command ---------------------------------------------------------- #
|
||||||
|
|
||||||
|
def debugCmd(args: Any):
|
||||||
|
project.chdir()
|
||||||
|
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"])
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Build Command ---------------------------------------------------------- #
|
||||||
|
|
||||||
|
def buildCmd(args: Any):
|
||||||
|
project.chdir()
|
||||||
|
if len(args['components']) == 0:
|
||||||
|
builder.buildAll(args['target'])
|
||||||
|
else:
|
||||||
|
builder.buildMany(args['components'], args['target'])
|
||||||
|
|
||||||
|
|
||||||
|
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=[])
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --- List Command ----------------------------------------------------------- #
|
||||||
|
|
||||||
|
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(elements))))
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
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)))
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
append(
|
||||||
|
args2.Keyword(
|
||||||
|
"clean",
|
||||||
|
shorthand="c",
|
||||||
|
description="Clean the build directory",
|
||||||
|
run=cleanCmd,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Nuke Command ---------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
def nukeCmd(args: Any):
|
||||||
|
project.chdir()
|
||||||
|
shell.rmrf(const.PROJECT_CK_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
append(
|
||||||
|
args2.Keyword(
|
||||||
|
"nuke",
|
||||||
|
shorthand="n",
|
||||||
|
description="Clean the build and cache directories",
|
||||||
|
run=nukeCmd,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Version Command ---------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
def versionCmd(args: Any):
|
||||||
|
print(f"CuteKit v{const.VERSION_STR}\n")
|
||||||
|
|
||||||
|
|
||||||
|
append(
|
||||||
|
args2.Keyword(
|
||||||
|
"version",
|
||||||
|
shorthand="v",
|
||||||
|
description="Show current version",
|
||||||
|
run=versionCmd,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Version Command ---------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
def graphCmd(args: Any):
|
||||||
|
project.chdir()
|
||||||
|
context = contextFor(args["target"])
|
||||||
|
graph.view(
|
||||||
|
context,
|
||||||
|
scope=args["scope"],
|
||||||
|
showExe=not args["only-libs"],
|
||||||
|
showDisabled=args["show-disabled"])
|
||||||
|
|
||||||
|
|
||||||
|
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]):
|
||||||
|
for extSpec, ext in extern.items():
|
||||||
|
extPath = os.path.join(const.EXTERN_DIR, extSpec)
|
||||||
|
|
||||||
|
if os.path.exists(extPath):
|
||||||
|
print(f"Skipping {extSpec}, already installed")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"Installing {extSpec}-{ext.tag} from {ext.git}...")
|
||||||
|
git.Repo.clone_from(ext.git, extPath, branch=ext.tag, depth=1)
|
||||||
|
|
||||||
|
if os.path.exists(os.path.join(extPath, "project.json")):
|
||||||
|
grabExtern(context.loadProject(extPath).extern)
|
||||||
|
|
||||||
|
|
||||||
|
def installCmd(args: Any):
|
||||||
|
project.chdir()
|
||||||
|
p = context.loadProject(".")
|
||||||
|
grabExtern(p.extern)
|
||||||
|
|
||||||
|
|
||||||
|
append(
|
||||||
|
args2.Keyword(
|
||||||
|
"install",
|
||||||
|
shorthand="i",
|
||||||
|
description="Install external dependencies",
|
||||||
|
run=installCmd,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Init Command ---------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
def initCmd(args: Any):
|
||||||
|
repo = args['repo']
|
||||||
|
list = args['list']
|
||||||
|
|
||||||
|
template = args['template']
|
||||||
|
name = args['name']
|
||||||
|
|
||||||
|
logger.info("Fetching registry...")
|
||||||
|
r = requests.get(
|
||||||
|
f'https://raw.githubusercontent.com/{repo}/main/registry.json')
|
||||||
|
|
||||||
|
if r.status_code != 200:
|
||||||
|
logger.error('Failed to fetch registry')
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
registry = r.json()
|
||||||
|
|
||||||
|
if list:
|
||||||
|
logger.info("Fetching registry...")
|
||||||
|
r = requests.get(
|
||||||
|
f'https://raw.githubusercontent.com/{repo}/main/registry.json')
|
||||||
|
|
||||||
|
if r.status_code != 200:
|
||||||
|
raise RuntimeError('Failed to fetch registry')
|
||||||
|
|
||||||
|
print('\n'.join(
|
||||||
|
f"* {entry['id']} - {entry['description']}" for entry in registry))
|
||||||
|
return
|
||||||
|
|
||||||
|
if not template:
|
||||||
|
raise RuntimeError('Template not specified')
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
raise RuntimeError('Name not specified')
|
||||||
|
|
||||||
|
if os.path.exists(name):
|
||||||
|
raise RuntimeError(f"Directory {name} already exists")
|
||||||
|
|
||||||
|
print(f"Creating project {name} from template {template}...")
|
||||||
|
shell.cloneDir(f"https://github.com/{repo}", template, name)
|
||||||
|
print(f"Project {name} created\n")
|
||||||
|
|
||||||
|
print("We suggest that you begin by typing:")
|
||||||
|
print(f" {vt100.GREEN}cd {name}{vt100.RESET}")
|
||||||
|
print(f" {vt100.GREEN}cutekit install{vt100.BRIGHT_BLACK} # Install external packages{vt100.RESET}")
|
||||||
|
print(
|
||||||
|
f" {vt100.GREEN}cutekit build{vt100.BRIGHT_BLACK} # Build the project{vt100.RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
33
cutekit/compat.py
Normal file
33
cutekit/compat.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
SUPPORTED_MANIFEST = [
|
||||||
|
"https://schemas.cute.engineering/stable/cutekit.manifest.component.v1",
|
||||||
|
"https://schemas.cute.engineering/stable/cutekit.manifest.project.v1",
|
||||||
|
"https://schemas.cute.engineering/stable/cutekit.manifest.target.v1",
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
OSDK_MANIFEST_NOT_SUPPORTED = "OSDK manifests are not supported by CuteKit. Please use CuteKit manifest instead"
|
||||||
|
|
||||||
|
UNSUPORTED_MANIFEST = {
|
||||||
|
"https://schemas.cute.engineering/stable/osdk.manifest.component.v1": OSDK_MANIFEST_NOT_SUPPORTED,
|
||||||
|
"https://schemas.cute.engineering/stable/osdk.manifest.project.v1": OSDK_MANIFEST_NOT_SUPPORTED,
|
||||||
|
"https://schemas.cute.engineering/stable/osdk.manifest.target.v1": OSDK_MANIFEST_NOT_SUPPORTED,
|
||||||
|
"https://schemas.cute.engineering/latest/osdk.manifest.component": OSDK_MANIFEST_NOT_SUPPORTED,
|
||||||
|
"https://schemas.cute.engineering/latest/osdk.manifest.project": OSDK_MANIFEST_NOT_SUPPORTED,
|
||||||
|
"https://schemas.cute.engineering/latest/osdk.manifest.target": OSDK_MANIFEST_NOT_SUPPORTED,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def ensureSupportedManifest(manifest: Any, path: str):
|
||||||
|
if not "$schema" in manifest:
|
||||||
|
raise RuntimeError(f"Missing $schema in {path}")
|
||||||
|
|
||||||
|
if manifest["$schema"] in UNSUPORTED_MANIFEST:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Unsupported manifest schema {manifest['$schema']} in {path}: {UNSUPORTED_MANIFEST[manifest['$schema']]}")
|
||||||
|
|
||||||
|
if not manifest["$schema"] in SUPPORTED_MANIFEST:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Unsupported manifest schema {manifest['$schema']} in {path}")
|
19
cutekit/const.py
Normal file
19
cutekit/const.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
VERSION = (0, 5, 0, "dev")
|
||||||
|
VERSION_STR = f"{VERSION[0]}.{VERSION[1]}.{VERSION[2]}{'-' + VERSION[3] if VERSION[3] else ''}"
|
||||||
|
MODULE_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
ARGV0 = os.path.basename(sys.argv[0])
|
||||||
|
PROJECT_CK_DIR = ".cutekit"
|
||||||
|
GLOBAL_CK_DIR = os.path.join(os.path.expanduser("~"), ".cutekit")
|
||||||
|
BUILD_DIR = os.path.join(PROJECT_CK_DIR, "build")
|
||||||
|
CACHE_DIR = os.path.join(PROJECT_CK_DIR, "cache")
|
||||||
|
EXTERN_DIR = os.path.join(PROJECT_CK_DIR, "extern")
|
||||||
|
SRC_DIR = "src"
|
||||||
|
META_DIR = f"meta"
|
||||||
|
TARGETS_DIR = os.path.join(META_DIR, "targets")
|
||||||
|
DEFAULT_REPO_TEMPLATES = "cute-engineering/cutekit-templates"
|
||||||
|
DESCRIPTION = "A build system and package manager for low-level software development"
|
||||||
|
PROJECT_LOG_FILE = os.path.join(PROJECT_CK_DIR, "cutekit.log")
|
||||||
|
GLOBAL_LOG_FILE = os.path.join(os.path.expanduser("~"), ".cutekit", "cutekit.log")
|
|
@ -1,17 +1,18 @@
|
||||||
from typing import cast, Protocol, Iterable
|
from typing import cast, Optional, Protocol, Iterable
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from cutekit.model import ProjectManifest, TargetManifest, ComponentManifest, Props, Type, Tool, Tools
|
||||||
|
from cutekit import const, shell, jexpr, utils, rules, mixins, project
|
||||||
|
|
||||||
from osdk.model import ProjectManifest, TargetManifest, ComponentManifest, Props, Type, Tool, Tools
|
logger = logging.getLogger(__name__)
|
||||||
from osdk.logger import Logger
|
|
||||||
from osdk import const, shell, jexpr, utils, rules, mixins
|
|
||||||
|
|
||||||
logger = Logger("context")
|
|
||||||
|
|
||||||
|
|
||||||
class IContext(Protocol):
|
class IContext(Protocol):
|
||||||
|
target: TargetManifest
|
||||||
|
|
||||||
def builddir(self) -> str:
|
def builddir(self) -> str:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
@ -21,7 +22,9 @@ class ComponentInstance:
|
||||||
disableReason = ""
|
disableReason = ""
|
||||||
manifest: ComponentManifest
|
manifest: ComponentManifest
|
||||||
sources: list[str] = []
|
sources: list[str] = []
|
||||||
|
res: list[str] = []
|
||||||
resolved: list[str] = []
|
resolved: list[str] = []
|
||||||
|
context: IContext
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -29,43 +32,54 @@ class ComponentInstance:
|
||||||
disableReason: str,
|
disableReason: str,
|
||||||
manifest: ComponentManifest,
|
manifest: ComponentManifest,
|
||||||
sources: list[str],
|
sources: list[str],
|
||||||
|
res: list[str],
|
||||||
resolved: list[str]):
|
resolved: list[str]):
|
||||||
self.enabled = enabled
|
self.enabled = enabled
|
||||||
self.disableReason = disableReason
|
self.disableReason = disableReason
|
||||||
self.manifest = manifest
|
self.manifest = manifest
|
||||||
self.sources = sources
|
self.sources = sources
|
||||||
|
self.res = res
|
||||||
self.resolved = resolved
|
self.resolved = resolved
|
||||||
|
|
||||||
|
def id(self) -> str:
|
||||||
|
return self.manifest.id
|
||||||
|
|
||||||
def isLib(self):
|
def isLib(self):
|
||||||
return self.manifest.type == Type.LIB
|
return self.manifest.type == Type.LIB
|
||||||
|
|
||||||
def binfile(self, context: IContext) -> str:
|
def objdir(self) -> str:
|
||||||
return os.path.join(context.builddir(), "bin", f"{self.manifest.id}.out")
|
return os.path.join(self.context.builddir(), f"{self.manifest.id}/obj")
|
||||||
|
|
||||||
def objdir(self, context: IContext) -> str:
|
def resdir(self) -> str:
|
||||||
return os.path.join(context.builddir(), "obj", self.manifest.id)
|
return os.path.join(self.context.builddir(), f"{self.manifest.id}/res")
|
||||||
|
|
||||||
def objsfiles(self, context: IContext) -> list[tuple[str, str]]:
|
def objsfiles(self) -> list[tuple[str, str]]:
|
||||||
return list(
|
def toOFile(s: str) -> str:
|
||||||
map(
|
return os.path.join(self.objdir(), s.replace(os.path.join(self.manifest.dirname(), ''), '') + ".o")
|
||||||
lambda s: (
|
return list(map(lambda s: (s, toOFile(s)), self.sources))
|
||||||
s, os.path.join(self.objdir(context), s.replace(os.path.join(self.manifest.dirname(), ''), '') + ".o")),
|
|
||||||
self.sources))
|
|
||||||
|
|
||||||
def libfile(self, context: IContext) -> str:
|
def resfiles(self) -> list[tuple[str, str, str]]:
|
||||||
return os.path.join(context.builddir(), "lib", f"{self.manifest.id}.a")
|
def toAssetFile(s: str) -> str:
|
||||||
|
return os.path.join(self.resdir(), s.replace(os.path.join(self.manifest.dirname(), 'res/'), ''))
|
||||||
|
|
||||||
def outfile(self, context: IContext) -> str:
|
def toAssetId(s: str) -> str:
|
||||||
|
return s.replace(os.path.join(self.manifest.dirname(), 'res/'), '')
|
||||||
|
|
||||||
|
return list(map(lambda s: (s, toAssetFile(s), toAssetId(s)), self.res))
|
||||||
|
|
||||||
|
def outfile(self) -> str:
|
||||||
if self.isLib():
|
if self.isLib():
|
||||||
return self.libfile(context)
|
return os.path.join(self.context.builddir(), self.manifest.id, f"lib/{self.manifest.id}.a")
|
||||||
else:
|
else:
|
||||||
return self.binfile(context)
|
return os.path.join(self.context.builddir(), self.manifest.id, f"bin/{self.manifest.id}.out")
|
||||||
|
|
||||||
def cinclude(self) -> str:
|
def cinclude(self) -> str:
|
||||||
if "cpp-root-include" in self.manifest.props:
|
if "cpp-root-include" in self.manifest.props:
|
||||||
return self.manifest.dirname()
|
return self.manifest.dirname()
|
||||||
else:
|
elif self.manifest.type == Type.LIB:
|
||||||
return str(Path(self.manifest.dirname()).parent)
|
return str(Path(self.manifest.dirname()).parent)
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
class Context(IContext):
|
class Context(IContext):
|
||||||
|
@ -81,31 +95,44 @@ class Context(IContext):
|
||||||
self.instances = instances
|
self.instances = instances
|
||||||
self.tools = tools
|
self.tools = tools
|
||||||
|
|
||||||
def componentByName(self, name: str) -> ComponentInstance | None:
|
def componentByName(self, name: str) -> ComponentInstance:
|
||||||
result = list(filter(lambda x: x.manifest.id == name, self.instances))
|
result = list(filter(lambda x: x.manifest.id == name, self.instances))
|
||||||
if len(result) == 0:
|
if len(result) == 0:
|
||||||
return None
|
raise RuntimeError(f"Component '{name}' not found")
|
||||||
return result[0]
|
return result[0]
|
||||||
|
|
||||||
def cincls(self) -> list[str]:
|
def cincls(self) -> list[str]:
|
||||||
includes = list(
|
includes = list(filter(lambda x: x != "", map(
|
||||||
map(lambda x: x.cinclude(), self.enabledInstances()))
|
lambda x: x.cinclude(), self.enabledInstances())))
|
||||||
return utils.uniq(includes)
|
return utils.uniq(includes)
|
||||||
|
|
||||||
def cdefs(self) -> list[str]:
|
def cdefs(self) -> list[str]:
|
||||||
return self.target.cdefs()
|
return self.target.cdefs()
|
||||||
|
|
||||||
def hashid(self) -> str:
|
def hashid(self) -> str:
|
||||||
return utils.hash((self.target.props, str(self.tools)))[0:8]
|
return utils.hash((self.target.props, [self.tools[t].toJson() for t in self.tools]))[0:8]
|
||||||
|
|
||||||
def builddir(self) -> str:
|
def builddir(self) -> str:
|
||||||
return os.path.join(const.BUILD_DIR, f"{self.target.id}-{self.hashid()[:8]}")
|
return os.path.join(const.BUILD_DIR, f"{self.target.id}-{self.hashid()[:8]}")
|
||||||
|
|
||||||
|
|
||||||
def loadAllTargets() -> list[TargetManifest]:
|
def loadAllTargets() -> list[TargetManifest]:
|
||||||
files = shell.find(const.TARGETS_DIR, ["*.json"])
|
projectRoot = project.root()
|
||||||
return list(
|
if projectRoot is None:
|
||||||
map(lambda path: TargetManifest(jexpr.evalRead(path), path), files))
|
return []
|
||||||
|
|
||||||
|
pj = loadProject(projectRoot)
|
||||||
|
paths = list(
|
||||||
|
map(lambda e: os.path.join(const.EXTERN_DIR,
|
||||||
|
e, const.TARGETS_DIR), pj.extern.keys())
|
||||||
|
) + [const.TARGETS_DIR]
|
||||||
|
|
||||||
|
ret = []
|
||||||
|
for entry in paths:
|
||||||
|
files = shell.find(entry, ["*.json"])
|
||||||
|
ret += list(map(lambda path: TargetManifest(jexpr.evalRead(path), path), files))
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def loadProject(path: str) -> ProjectManifest:
|
def loadProject(path: str) -> ProjectManifest:
|
||||||
|
@ -117,7 +144,7 @@ def loadTarget(id: str) -> TargetManifest:
|
||||||
try:
|
try:
|
||||||
return next(filter(lambda t: t.id == id, loadAllTargets()))
|
return next(filter(lambda t: t.id == id, loadAllTargets()))
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
raise Exception(f"Target '{id}' not found")
|
raise RuntimeError(f"Target '{id}' not found")
|
||||||
|
|
||||||
|
|
||||||
def loadAllComponents() -> list[ComponentManifest]:
|
def loadAllComponents() -> list[ComponentManifest]:
|
||||||
|
@ -135,7 +162,7 @@ def filterDisabled(components: list[ComponentManifest], target: TargetManifest)
|
||||||
list(filter(lambda c: not c.isEnabled(target)[0], components))
|
list(filter(lambda c: not c.isEnabled(target)[0], components))
|
||||||
|
|
||||||
|
|
||||||
def providerFor(what: str, components: list[ComponentManifest]) -> tuple[str | None, str]:
|
def providerFor(what: str, components: list[ComponentManifest]) -> tuple[Optional[str], str]:
|
||||||
result: list[ComponentManifest] = list(
|
result: list[ComponentManifest] = list(
|
||||||
filter(lambda c: c.id == what, components))
|
filter(lambda c: c.id == what, components))
|
||||||
|
|
||||||
|
@ -167,7 +194,7 @@ def resolveDeps(componentSpec: str, components: list[ComponentManifest], target:
|
||||||
return False, unresolvedReason, []
|
return False, unresolvedReason, []
|
||||||
|
|
||||||
if resolved in stack:
|
if resolved in stack:
|
||||||
raise Exception(f"Dependency loop: {stack} -> {resolved}")
|
raise RuntimeError(f"Dependency loop: {stack} -> {resolved}")
|
||||||
|
|
||||||
stack.append(resolved)
|
stack.append(resolved)
|
||||||
|
|
||||||
|
@ -191,30 +218,39 @@ def resolveDeps(componentSpec: str, components: list[ComponentManifest], target:
|
||||||
return enabled, unresolvedReason, resolved
|
return enabled, unresolvedReason, resolved
|
||||||
|
|
||||||
|
|
||||||
def instanciate(componentSpec: str, components: list[ComponentManifest], target: TargetManifest) -> ComponentInstance | None:
|
def instanciate(componentSpec: str, components: list[ComponentManifest], target: TargetManifest) -> Optional[ComponentInstance]:
|
||||||
manifest = next(filter(lambda c: c.id == componentSpec, components))
|
manifest = next(filter(lambda c: c.id == componentSpec, components))
|
||||||
wildcards = set(
|
wildcards = set(
|
||||||
chain(*map(lambda rule: rule.fileIn, rules.rules.values())))
|
chain(*map(lambda rule: rule.fileIn, rules.rules.values())))
|
||||||
sources = shell.find(
|
sources = shell.find(
|
||||||
manifest.subdirs, list(wildcards), recusive=False)
|
manifest.subdirs, list(wildcards), recusive=False)
|
||||||
|
|
||||||
|
res = shell.find(os.path.join(manifest.dirname(), "res"))
|
||||||
|
|
||||||
enabled, unresolvedReason, resolved = resolveDeps(
|
enabled, unresolvedReason, resolved = resolveDeps(
|
||||||
componentSpec, components, target)
|
componentSpec, components, target)
|
||||||
|
|
||||||
return ComponentInstance(enabled, unresolvedReason, manifest, sources, resolved[1:])
|
return ComponentInstance(enabled, unresolvedReason, manifest, sources, res, resolved[1:])
|
||||||
|
|
||||||
|
|
||||||
def instanciateDisabled(component: ComponentManifest, target: TargetManifest) -> ComponentInstance:
|
def instanciateDisabled(component: ComponentManifest, target: TargetManifest) -> ComponentInstance:
|
||||||
return ComponentInstance(False, component.isEnabled(target)[1], component, [], [])
|
return ComponentInstance(
|
||||||
|
enabled=False,
|
||||||
|
disableReason=component.isEnabled(target)[1],
|
||||||
|
manifest=component,
|
||||||
|
sources=[],
|
||||||
|
res=[],
|
||||||
|
resolved=[])
|
||||||
|
|
||||||
|
|
||||||
context: dict = {}
|
context: dict[str, Context] = {}
|
||||||
|
|
||||||
|
|
||||||
def contextFor(targetSpec: str, props: Props = {}) -> Context:
|
def contextFor(targetSpec: str, props: Props = {}) -> Context:
|
||||||
if targetSpec in context:
|
if targetSpec in context:
|
||||||
return context[targetSpec]
|
return context[targetSpec]
|
||||||
|
|
||||||
logger.log(f"Loading context for '{targetSpec}'")
|
logger.info(f"Loading context for '{targetSpec}'")
|
||||||
|
|
||||||
targetEls = targetSpec.split(":")
|
targetEls = targetSpec.split(":")
|
||||||
|
|
||||||
|
@ -261,4 +297,7 @@ def contextFor(targetSpec: str, props: Props = {}) -> Context:
|
||||||
tools,
|
tools,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for instance in instances:
|
||||||
|
instance.context = context[targetSpec]
|
||||||
|
|
||||||
return context[targetSpec]
|
return context[targetSpec]
|
|
@ -1,10 +1,11 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from osdk.context import Context
|
from typing import Optional
|
||||||
from osdk import vt100
|
from cutekit.context import Context
|
||||||
|
from cutekit import vt100
|
||||||
|
|
||||||
|
|
||||||
def view(context: Context, scope: str | None = None, showExe: bool = True, showDisabled: bool = False):
|
def view(context: Context, scope: Optional[str] = None, showExe: bool = True, showDisabled: bool = False):
|
||||||
from graphviz import Digraph
|
from graphviz import Digraph
|
||||||
|
|
||||||
g = Digraph(context.target.id, filename='graph.gv')
|
g = Digraph(context.target.id, filename='graph.gv')
|
56
cutekit/jexpr.py
Normal file
56
cutekit/jexpr.py
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import os
|
||||||
|
from typing import Any, cast, Callable, Final
|
||||||
|
import json
|
||||||
|
|
||||||
|
import cutekit.shell as shell
|
||||||
|
from cutekit.compat import ensureSupportedManifest
|
||||||
|
|
||||||
|
Json = Any
|
||||||
|
Builtin = Callable[..., Json]
|
||||||
|
|
||||||
|
BUILTINS: Final[dict[str, Builtin]] = {
|
||||||
|
"uname": lambda arg, ctx: getattr(shell.uname(), arg).lower(),
|
||||||
|
"include": lambda arg, ctx: evalRead(arg),
|
||||||
|
"evalRead": lambda arg, ctx: evalRead(arg),
|
||||||
|
"join": lambda lhs, rhs, ctx: cast(Json, {**lhs, **rhs} if isinstance(lhs, dict) else lhs + rhs),
|
||||||
|
"concat": lambda *args, ctx: "".join(args),
|
||||||
|
"eval": lambda arg, ctx: eval(arg, ctx["filepath"]),
|
||||||
|
"read": lambda arg, ctx: read(arg),
|
||||||
|
"exec": lambda *args, ctx: shell.popen(*args).splitlines(),
|
||||||
|
"latest": lambda arg, ctx: shell.latest(arg),
|
||||||
|
"abspath": lambda *args, ctx: os.path.normpath(os.path.join(os.path.dirname(ctx["filepath"]), *args))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def eval(jexpr: Json, filePath: str) -> Json:
|
||||||
|
if isinstance(jexpr, dict):
|
||||||
|
result = {}
|
||||||
|
for k in cast(dict[str, Json], jexpr):
|
||||||
|
result[k] = eval(jexpr[k], filePath)
|
||||||
|
return cast(Json, result)
|
||||||
|
elif isinstance(jexpr, list):
|
||||||
|
jexpr = cast(list[Json], jexpr)
|
||||||
|
if len(jexpr) > 0 and isinstance(jexpr[0], str) and jexpr[0].startswith("@"):
|
||||||
|
funcName = jexpr[0][1:]
|
||||||
|
if funcName in BUILTINS:
|
||||||
|
return BUILTINS[funcName](*eval(jexpr[1:], filePath), ctx={"filepath": filePath})
|
||||||
|
|
||||||
|
raise RuntimeError(f"Unknown macro {funcName}")
|
||||||
|
else:
|
||||||
|
return list(map(lambda j: eval(j, filePath), jexpr))
|
||||||
|
else:
|
||||||
|
return jexpr
|
||||||
|
|
||||||
|
|
||||||
|
def read(path: str) -> Json:
|
||||||
|
try:
|
||||||
|
with open(path, "r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except:
|
||||||
|
raise RuntimeError(f"Failed to read {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def evalRead(path: str) -> Json:
|
||||||
|
data = read(path)
|
||||||
|
ensureSupportedManifest(data, path)
|
||||||
|
return eval(data, path)
|
|
@ -1,5 +1,5 @@
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from osdk.model import TargetManifest, Tools
|
from cutekit.model import TargetManifest, Tools
|
||||||
|
|
||||||
Mixin = Callable[[TargetManifest, Tools], Tools]
|
Mixin = Callable[[TargetManifest, Tools], Tools]
|
||||||
|
|
||||||
|
@ -43,12 +43,22 @@ def makeMixinOptimize(level: str) -> Mixin:
|
||||||
|
|
||||||
|
|
||||||
def mixinDebug(target: TargetManifest, tools: Tools) -> Tools:
|
def mixinDebug(target: TargetManifest, tools: Tools) -> Tools:
|
||||||
patchToolArgs(tools, "cc", ["-g"])
|
patchToolArgs(tools, "cc", ["-g", "-gdwarf-4"])
|
||||||
patchToolArgs(tools, "cxx", ["-g"])
|
patchToolArgs(tools, "cxx", ["-g", "-gdwarf-4"])
|
||||||
|
|
||||||
return tools
|
return tools
|
||||||
|
|
||||||
|
|
||||||
|
def makeMixinTune(tune: str) -> Mixin:
|
||||||
|
def mixinTune(target: TargetManifest, tools: Tools) -> Tools:
|
||||||
|
patchToolArgs(tools, "cc", [f"-mtune={tune}"])
|
||||||
|
patchToolArgs(tools, "cxx", [f"-mtune={tune}"])
|
||||||
|
|
||||||
|
return tools
|
||||||
|
|
||||||
|
return mixinTune
|
||||||
|
|
||||||
|
|
||||||
mixins: dict[str, Mixin] = {
|
mixins: dict[str, Mixin] = {
|
||||||
"cache": mixinCache,
|
"cache": mixinCache,
|
||||||
"debug": mixinDebug,
|
"debug": mixinDebug,
|
||||||
|
@ -56,6 +66,7 @@ mixins: dict[str, Mixin] = {
|
||||||
"msan": makeMixinSan("memory"),
|
"msan": makeMixinSan("memory"),
|
||||||
"tsan": makeMixinSan("thread"),
|
"tsan": makeMixinSan("thread"),
|
||||||
"ubsan": makeMixinSan("undefined"),
|
"ubsan": makeMixinSan("undefined"),
|
||||||
|
"tune": makeMixinTune("native"),
|
||||||
"o3": makeMixinOptimize("3"),
|
"o3": makeMixinOptimize("3"),
|
||||||
"o2": makeMixinOptimize("2"),
|
"o2": makeMixinOptimize("2"),
|
||||||
"o1": makeMixinOptimize("1"),
|
"o1": makeMixinOptimize("1"),
|
||||||
|
@ -66,5 +77,6 @@ mixins: dict[str, Mixin] = {
|
||||||
def append(mixinSpec: str, mixin: Mixin):
|
def append(mixinSpec: str, mixin: Mixin):
|
||||||
mixins[mixinSpec] = mixin
|
mixins[mixinSpec] = mixin
|
||||||
|
|
||||||
|
|
||||||
def byId(id: str) -> Mixin:
|
def byId(id: str) -> Mixin:
|
||||||
return mixins[id]
|
return mixins[id]
|
|
@ -1,14 +1,12 @@
|
||||||
import os
|
import os
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from json import JSONEncoder
|
import logging
|
||||||
|
|
||||||
from osdk.jexpr import Json
|
from cutekit.jexpr import Json
|
||||||
from osdk.logger import Logger
|
|
||||||
from osdk import const, utils
|
|
||||||
|
|
||||||
|
|
||||||
logger = Logger("model")
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
Props = dict[str, Any]
|
Props = dict[str, Any]
|
||||||
|
|
||||||
|
@ -26,25 +24,32 @@ class Manifest:
|
||||||
type: Type = Type.UNKNOWN
|
type: Type = Type.UNKNOWN
|
||||||
path: str = ""
|
path: str = ""
|
||||||
|
|
||||||
def __init__(self, json: Json = None, path: str = "", strict=True, **kwargs):
|
def __init__(self, json: Json = None, path: str = "", strict: bool = True, **kwargs: Any):
|
||||||
if json is not None:
|
if json is not None:
|
||||||
if not "id" in json:
|
if not "id" in json:
|
||||||
raise ValueError("Missing id")
|
raise RuntimeError("Missing id")
|
||||||
|
|
||||||
self.id = json["id"]
|
self.id = json["id"]
|
||||||
|
|
||||||
if not "type" in json and strict:
|
if not "type" in json and strict:
|
||||||
raise ValueError("Missing type")
|
raise RuntimeError("Missing type")
|
||||||
|
|
||||||
self.type = Type(json["type"])
|
self.type = Type(json["type"])
|
||||||
|
|
||||||
self.path = path
|
self.path = path
|
||||||
elif strict:
|
elif strict:
|
||||||
raise ValueError("Missing json")
|
raise RuntimeError("Missing json")
|
||||||
|
|
||||||
for key in kwargs:
|
for key in kwargs:
|
||||||
setattr(self, key, kwargs[key])
|
setattr(self, key, kwargs[key])
|
||||||
|
|
||||||
|
def toJson(self) -> Json:
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"type": self.type.value,
|
||||||
|
"path": self.path
|
||||||
|
}
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Manifest(id={self.id}, type={self.type}, path={self.path})"
|
return f"Manifest(id={self.id}, type={self.type}, path={self.path})"
|
||||||
|
|
||||||
|
@ -59,23 +64,29 @@ class Extern:
|
||||||
git: str = ""
|
git: str = ""
|
||||||
tag: str = ""
|
tag: str = ""
|
||||||
|
|
||||||
def __init__(self, json: Json = None, strict=True, **kwargs):
|
def __init__(self, json: Json = None, strict: bool = True, **kwargs: Any):
|
||||||
if json is not None:
|
if json is not None:
|
||||||
if not "git" in json and strict:
|
if not "git" in json and strict:
|
||||||
raise ValueError("Missing git")
|
raise RuntimeError("Missing git")
|
||||||
|
|
||||||
self.git = json["git"]
|
self.git = json["git"]
|
||||||
|
|
||||||
if not "tag" in json and strict:
|
if not "tag" in json and strict:
|
||||||
raise ValueError("Missing tag")
|
raise RuntimeError("Missing tag")
|
||||||
|
|
||||||
self.tag = json["tag"]
|
self.tag = json["tag"]
|
||||||
elif strict:
|
elif strict:
|
||||||
raise ValueError("Missing json")
|
raise RuntimeError("Missing json")
|
||||||
|
|
||||||
for key in kwargs:
|
for key in kwargs:
|
||||||
setattr(self, key, kwargs[key])
|
setattr(self, key, kwargs[key])
|
||||||
|
|
||||||
|
def toJson(self) -> Json:
|
||||||
|
return {
|
||||||
|
"git": self.git,
|
||||||
|
"tag": self.tag
|
||||||
|
}
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Extern(git={self.git}, tag={self.tag})"
|
return f"Extern(git={self.git}, tag={self.tag})"
|
||||||
|
|
||||||
|
@ -87,20 +98,27 @@ class ProjectManifest(Manifest):
|
||||||
description: str = ""
|
description: str = ""
|
||||||
extern: dict[str, Extern] = {}
|
extern: dict[str, Extern] = {}
|
||||||
|
|
||||||
def __init__(self, json: Json = None, path: str = "", strict=True, **kwargs):
|
def __init__(self, json: Json = None, path: str = "", strict: bool = True, **kwargs: Any):
|
||||||
if json is not None:
|
if json is not None:
|
||||||
if not "description" in json and strict:
|
if not "description" in json and strict:
|
||||||
raise ValueError("Missing description")
|
raise RuntimeError("Missing description")
|
||||||
|
|
||||||
self.description = json["description"]
|
self.description = json["description"]
|
||||||
|
|
||||||
self.extern = {k: Extern(v)
|
self.extern = {k: Extern(v)
|
||||||
for k, v in json.get("extern", {}).items()}
|
for k, v in json.get("extern", {}).items()}
|
||||||
elif strict:
|
elif strict:
|
||||||
raise ValueError("Missing json")
|
raise RuntimeError("Missing json")
|
||||||
|
|
||||||
super().__init__(json, path, strict, **kwargs)
|
super().__init__(json, path, strict, **kwargs)
|
||||||
|
|
||||||
|
def toJson(self) -> Json:
|
||||||
|
return {
|
||||||
|
**super().toJson(),
|
||||||
|
"description": self.description,
|
||||||
|
"extern": {k: v.toJson() for k, v in self.extern.items()}
|
||||||
|
}
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"ProjectManifest(id={self.id}, type={self.type}, path={self.path}, description={self.description}, extern={self.extern})"
|
return f"ProjectManifest(id={self.id}, type={self.type}, path={self.path}, description={self.description}, extern={self.extern})"
|
||||||
|
|
||||||
|
@ -113,25 +131,32 @@ class Tool:
|
||||||
args: list[str] = []
|
args: list[str] = []
|
||||||
files: list[str] = []
|
files: list[str] = []
|
||||||
|
|
||||||
def __init__(self, json: Json = None, strict=True, **kwargs):
|
def __init__(self, json: Json = None, strict: bool = True, **kwargs: Any):
|
||||||
if json is not None:
|
if json is not None:
|
||||||
if not "cmd" in json and strict:
|
if not "cmd" in json and strict:
|
||||||
raise ValueError("Missing cmd")
|
raise RuntimeError("Missing cmd")
|
||||||
|
|
||||||
self.cmd = json.get("cmd", self.cmd)
|
self.cmd = json.get("cmd", self.cmd)
|
||||||
|
|
||||||
if not "args" in json and strict:
|
if not "args" in json and strict:
|
||||||
raise ValueError("Missing args")
|
raise RuntimeError("Missing args")
|
||||||
|
|
||||||
self.args = json.get("args", [])
|
self.args = json.get("args", [])
|
||||||
|
|
||||||
self.files = json.get("files", [])
|
self.files = json.get("files", [])
|
||||||
elif strict:
|
elif strict:
|
||||||
raise ValueError("Missing json")
|
raise RuntimeError("Missing json")
|
||||||
|
|
||||||
for key in kwargs:
|
for key in kwargs:
|
||||||
setattr(self, key, kwargs[key])
|
setattr(self, key, kwargs[key])
|
||||||
|
|
||||||
|
def toJson(self) -> Json:
|
||||||
|
return {
|
||||||
|
"cmd": self.cmd,
|
||||||
|
"args": self.args,
|
||||||
|
"files": self.files
|
||||||
|
}
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Tool(cmd={self.cmd}, args={self.args}, files={self.files})"
|
return f"Tool(cmd={self.cmd}, args={self.args}, files={self.files})"
|
||||||
|
|
||||||
|
@ -147,15 +172,15 @@ class TargetManifest(Manifest):
|
||||||
tools: Tools
|
tools: Tools
|
||||||
routing: dict[str, str]
|
routing: dict[str, str]
|
||||||
|
|
||||||
def __init__(self, json: Json = None, path: str = "", strict=True, **kwargs):
|
def __init__(self, json: Json = None, path: str = "", strict: bool = True, **kwargs: Any):
|
||||||
if json is not None:
|
if json is not None:
|
||||||
if not "props" in json and strict:
|
if not "props" in json and strict:
|
||||||
raise ValueError("Missing props")
|
raise RuntimeError("Missing props")
|
||||||
|
|
||||||
self.props = json["props"]
|
self.props = json["props"]
|
||||||
|
|
||||||
if not "tools" in json and strict:
|
if not "tools" in json and strict:
|
||||||
raise ValueError("Missing tools")
|
raise RuntimeError("Missing tools")
|
||||||
|
|
||||||
self.tools = {k: Tool(v) for k, v in json["tools"].items()}
|
self.tools = {k: Tool(v) for k, v in json["tools"].items()}
|
||||||
|
|
||||||
|
@ -163,6 +188,14 @@ class TargetManifest(Manifest):
|
||||||
|
|
||||||
super().__init__(json, path, strict, **kwargs)
|
super().__init__(json, path, strict, **kwargs)
|
||||||
|
|
||||||
|
def toJson(self) -> Json:
|
||||||
|
return {
|
||||||
|
**super().toJson(),
|
||||||
|
"props": self.props,
|
||||||
|
"tools": {k: v.toJson() for k, v in self.tools.items()},
|
||||||
|
"routing": self.routing
|
||||||
|
}
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"TargetManifest({self.id})"
|
return f"TargetManifest({self.id})"
|
||||||
|
|
||||||
|
@ -178,9 +211,9 @@ class TargetManifest(Manifest):
|
||||||
macrovalue = str(prop).lower().replace(" ", "_").replace("-", "_")
|
macrovalue = str(prop).lower().replace(" ", "_").replace("-", "_")
|
||||||
if isinstance(prop, bool):
|
if isinstance(prop, bool):
|
||||||
if prop:
|
if prop:
|
||||||
defines += [f"-D__osdk_{macroname}__"]
|
defines += [f"-D__ck_{macroname}__"]
|
||||||
else:
|
else:
|
||||||
defines += [f"-D__osdk_{macroname}_{macrovalue}__"]
|
defines += [f"-D__ck_{macroname}_{macrovalue}__"]
|
||||||
|
|
||||||
return defines
|
return defines
|
||||||
|
|
||||||
|
@ -194,7 +227,7 @@ class ComponentManifest(Manifest):
|
||||||
provides: list[str] = []
|
provides: list[str] = []
|
||||||
subdirs: list[str] = []
|
subdirs: list[str] = []
|
||||||
|
|
||||||
def __init__(self, json: Json = None, path: str = "", strict=True, **kwargs):
|
def __init__(self, json: Json = None, path: str = "", strict: bool = True, **kwargs: Any):
|
||||||
if json is not None:
|
if json is not None:
|
||||||
self.decription = json.get("description", self.decription)
|
self.decription = json.get("description", self.decription)
|
||||||
self.props = json.get("props", self.props)
|
self.props = json.get("props", self.props)
|
||||||
|
@ -208,19 +241,31 @@ class ComponentManifest(Manifest):
|
||||||
|
|
||||||
super().__init__(json, path, strict, **kwargs)
|
super().__init__(json, path, strict, **kwargs)
|
||||||
|
|
||||||
|
def toJson(self) -> Json:
|
||||||
|
return {
|
||||||
|
**super().toJson(),
|
||||||
|
"description": self.decription,
|
||||||
|
"props": self.props,
|
||||||
|
"tools": {k: v.toJson() for k, v in self.tools.items()},
|
||||||
|
"enableIf": self.enableIf,
|
||||||
|
"requires": self.requires,
|
||||||
|
"provides": self.provides,
|
||||||
|
"subdirs": self.subdirs
|
||||||
|
}
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"ComponentManifest({self.id})"
|
return f"ComponentManifest({self.id})"
|
||||||
|
|
||||||
def isEnabled(self, target: TargetManifest) -> tuple[bool, str]:
|
def isEnabled(self, target: TargetManifest) -> tuple[bool, str]:
|
||||||
for k, v in self.enableIf.items():
|
for k, v in self.enableIf.items():
|
||||||
if not k in target.props:
|
if not k in target.props:
|
||||||
logger.log(
|
logger.info(
|
||||||
f"Component {self.id} disabled by missing {k} in target")
|
f"Component {self.id} disabled by missing {k} in target")
|
||||||
return False, f"Missing props '{k}' in target"
|
return False, f"Missing props '{k}' in target"
|
||||||
|
|
||||||
if not target.props[k] in v:
|
if not target.props[k] in v:
|
||||||
vStrs = [f"'{str(x)}'" for x in v]
|
vStrs = [f"'{str(x)}'" for x in v]
|
||||||
logger.log(
|
logger.info(
|
||||||
f"Component {self.id} disabled by {k}={target.props[k]} not in {v}")
|
f"Component {self.id} disabled by {k}={target.props[k]} not in {v}")
|
||||||
return False, f"Props missmatch for '{k}': Got '{target.props[k]}' but expected {', '.join(vStrs)}"
|
return False, f"Props missmatch for '{k}': Got '{target.props[k]}' but expected {', '.join(vStrs)}"
|
||||||
|
|
|
@ -25,7 +25,7 @@ use Python.
|
||||||
import textwrap
|
import textwrap
|
||||||
from typing import TextIO, Union
|
from typing import TextIO, Union
|
||||||
|
|
||||||
from osdk.utils import asList
|
from cutekit.utils import asList
|
||||||
|
|
||||||
|
|
||||||
def escapePath(word: str) -> str:
|
def escapePath(word: str) -> str:
|
44
cutekit/plugins.py
Normal file
44
cutekit/plugins.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from cutekit import shell, project, const, context
|
||||||
|
|
||||||
|
import importlib.util as importlib
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def load(path: str):
|
||||||
|
logger.info(f"Loading plugin {path}")
|
||||||
|
spec = importlib.spec_from_file_location("plugin", path)
|
||||||
|
|
||||||
|
if not spec or not spec.loader:
|
||||||
|
logger.error(f"Failed to load plugin {path}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
module = importlib.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
|
||||||
|
|
||||||
|
def loadAll():
|
||||||
|
logger.info("Loading plugins...")
|
||||||
|
|
||||||
|
projectRoot = project.root()
|
||||||
|
|
||||||
|
if projectRoot is None:
|
||||||
|
logger.info("Not in project, skipping plugin loading")
|
||||||
|
return
|
||||||
|
|
||||||
|
pj = context.loadProject(projectRoot)
|
||||||
|
paths = list(map(lambda e: os.path.join(const.EXTERN_DIR, e), pj.extern.keys())) + ["."]
|
||||||
|
|
||||||
|
for dirname in paths:
|
||||||
|
pluginDir = os.path.join(projectRoot, dirname, const.META_DIR, "plugins")
|
||||||
|
|
||||||
|
for files in shell.readdir(pluginDir):
|
||||||
|
if files.endswith(".py"):
|
||||||
|
plugin = load(os.path.join(pluginDir, files))
|
||||||
|
|
||||||
|
if plugin:
|
||||||
|
logger.info(f"Loaded plugin {plugin.name}")
|
||||||
|
plugin.init()
|
||||||
|
|
20
cutekit/project.py
Normal file
20
cutekit/project.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
def root() -> Optional[str]:
|
||||||
|
cwd = os.getcwd()
|
||||||
|
while cwd != "/":
|
||||||
|
if os.path.isfile(os.path.join(cwd, "project.json")):
|
||||||
|
return cwd
|
||||||
|
cwd = os.path.dirname(cwd)
|
||||||
|
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)
|
|
@ -1,12 +1,15 @@
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
class Rule:
|
class Rule:
|
||||||
id: str
|
id: str
|
||||||
fileIn: list[str]
|
fileIn: list[str]
|
||||||
fileOut: list[str]
|
fileOut: list[str]
|
||||||
rule: str
|
rule: str
|
||||||
args: list[str]
|
args: list[str]
|
||||||
deps: str | None = None
|
deps: Optional[str] = None
|
||||||
|
|
||||||
def __init__(self, id: str, fileIn: list[str], fileOut: list[str], rule: str, args: list[str] = [], deps: str | None = None):
|
def __init__(self, id: str, fileIn: list[str], fileOut: list[str], rule: str, args: list[str] = [], deps: Optional[str] = None):
|
||||||
self.id = id
|
self.id = id
|
||||||
self.fileIn = fileIn
|
self.fileIn = fileIn
|
||||||
self.fileOut = fileOut
|
self.fileOut = fileOut
|
||||||
|
@ -14,17 +17,18 @@ class Rule:
|
||||||
self.args = args
|
self.args = args
|
||||||
self.deps = deps
|
self.deps = deps
|
||||||
|
|
||||||
|
|
||||||
rules: dict[str, Rule] = {
|
rules: dict[str, Rule] = {
|
||||||
"cc": Rule("cc", ["*.c"], ["*.o"], "-c -o $out $in -MD -MF $out.d $flags $cincs $cdefs", ["-std=gnu2x",
|
"cc": Rule("cc", ["*.c"], ["*.o"], "-c -o $out $in -MD -MF $out.d $flags $cincs $cdefs", ["-std=gnu2x",
|
||||||
"-Wall",
|
"-Wall",
|
||||||
"-Wextra",
|
"-Wextra",
|
||||||
"-Werror"], "$out.d"),
|
"-Werror"], "$out.d"),
|
||||||
"cxx": Rule("cxx", ["*.cpp", "*.cc", "*.cxx"], ["*.o"], "-c -o $out $in -MD -MF $out.d $flags $cincs $cdefs", ["-std=gnu++2b",
|
"cxx": Rule("cxx", ["*.cpp", "*.cc", "*.cxx"], ["*.o"], "-c -o $out $in -MD -MF $out.d $flags $cincs $cdefs", ["-std=gnu++2b",
|
||||||
"-Wall",
|
"-Wall",
|
||||||
"-Wextra",
|
"-Wextra",
|
||||||
"-Werror",
|
"-Werror",
|
||||||
"-fno-exceptions",
|
"-fno-exceptions",
|
||||||
"-fno-rtti"], "$out.d"),
|
"-fno-rtti"], "$out.d"),
|
||||||
"as": Rule("as", ["*.s", "*.asm", "*.S"], ["*.o"], "-o $out $in $flags"),
|
"as": Rule("as", ["*.s", "*.asm", "*.S"], ["*.o"], "-o $out $in $flags"),
|
||||||
"ar": Rule("ar", ["*.o"], ["*.a"], "$flags $out $in"),
|
"ar": Rule("ar", ["*.o"], ["*.a"], "$flags $out $in"),
|
||||||
"ld": Rule("ld", ["*.o", "*.a"], ["*.out"], "-o $out $in $flags"),
|
"ld": Rule("ld", ["*.o", "*.a"], ["*.out"], "-o $out $in $flags"),
|
||||||
|
@ -35,7 +39,7 @@ def append(rule: Rule):
|
||||||
rules[rule.id] = rule
|
rules[rule.id] = rule
|
||||||
|
|
||||||
|
|
||||||
def byFileIn(fileIn: str) -> Rule | None:
|
def byFileIn(fileIn: str) -> Optional[Rule]:
|
||||||
for key in rules:
|
for key in rules:
|
||||||
rule = rules[key]
|
rule = rules[key]
|
||||||
for ext in rule.fileIn:
|
for ext in rule.fileIn:
|
||||||
|
@ -44,5 +48,5 @@ def byFileIn(fileIn: str) -> Rule | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def byId(id: str) -> Rule | None:
|
def byId(id: str) -> Optional[Rule]:
|
||||||
return rules.get(id, None)
|
return rules.get(id, None)
|
|
@ -8,11 +8,14 @@ import re
|
||||||
import shutil
|
import shutil
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import platform
|
import platform
|
||||||
|
import logging
|
||||||
|
import tempfile
|
||||||
|
|
||||||
from osdk.logger import Logger
|
|
||||||
from osdk import const
|
|
||||||
|
|
||||||
logger = Logger("shell")
|
from typing import Optional
|
||||||
|
from cutekit import const
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Uname:
|
class Uname:
|
||||||
|
@ -33,6 +36,8 @@ def uname() -> Uname:
|
||||||
result.machine = "arm64"
|
result.machine = "arm64"
|
||||||
case "AMD64":
|
case "AMD64":
|
||||||
result.machine = "x86_64"
|
result.machine = "x86_64"
|
||||||
|
case _:
|
||||||
|
pass
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -43,7 +48,7 @@ def sha256sum(path: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
def find(path: str | list[str], wildcards: list[str] = [], recusive: bool = True) -> list[str]:
|
def find(path: str | list[str], wildcards: list[str] = [], recusive: bool = True) -> list[str]:
|
||||||
logger.log(f"Looking for files in {path} matching {wildcards}")
|
logger.info(f"Looking for files in {path} matching {wildcards}")
|
||||||
|
|
||||||
result: list[str] = []
|
result: list[str] = []
|
||||||
|
|
||||||
|
@ -79,7 +84,7 @@ def find(path: str | list[str], wildcards: list[str] = [], recusive: bool = True
|
||||||
|
|
||||||
|
|
||||||
def mkdir(path: str) -> str:
|
def mkdir(path: str) -> str:
|
||||||
logger.log(f"Creating directory {path}")
|
logger.info(f"Creating directory {path}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.makedirs(path)
|
os.makedirs(path)
|
||||||
|
@ -90,7 +95,7 @@ def mkdir(path: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
def rmrf(path: str) -> bool:
|
def rmrf(path: str) -> bool:
|
||||||
logger.log(f"Removing directory {path}")
|
logger.info(f"Removing directory {path}")
|
||||||
|
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
return False
|
return False
|
||||||
|
@ -98,17 +103,18 @@ def rmrf(path: str) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def wget(url: str, path: str | None = None) -> str:
|
def wget(url: str, path: Optional[str] = None) -> str:
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
if path is None:
|
if path is None:
|
||||||
path = os.path.join(const.CACHE_DIR,
|
path = os.path.join(
|
||||||
|
const.CACHE_DIR,
|
||||||
hashlib.sha256(url.encode('utf-8')).hexdigest())
|
hashlib.sha256(url.encode('utf-8')).hexdigest())
|
||||||
|
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
return path
|
return path
|
||||||
|
|
||||||
logger.log(f"Downloading {url} to {path}")
|
logger.info(f"Downloading {url} to {path}")
|
||||||
|
|
||||||
r = requests.get(url, stream=True)
|
r = requests.get(url, stream=True)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
@ -121,48 +127,55 @@ def wget(url: str, path: str | None = None) -> str:
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
def exec(*args: str):
|
def exec(*args: str, quiet: bool = False) -> bool:
|
||||||
logger.log(f"Executing {args}")
|
logger.info(f"Executing {args}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
proc = subprocess.run(args)
|
proc = subprocess.run(
|
||||||
|
args, stdout=sys.stdout if not quiet else subprocess.PIPE, stderr=sys.stderr if not quiet else subprocess.PIPE)
|
||||||
|
|
||||||
|
if proc.stdout:
|
||||||
|
logger.info(proc.stdout.decode('utf-8'))
|
||||||
|
|
||||||
|
if proc.stderr:
|
||||||
|
logger.error(proc.stderr.decode('utf-8'))
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise Exception(f"{args[0]}: Command not found")
|
raise RuntimeError(f"{args[0]}: Command not found")
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
raise Exception(f"{args[0]}: Interrupted")
|
raise RuntimeError(f"{args[0]}: Interrupted")
|
||||||
|
|
||||||
if proc.returncode == -signal.SIGSEGV:
|
if proc.returncode == -signal.SIGSEGV:
|
||||||
raise Exception(f"{args[0]}: Segmentation fault")
|
raise RuntimeError(f"{args[0]}: Segmentation fault")
|
||||||
|
|
||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
raise Exception(
|
raise RuntimeError(
|
||||||
f"{args[0]}: Process exited with code {proc.returncode}")
|
f"{args[0]}: Process exited with code {proc.returncode}")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def popen(*args: str) -> str:
|
def popen(*args: str) -> str:
|
||||||
logger.log(f"Executing {args}")
|
logger.info(f"Executing {args}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
proc = subprocess.run(args, stdout=subprocess.PIPE, stderr=sys.stderr)
|
proc = subprocess.run(args, stdout=subprocess.PIPE, stderr=sys.stderr)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise Exception(f"{args[0]}: Command not found")
|
raise RuntimeError(f"{args[0]}: Command not found")
|
||||||
|
|
||||||
if proc.returncode == -signal.SIGSEGV:
|
if proc.returncode == -signal.SIGSEGV:
|
||||||
raise Exception(f"{args[0]}: Segmentation fault")
|
raise RuntimeError(f"{args[0]}: Segmentation fault")
|
||||||
|
|
||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
raise Exception(
|
raise RuntimeError(
|
||||||
f"{args[0]}: Process exited with code {proc.returncode}")
|
f"{args[0]}: Process exited with code {proc.returncode}")
|
||||||
|
|
||||||
return proc.stdout.decode('utf-8')
|
return proc.stdout.decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
def readdir(path: str) -> list[str]:
|
def readdir(path: str) -> list[str]:
|
||||||
logger.log(f"Reading directory {path}")
|
logger.info(f"Reading directory {path}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return os.listdir(path)
|
return os.listdir(path)
|
||||||
|
@ -171,11 +184,36 @@ def readdir(path: str) -> list[str]:
|
||||||
|
|
||||||
|
|
||||||
def cp(src: str, dst: str):
|
def cp(src: str, dst: str):
|
||||||
logger.log(f"Copying {src} to {dst}")
|
logger.info(f"Copying {src} to {dst}")
|
||||||
|
|
||||||
shutil.copy(src, dst)
|
shutil.copy(src, dst)
|
||||||
|
|
||||||
|
|
||||||
|
def mv(src: str, dst: str):
|
||||||
|
logger.info(f"Moving {src} to {dst}")
|
||||||
|
|
||||||
|
shutil.move(src, dst)
|
||||||
|
|
||||||
|
|
||||||
|
def cpTree(src: str, dst: str):
|
||||||
|
logger.info(f"Copying {src} to {dst}")
|
||||||
|
|
||||||
|
shutil.copytree(src, dst, dirs_exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def cloneDir(url: str, path: str, dest: str) -> str:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
mkdir(tmp)
|
||||||
|
exec(*["git", "clone", "-n", "--depth=1",
|
||||||
|
"--filter=tree:0", url, tmp, "-q"], quiet=True)
|
||||||
|
exec(*["git", "-C", tmp, "sparse-checkout",
|
||||||
|
"set", "--no-cone", path, "-q"], quiet=True)
|
||||||
|
exec(*["git", "-C", tmp, "checkout", "-q", "--no-progress"], quiet=True)
|
||||||
|
mv(os.path.join(tmp, path), dest)
|
||||||
|
|
||||||
|
return dest
|
||||||
|
|
||||||
|
|
||||||
LATEST_CACHE: dict[str, str] = {}
|
LATEST_CACHE: dict[str, str] = {}
|
||||||
|
|
||||||
|
|
||||||
|
@ -194,7 +232,7 @@ def latest(cmd: str) -> str:
|
||||||
if cmd in LATEST_CACHE:
|
if cmd in LATEST_CACHE:
|
||||||
return LATEST_CACHE[cmd]
|
return LATEST_CACHE[cmd]
|
||||||
|
|
||||||
logger.log(f"Finding latest version of {cmd}")
|
logger.info(f"Finding latest version of {cmd}")
|
||||||
|
|
||||||
regex = re.compile(r"^" + re.escape(cmd) + r"(-.[0-9]+)?(\.exe)?$")
|
regex = re.compile(r"^" + re.escape(cmd) + r"(-.[0-9]+)?(\.exe)?$")
|
||||||
|
|
||||||
|
@ -206,12 +244,12 @@ def latest(cmd: str) -> str:
|
||||||
versions.append(f)
|
versions.append(f)
|
||||||
|
|
||||||
if len(versions) == 0:
|
if len(versions) == 0:
|
||||||
raise Exception(f"{cmd} not found")
|
raise RuntimeError(f"{cmd} not found")
|
||||||
|
|
||||||
versions.sort()
|
versions.sort()
|
||||||
chosen = versions[-1]
|
chosen = versions[-1]
|
||||||
|
|
||||||
logger.log(f"Chosen {chosen} as latest version of {cmd}")
|
logger.info(f"Chosen {chosen} as latest version of {cmd}")
|
||||||
|
|
||||||
LATEST_CACHE[cmd] = chosen
|
LATEST_CACHE[cmd] = chosen
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
from typing import Any, TypeVar, cast
|
import os
|
||||||
|
from typing import Any, TypeVar, cast, Optional, Union
|
||||||
import json
|
import json
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
from osdk import shell, const
|
|
||||||
|
|
||||||
T = TypeVar('T')
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,7 +15,7 @@ def uniq(l: list[str]) -> list[str]:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def hash(obj: Any, keys: list[str] = [], cls: type[json.JSONEncoder] | None = None) -> str:
|
def hash(obj: Any, keys: list[str] = [], cls: Optional[type[json.JSONEncoder]] = None) -> str:
|
||||||
toHash = {}
|
toHash = {}
|
||||||
if len(keys) == 0:
|
if len(keys) == 0:
|
||||||
toHash = obj
|
toHash = obj
|
||||||
|
@ -52,9 +51,13 @@ def key(obj: Any, keys: list[str] = []) -> str:
|
||||||
return "-".join(k)
|
return "-".join(k)
|
||||||
|
|
||||||
|
|
||||||
def asList(i: T | list[T] | None) -> list[T]:
|
def asList(i: Optional[Union[T, list[T]]]) -> list[T]:
|
||||||
if i is None:
|
if i is None:
|
||||||
return []
|
return []
|
||||||
if isinstance(i, list):
|
if isinstance(i, list):
|
||||||
return cast(list[T], i)
|
return cast(list[T], i)
|
||||||
return [i]
|
return [i]
|
||||||
|
|
||||||
|
|
||||||
|
def isNewer(path1: str, path2: str) -> bool:
|
||||||
|
return not os.path.exists(path2) or os.path.getmtime(path1) > os.path.getmtime(path2)
|
|
@ -1,19 +1,22 @@
|
||||||
BLACK = "\033[0;30m"
|
BLACK = "\033[30m"
|
||||||
RED = "\033[0;31m"
|
RED = "\033[31m"
|
||||||
GREEN = "\033[0;32m"
|
GREEN = "\033[32m"
|
||||||
BROWN = "\033[0;33m"
|
BROWN = "\033[33m"
|
||||||
BLUE = "\033[0;34m"
|
BLUE = "\033[34m"
|
||||||
PURPLE = "\033[0;35m"
|
PURPLE = "\033[35m"
|
||||||
CYAN = "\033[0;36m"
|
CYAN = "\033[36m"
|
||||||
LIGHT_GRAY = "\033[0;37m"
|
WHITE = "\033[37m"
|
||||||
DARK_GRAY = "\033[1;30m"
|
|
||||||
LIGHT_RED = "\033[1;31m"
|
|
||||||
LIGHT_GREEN = "\033[1;32m"
|
BRIGHT_BLACK = "\033[90m"
|
||||||
YELLOW = "\033[1;33m"
|
BRIGHT_RED = "\033[91m"
|
||||||
LIGHT_BLUE = "\033[1;34m"
|
BRIGHT_GREEN = "\033[92m"
|
||||||
LIGHT_PURPLE = "\033[1;35m"
|
BRIGHT_BROWN = "\033[93m"
|
||||||
LIGHT_CYAN = "\033[1;36m"
|
BRIGHT_BLUE = "\033[94m"
|
||||||
LIGHT_WHITE = "\033[1;37m"
|
BRIGHT_PURPLE = "\033[95m"
|
||||||
|
BRIGHT_CYAN = "\033[96m"
|
||||||
|
BRIGHT_WHITE = "\033[97m"
|
||||||
|
|
||||||
BOLD = "\033[1m"
|
BOLD = "\033[1m"
|
||||||
FAINT = "\033[2m"
|
FAINT = "\033[2m"
|
||||||
ITALIC = "\033[3m"
|
ITALIC = "\033[3m"
|
||||||
|
@ -25,10 +28,10 @@ RESET = "\033[0m"
|
||||||
|
|
||||||
|
|
||||||
def title(text: str):
|
def title(text: str):
|
||||||
print(f"{LIGHT_WHITE}{text}{RESET}:")
|
print(f"{BOLD}{text}{RESET}:")
|
||||||
|
|
||||||
|
|
||||||
def wordwrap(text: str, width: int = 60, newline="\n") -> str:
|
def wordwrap(text: str, width: int = 60, newline: str = "\n") -> str:
|
||||||
result = ""
|
result = ""
|
||||||
curr = 0
|
curr = 0
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
import sys
|
|
||||||
|
|
||||||
from os.path import isdir
|
|
||||||
from osdk import const, shell
|
|
||||||
from osdk.args import parse
|
|
||||||
from osdk.cmds import exec, usage
|
|
||||||
from osdk.plugins import loadAll
|
|
||||||
import osdk.vt100 as vt100
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
a = parse(sys.argv[1:])
|
|
||||||
|
|
||||||
if not a.consumeOpt("verbose", False):
|
|
||||||
if not isdir(const.OSDK_DIR):
|
|
||||||
shell.mkdir(const.OSDK_DIR)
|
|
||||||
sys.stderr = open(f"{const.OSDK_DIR}/osdk.log", "w")
|
|
||||||
|
|
||||||
try:
|
|
||||||
loadAll()
|
|
||||||
exec(a)
|
|
||||||
return 0
|
|
||||||
except Exception as e:
|
|
||||||
print(f"{vt100.RED}{e}{vt100.RESET}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
usage()
|
|
||||||
print()
|
|
||||||
|
|
||||||
raise e
|
|
347
osdk/cmds.py
347
osdk/cmds.py
|
@ -1,347 +0,0 @@
|
||||||
from typing import Callable, cast
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
|
|
||||||
from osdk.args import Args
|
|
||||||
from osdk.context import contextFor
|
|
||||||
from osdk import context, shell, const, vt100, builder, graph
|
|
||||||
|
|
||||||
Callback = Callable[[Args], None]
|
|
||||||
|
|
||||||
|
|
||||||
class Cmd:
|
|
||||||
shortName: str | None
|
|
||||||
longName: str
|
|
||||||
helpText: str
|
|
||||||
callback: Callable[[Args], None]
|
|
||||||
isPlugin: bool = False
|
|
||||||
|
|
||||||
def __init__(self, shortName: str | None, longName: str, helpText: str, callback: Callable[[Args], None]):
|
|
||||||
self.shortName = shortName
|
|
||||||
self.longName = longName
|
|
||||||
self.helpText = helpText
|
|
||||||
self.callback = callback
|
|
||||||
|
|
||||||
|
|
||||||
cmds: list[Cmd] = []
|
|
||||||
|
|
||||||
|
|
||||||
def append(cmd: Cmd):
|
|
||||||
cmd.isPlugin = True
|
|
||||||
cmds.append(cmd)
|
|
||||||
cmds.sort(key=lambda c: c.shortName or c.longName)
|
|
||||||
|
|
||||||
|
|
||||||
def runCmd(args: Args):
|
|
||||||
targetSpec = cast(str, args.consumeOpt(
|
|
||||||
"target", "host-" + shell.uname().machine))
|
|
||||||
|
|
||||||
componentSpec = args.consumeArg()
|
|
||||||
|
|
||||||
if componentSpec is None:
|
|
||||||
raise Exception("Component not specified")
|
|
||||||
|
|
||||||
exe = builder.build(componentSpec, targetSpec)
|
|
||||||
|
|
||||||
shell.exec(exe)
|
|
||||||
|
|
||||||
|
|
||||||
cmds += [Cmd("r", "run", "Run the target", runCmd)]
|
|
||||||
|
|
||||||
|
|
||||||
def debugCmd(args: Args):
|
|
||||||
targetSpec = cast(str, args.consumeOpt(
|
|
||||||
"target", "host-" + shell.uname().machine))
|
|
||||||
|
|
||||||
componentSpec = args.consumeArg()
|
|
||||||
|
|
||||||
if componentSpec is None:
|
|
||||||
raise Exception("Component not specified")
|
|
||||||
|
|
||||||
exe = builder.build(componentSpec, targetSpec)
|
|
||||||
|
|
||||||
shell.exec("lldb", "-o", "run", exe)
|
|
||||||
|
|
||||||
|
|
||||||
cmds += [Cmd("d", "debug", "Debug the target", debugCmd)]
|
|
||||||
|
|
||||||
|
|
||||||
def buildCmd(args: Args):
|
|
||||||
targetSpec = cast(str, args.consumeOpt(
|
|
||||||
"target", "host-" + shell.uname().machine))
|
|
||||||
|
|
||||||
componentSpec = args.consumeArg()
|
|
||||||
|
|
||||||
if componentSpec is None:
|
|
||||||
builder.buildAll(targetSpec)
|
|
||||||
else:
|
|
||||||
builder.build(componentSpec, targetSpec)
|
|
||||||
|
|
||||||
|
|
||||||
cmds += [Cmd("b", "build", "Build the target", buildCmd)]
|
|
||||||
|
|
||||||
|
|
||||||
def listCmd(args: Args):
|
|
||||||
components = context.loadAllComponents()
|
|
||||||
targets = context.loadAllTargets()
|
|
||||||
|
|
||||||
vt100.title("Components")
|
|
||||||
if len(components) == 0:
|
|
||||||
print(f" (No components 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)))))
|
|
||||||
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
cmds += [Cmd("l", "list", "List the targets", listCmd)]
|
|
||||||
|
|
||||||
|
|
||||||
def cleanCmd(args: Args):
|
|
||||||
shell.rmrf(const.BUILD_DIR)
|
|
||||||
|
|
||||||
|
|
||||||
cmds += [Cmd("c", "clean", "Clean the build directory", cleanCmd)]
|
|
||||||
|
|
||||||
|
|
||||||
def nukeCmd(args: Args):
|
|
||||||
shell.rmrf(const.OSDK_DIR)
|
|
||||||
|
|
||||||
|
|
||||||
cmds += [Cmd("n", "nuke", "Clean the build directory and cache", nukeCmd)]
|
|
||||||
|
|
||||||
|
|
||||||
def helpCmd(args: Args):
|
|
||||||
usage()
|
|
||||||
|
|
||||||
print()
|
|
||||||
|
|
||||||
vt100.title("Description")
|
|
||||||
print(" Operating System Development Kit.")
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
cmds += [Cmd("h", "help", "Show this help message", helpCmd)]
|
|
||||||
|
|
||||||
|
|
||||||
def versionCmd(args: Args):
|
|
||||||
print(f"OSDK v{const.get_version()}\n")
|
|
||||||
|
|
||||||
|
|
||||||
cmds += [Cmd("v", "version", "Show current version", versionCmd)]
|
|
||||||
|
|
||||||
|
|
||||||
def graphCmd(args: Args):
|
|
||||||
targetSpec = cast(str, args.consumeOpt(
|
|
||||||
"target", "host-" + shell.uname().machine))
|
|
||||||
|
|
||||||
scope: str | None = cast(str | None, 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)
|
|
||||||
|
|
||||||
|
|
||||||
cmds += [Cmd("g", "graph", "Show dependency graph", graphCmd)]
|
|
||||||
|
|
||||||
|
|
||||||
def installCmd(args: Args):
|
|
||||||
project = context.loadProject(".")
|
|
||||||
|
|
||||||
for extSpec in project.extern:
|
|
||||||
ext = project.extern[extSpec]
|
|
||||||
|
|
||||||
extPath = os.path.join(const.EXTERN_DIR, extSpec)
|
|
||||||
|
|
||||||
if os.path.exists(extPath):
|
|
||||||
print(f"Skipping {extSpec}, already installed")
|
|
||||||
continue
|
|
||||||
|
|
||||||
print(f"Installing {extSpec}-{ext.tag} from {ext.git}...")
|
|
||||||
shell.popen("git", "clone", "--depth", "1", "--branch",
|
|
||||||
ext.tag, ext.git, extPath)
|
|
||||||
|
|
||||||
|
|
||||||
cmds += [Cmd("i", "install", "Install all the external packages", installCmd)]
|
|
||||||
|
|
||||||
|
|
||||||
def initCmd(args: Args):
|
|
||||||
"""
|
|
||||||
|
|
|
||||||
| - project.json
|
|
||||||
| - src/
|
|
||||||
| | - project_name/
|
|
||||||
| | - main.c
|
|
||||||
| | - manifest.json
|
|
||||||
| - meta/
|
|
||||||
| | - targets/
|
|
||||||
| | | - host-*.json
|
|
||||||
| | - plugins/
|
|
||||||
| | | - run.py
|
|
||||||
| - .gitignore
|
|
||||||
| - README.md
|
|
||||||
|
|
|
||||||
"""
|
|
||||||
|
|
||||||
print("This utility will walk you through creating a new project.")
|
|
||||||
print("This only covers the most common items, and tries to give sensible defaults.")
|
|
||||||
print()
|
|
||||||
print("First, let's create a project.json file.")
|
|
||||||
|
|
||||||
project_name = input("Project name: ")
|
|
||||||
description = input("Description: ")
|
|
||||||
|
|
||||||
to_create = ["src", "meta", os.path.join("meta", "targets"), os.path.join("meta", "plugins")]
|
|
||||||
|
|
||||||
os.mkdir(project_name.lower())
|
|
||||||
for directory in to_create:
|
|
||||||
os.mkdir(os.path.join(project_name.lower(), directory))
|
|
||||||
|
|
||||||
with open(os.path.join(project_name.lower(), "project.json"), "w") as f:
|
|
||||||
f.write(json.dumps({
|
|
||||||
"$schema": "https://schemas.cute.engineering/latest/osdk.manifest.component",
|
|
||||||
"name": project_name,
|
|
||||||
"type": "project",
|
|
||||||
"description": description,
|
|
||||||
"extern": {},
|
|
||||||
}, indent=4))
|
|
||||||
|
|
||||||
with open(os.path.join(project_name.lower(), ".gitignore"), "w") as f:
|
|
||||||
f.write(".osdk\n.ninja_log\n__pycache__\n")
|
|
||||||
|
|
||||||
with open(os.path.join(project_name.lower(), "README.md"), "w") as f:
|
|
||||||
f.write(f"# {project_name}\n")
|
|
||||||
f.write("I was created using the OSDK!\n")
|
|
||||||
f.write(
|
|
||||||
"You can find more information about the OSDK in its [Repo](https://github.com/cute-engineering/osdk)."
|
|
||||||
)
|
|
||||||
|
|
||||||
with open(os.path.join(project_name.lower(), "src", "main.c"), "w") as f:
|
|
||||||
f.write("#include <stdio.h>\n\n")
|
|
||||||
f.write("int main(void)\n{\n")
|
|
||||||
f.write(" printf(\"Hello, World!\\n\");\n")
|
|
||||||
f.write(" return 0;\n")
|
|
||||||
f.write("}")
|
|
||||||
|
|
||||||
with open(os.path.join(project_name.lower(), "src", "manifest.json"), "w") as f:
|
|
||||||
f.write(json.dumps({
|
|
||||||
"$schema": "https://schemas.cute.engineering/latest/osdk.manifest.component",
|
|
||||||
"id": project_name.lower(),
|
|
||||||
"type": "exe",
|
|
||||||
"description": description,
|
|
||||||
}, indent=4))
|
|
||||||
|
|
||||||
with open(os.path.join(project_name.lower(), "meta", "plugins", "run.py"), "w") as f:
|
|
||||||
f.write("from osdk import builder, shell\n")
|
|
||||||
f.write("from osdk.args import Args\n")
|
|
||||||
f.write("from osdk.cmds import Cmd, append\n\n")
|
|
||||||
f.write("def runCmd(args: Args) -> None:\n")
|
|
||||||
f.write(
|
|
||||||
f" {project_name.lower()} = builder.build(\"{project_name.lower()}\", \"host-{shell.uname().machine}\")\n"
|
|
||||||
)
|
|
||||||
f.write(f" shell.exec(*[{project_name.lower()}])")
|
|
||||||
f.write("\n\nappend(Cmd(\"s\", \"start\", \"Run the project\", runCmd))")
|
|
||||||
|
|
||||||
with open(os.path.join(project_name.lower(), "meta", "targets", f"host-{shell.uname().machine}.json"), "w") as f:
|
|
||||||
f.write(json.dumps({
|
|
||||||
"$schema": "https://schemas.cute.engineering/latest/osdk.manifest.component",
|
|
||||||
"id": f"host-{shell.uname().machine}",
|
|
||||||
"type": "target",
|
|
||||||
"props": {
|
|
||||||
"arch": shell.uname().machine,
|
|
||||||
"toolchain": "clang",
|
|
||||||
"sys": [
|
|
||||||
"@uname",
|
|
||||||
"sysname"
|
|
||||||
],
|
|
||||||
"abi": "unknown",
|
|
||||||
"freestanding": False,
|
|
||||||
"host": True,
|
|
||||||
},
|
|
||||||
"tools": {
|
|
||||||
"cc": {
|
|
||||||
"cmd": [
|
|
||||||
"@latest",
|
|
||||||
"clang"
|
|
||||||
],
|
|
||||||
"args": []
|
|
||||||
},
|
|
||||||
"cxx": {
|
|
||||||
"cmd": [
|
|
||||||
"@latest",
|
|
||||||
"clang++"
|
|
||||||
],
|
|
||||||
"args": []
|
|
||||||
},
|
|
||||||
"ld": {
|
|
||||||
"cmd": [
|
|
||||||
"@latest",
|
|
||||||
"clang++"
|
|
||||||
],
|
|
||||||
"args": [
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"ar": {
|
|
||||||
"cmd": [
|
|
||||||
"@latest",
|
|
||||||
"llvm-ar"
|
|
||||||
],
|
|
||||||
"args": [
|
|
||||||
"rcs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"as": {
|
|
||||||
"cmd": "clang",
|
|
||||||
"args": [
|
|
||||||
"-c"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, indent=4))
|
|
||||||
|
|
||||||
shell.exec(*["git", "init", project_name.lower()])
|
|
||||||
print("Done! Don't forget to add a LICENSE ;)")
|
|
||||||
|
|
||||||
|
|
||||||
cmds += [Cmd("I", "init", "Start a new project", initCmd)]
|
|
||||||
|
|
||||||
|
|
||||||
def usage():
|
|
||||||
print(f"Usage: {const.ARGV0} <command> [args...]")
|
|
||||||
|
|
||||||
|
|
||||||
def exec(args: Args):
|
|
||||||
cmd = args.consumeArg()
|
|
||||||
|
|
||||||
if cmd is None:
|
|
||||||
raise Exception("No command specified")
|
|
||||||
|
|
||||||
for c in cmds:
|
|
||||||
if c.shortName == cmd or c.longName == cmd:
|
|
||||||
c.callback(args)
|
|
||||||
return
|
|
||||||
|
|
||||||
raise Exception(f"Unknown command {cmd}")
|
|
|
@ -1,14 +0,0 @@
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
VERSION = "0.4.1"
|
|
||||||
MODULE_DIR = os.path.dirname(os.path.realpath(__file__))
|
|
||||||
ARGV0 = os.path.basename(sys.argv[0])
|
|
||||||
OSDK_DIR = ".osdk"
|
|
||||||
BUILD_DIR = os.path.join(OSDK_DIR, "build")
|
|
||||||
CACHE_DIR = os.path.join(OSDK_DIR, "cache")
|
|
||||||
EXTERN_DIR = os.path.join(OSDK_DIR, "extern")
|
|
||||||
SRC_DIR = "src"
|
|
||||||
META_DIR = f"meta"
|
|
||||||
TARGETS_DIR = os.path.join(META_DIR, "targets")
|
|
|
@ -1,51 +0,0 @@
|
||||||
from typing import Any, cast, Callable, Final
|
|
||||||
import json
|
|
||||||
|
|
||||||
import osdk.shell as shell
|
|
||||||
|
|
||||||
Json = Any
|
|
||||||
Builtin = Callable[..., Json]
|
|
||||||
|
|
||||||
BUILTINS: Final[dict[str, Builtin]] = {
|
|
||||||
"uname": lambda arg: getattr(shell.uname(), arg).lower(),
|
|
||||||
"include": lambda arg: evalRead(arg),
|
|
||||||
"evalRead": lambda arg: evalRead(arg),
|
|
||||||
"join": lambda lhs, rhs: cast(Json, {**lhs, **rhs} if isinstance(lhs, dict) else lhs + rhs),
|
|
||||||
"concat": lambda *args: "".join(args),
|
|
||||||
"eval": lambda arg: eval(arg),
|
|
||||||
"read": lambda arg: read(arg),
|
|
||||||
"exec": lambda *args: shell.popen(*args).splitlines(),
|
|
||||||
"latest": lambda arg: shell.latest(arg),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def eval(jexpr: Json) -> Json:
|
|
||||||
if isinstance(jexpr, dict):
|
|
||||||
result = {}
|
|
||||||
for k in cast(dict[str, Json], jexpr):
|
|
||||||
result[k] = eval(jexpr[k])
|
|
||||||
return cast(Json, result)
|
|
||||||
elif isinstance(jexpr, list):
|
|
||||||
jexpr = cast(list[Json], jexpr)
|
|
||||||
if len(jexpr) > 0 and isinstance(jexpr[0], str) and jexpr[0].startswith("@"):
|
|
||||||
funcName = jexpr[0][1:]
|
|
||||||
if funcName in BUILTINS:
|
|
||||||
return BUILTINS[funcName](*eval(jexpr[1:]))
|
|
||||||
|
|
||||||
raise Exception(f"Unknown macro {funcName}")
|
|
||||||
else:
|
|
||||||
return list(map(eval, jexpr))
|
|
||||||
else:
|
|
||||||
return jexpr
|
|
||||||
|
|
||||||
|
|
||||||
def read(path: str) -> Json:
|
|
||||||
try:
|
|
||||||
with open(path, "r") as f:
|
|
||||||
return json.load(f)
|
|
||||||
except:
|
|
||||||
raise Exception(f"Failed to read {path}")
|
|
||||||
|
|
||||||
|
|
||||||
def evalRead(path: str) -> Json:
|
|
||||||
return eval(read(path))
|
|
|
@ -1,21 +0,0 @@
|
||||||
import sys
|
|
||||||
import osdk.vt100 as vt100
|
|
||||||
|
|
||||||
|
|
||||||
class Logger:
|
|
||||||
name: str
|
|
||||||
|
|
||||||
def __init__(self, name: str):
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
def log(self, message: str):
|
|
||||||
print(
|
|
||||||
f"{vt100.CYAN}[{self.name}]{vt100.RESET} {message}", file=sys.stderr)
|
|
||||||
|
|
||||||
def warn(self, message: str):
|
|
||||||
print(
|
|
||||||
f"{vt100.YELLOW}[{self.name}]{vt100.RESET} {message}", file=sys.stderr)
|
|
||||||
|
|
||||||
def error(self, message: str):
|
|
||||||
print(
|
|
||||||
f"{vt100.RED}[{self.name}]{vt100.RESET} {message}", file=sys.stderr)
|
|
|
@ -1,30 +0,0 @@
|
||||||
import os
|
|
||||||
|
|
||||||
import importlib.util as importlib
|
|
||||||
from osdk.logger import Logger
|
|
||||||
from osdk.shell import readdir
|
|
||||||
|
|
||||||
logger = Logger("plugins")
|
|
||||||
|
|
||||||
|
|
||||||
def load(path: str):
|
|
||||||
logger.log(f"Loading plugin {path}")
|
|
||||||
spec = importlib.spec_from_file_location("plugin", path)
|
|
||||||
|
|
||||||
if not spec or not spec.loader:
|
|
||||||
logger.error(f"Failed to load plugin {path}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
module = importlib.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(module)
|
|
||||||
|
|
||||||
|
|
||||||
def loadAll():
|
|
||||||
logger.log("Loading plugins...")
|
|
||||||
for files in readdir(os.path.join("meta", "plugins")):
|
|
||||||
if files.endswith(".py"):
|
|
||||||
plugin = load(os.path.join("meta", "plugins", files))
|
|
||||||
|
|
||||||
if plugin:
|
|
||||||
print(f"Loaded plugin {plugin.name}")
|
|
||||||
plugin.init()
|
|
|
@ -1,2 +1,2 @@
|
||||||
requests ~= 2.28.0
|
requests ~= 2.28.0
|
||||||
graphviz ~= 0.20.1
|
graphviz ~= 0.20.1
|
13
setup.py
13
setup.py
|
@ -1,22 +1,23 @@
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
from osdk.const import VERSION
|
from cutekit.const import VERSION_STR, DESCRIPTION
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="osdk",
|
name="cutekit",
|
||||||
version=VERSION,
|
version=VERSION_STR,
|
||||||
python_requires='>=3.10',
|
python_requires='>=3.10',
|
||||||
description="Operating System Development Kit",
|
description=DESCRIPTION,
|
||||||
author="Cute Engineering",
|
author="Cute Engineering",
|
||||||
author_email="contact@cute.engineering",
|
author_email="contact@cute.engineering",
|
||||||
url="https://cute.engineering/",
|
url="https://cute.engineering/",
|
||||||
packages=["osdk"],
|
packages=["cutekit"],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"requests",
|
"requests",
|
||||||
"graphviz"
|
"graphviz"
|
||||||
],
|
],
|
||||||
entry_points={
|
entry_points={
|
||||||
"console_scripts": [
|
"console_scripts": [
|
||||||
"osdk = osdk:main",
|
"ck = cutekit:main",
|
||||||
|
"cutekit = cutekit:main",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
license="MIT",
|
license="MIT",
|
||||||
|
|
Loading…
Reference in a new issue