Compare commits
52 commits
stable
...
origin/0.7
Author | SHA1 | Date | |
---|---|---|---|
Sleepy Monax | ad5cc391be | ||
Sleepy Monax | 647043f4d3 | ||
Sleepy Monax | a13c76cddf | ||
Sleepy Monax | d3d4482f8e | ||
Sleepy Monax | 01371ed5d4 | ||
Sleepy Monax | d64980d43d | ||
Sleepy Monax | 2e005bd48e | ||
Sleepy Monax | 9164c02a3e | ||
Sleepy Monax | ec77c87515 | ||
Sleepy Monax | 2f8b35f79e | ||
Sleepy Monax | 3dcca6591b | ||
Jordan ⌨️ | 86768ad452 | ||
Jordan ⌨️ | b91e73662a | ||
Sleepy Monax | da8d82e09f | ||
Sleepy Monax | 35843cce22 | ||
Sleepy Monax | c4ae611ec3 | ||
Sleepy Monax | 3019b1807f | ||
Sleepy Monax | 07932d134a | ||
Sleepy Monax | aa6a55f717 | ||
Sleepy Monax | 16225f50d4 | ||
Sleepy Monax | dd4324bad4 | ||
Sleepy Monax | f134c5752b | ||
Sleepy Monax | 9c9db6c36b | ||
Sleepy Monax | 1fd19e757a | ||
Sleepy Monax | 594953ea68 | ||
Sleepy Monax | b0c8ee351f | ||
Sleepy Monax | 1a7c2c475a | ||
Sleepy Monax | 5805d1ddf3 | ||
Jordan ⌨️ | 4c18fe5089 | ||
Sleepy Monax | 0c7191d050 | ||
Sleepy Monax | 78130b4f7f | ||
Sleepy Monax | 3e8c2f94ff | ||
Sleepy Monax | 00e35a2d0f | ||
Sleepy Monax | f810003ab9 | ||
Sleepy Monax | 83a57b77eb | ||
Sleepy Monax | da5a11932f | ||
Sleepy Monax | 0dd584f881 | ||
Sleepy Monax | ecdae7903e | ||
Sleepy Monax | d6cff3bcbc | ||
Sleepy Monax | 0dac774fb8 | ||
Sleepy Monax | 0bbced54c4 | ||
Sleepy Monax | f50a571afa | ||
Jordan ⌨️ | 2b452c744a | ||
Sleepy Monax | ea0e5613e8 | ||
Sleepy Monax | cc88242cef | ||
Sleepy Monax | 6c0f6d5bd5 | ||
Sleepy Monax | 8a9c4689e9 | ||
Sleepy Monax | f3e7727796 | ||
Sleepy Monax | a76f4921e4 | ||
Sleepy Monax | c136dd8e1a | ||
Jordan ⌨️ | fafcfbca48 | ||
Jordan ⌨️ | d8b7a1630c |
4
.github/workflows/checks.yml
vendored
4
.github/workflows/checks.yml
vendored
|
@ -13,12 +13,12 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
python-version: '3.x'
|
python-version: '3.11'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
|
|
7
.github/workflows/publish.yml
vendored
7
.github/workflows/publish.yml
vendored
|
@ -20,11 +20,14 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
python-version: '3.x'
|
python-version: '3.11'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -127,3 +127,4 @@ dmypy.json
|
||||||
|
|
||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
.vscode/
|
||||||
|
|
66
README.md
66
README.md
|
@ -1,66 +0,0 @@
|
||||||
<br/>
|
|
||||||
<br/>
|
|
||||||
<br/>
|
|
||||||
<p align="center">
|
|
||||||
<img src="logo.png" width="200" height="200">
|
|
||||||
</p>
|
|
||||||
<h1 align="center">CuteKit</h1>
|
|
||||||
<p align="center">
|
|
||||||
The *magical* build system and package manager
|
|
||||||
</p>
|
|
||||||
<br/>
|
|
||||||
<br/>
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
## Table of contents
|
|
||||||
|
|
||||||
- [Table of contents](#table-of-contents)
|
|
||||||
- [Introduction](#introduction)
|
|
||||||
- [Quick-start](#quick-start)
|
|
||||||
- [Example](#example)
|
|
||||||
|
|
||||||
|
|
||||||
## Introduction
|
|
||||||
|
|
||||||
**CuteKit** is a simple - yet - powerful build system and package manager for C and C++. It:
|
|
||||||
|
|
||||||
- ✨ It uses **JSON**: Cutekit uses JSON instead of introducing a whole new programming language for describing the project. And also has macros to help the user experience (see [Jexpr](doc/spec/jexpr.md)).
|
|
||||||
- ✨ It's a **package manager**: Cutekit package manager is based on **Git**, nothing is centralized.
|
|
||||||
- ✨ It's **extendible**: Cutekit can be [extended](./doc/extends.md) by writing custom Python plugins.
|
|
||||||
- ✨ It's **easy**: the [**templates**](./doc/templates.md) help the user quick-start a project.
|
|
||||||
- ✨ It's **portable**: Cutekit can run on MacOS Gnu/Linux and Windows.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
To install Cutekit, you may use your favourite package manager if it is available. Or you can install it manually by following the instructions below.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ git clone https://github.com/cute-engineering/cutekit
|
|
||||||
|
|
||||||
$ cd cutekit
|
|
||||||
|
|
||||||
# If you want to use the latest version of Cutekit, you can switch to the dev branch.
|
|
||||||
# $ git switch dev
|
|
||||||
|
|
||||||
$ pip install --user -e .
|
|
||||||
```
|
|
||||||
|
|
||||||
## Quick-start
|
|
||||||
|
|
||||||
-> If you directly want to start using Cutekit for a new project, you can just run `$ ck I host` and it will create a new project in the host directory (you can rename it later).
|
|
||||||
|
|
||||||
-> If you want to use Cutekit for writing operating systems, you can create a new [limine](https://github.com/limine-bootloader/limine/)-based project by running `$ ck I limine-barebone`.
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
If you want to see how it works you can read the [doc/cutekit.md](doc/cutekit.md) file.
|
|
||||||
|
|
||||||
# License
|
|
||||||
|
|
||||||
<a href="https://opensource.org/licenses/MIT">
|
|
||||||
<img align="right" height="96" alt="MIT License" src="doc/mit.svg" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
Cutekit is licensed under the **MIT License**.
|
|
||||||
|
|
||||||
The full text of the license can be accessed via [this link](https://opensource.org/licenses/MIT) and is also included in the [license.md](license.md) file of this software package.
|
|
|
@ -9,6 +9,8 @@ from . import (
|
||||||
graph, # noqa: F401 this is imported for side effects
|
graph, # noqa: F401 this is imported for side effects
|
||||||
model,
|
model,
|
||||||
plugins,
|
plugins,
|
||||||
|
pods, # noqa: F401 this is imported for side effects
|
||||||
|
shell,
|
||||||
vt100,
|
vt100,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -39,10 +41,7 @@ def setupLogger(verbose: bool):
|
||||||
if projectRoot is not None:
|
if projectRoot is not None:
|
||||||
logFile = os.path.join(projectRoot.dirname(), const.PROJECT_LOG_FILE)
|
logFile = os.path.join(projectRoot.dirname(), const.PROJECT_LOG_FILE)
|
||||||
|
|
||||||
# create the directory if it doesn't exist
|
shell.mkdir(os.path.dirname(logFile))
|
||||||
logDir = os.path.dirname(logFile)
|
|
||||||
if not os.path.isdir(logDir):
|
|
||||||
os.makedirs(logDir)
|
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
|
@ -55,18 +54,22 @@ def setupLogger(verbose: bool):
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
try:
|
try:
|
||||||
a = cli.parse(sys.argv[1:])
|
shell.mkdir(const.GLOBAL_CK_DIR)
|
||||||
setupLogger(a.consumeOpt("verbose", False) is True)
|
extraArgs = os.environ.get("CK_EXTRA_ARGS", None)
|
||||||
plugins.loadAll()
|
args = cli.parse((extraArgs.split(" ") if extraArgs else []) + sys.argv[1:])
|
||||||
cli.exec(a)
|
setupLogger(args.consumeOpt("verbose", False) is True)
|
||||||
print()
|
|
||||||
|
const.setup()
|
||||||
|
plugins.setup(args)
|
||||||
|
pods.setup(args)
|
||||||
|
cli.exec(args)
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
cli.error(str(e))
|
cli.error(str(e))
|
||||||
cli.usage()
|
cli.usage()
|
||||||
print()
|
|
||||||
return 1
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print()
|
print()
|
||||||
return 1
|
|
||||||
|
return 1
|
||||||
|
|
86
cutekit/bootstrap.sh
Normal file
86
cutekit/bootstrap.sh
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# This script is meant to be place at the root of any cutekit project.
|
||||||
|
# It will make sure that the virtual environment is set up and that the
|
||||||
|
# plugins requirements are installed.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ -z "$CUTEKIT_PYTHON" ]; then
|
||||||
|
export CUTEKIT_PYTHON="python3.11"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$CUTEKIT_VERSION" ]; then
|
||||||
|
export CUTEKIT_VERSION="0.7-dev"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$CUTEKIT_NOVENV" ]; then
|
||||||
|
echo "CUTEKIT_NOVENV is set, skipping virtual environment setup."
|
||||||
|
exec cutekit $@
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$1" == "tools" -a "$2" == "nuke" ]; then
|
||||||
|
rm -rf .cutekit/tools .cutekit/venv
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f .cutekit/tools/ready ]; then
|
||||||
|
if [ ! \( "$1" == "tools" -a "$2" == "setup" \) ]; then
|
||||||
|
echo "CuteKit is not installed."
|
||||||
|
echo "This script will install cutekit into $PWD/.cutekit"
|
||||||
|
|
||||||
|
read -p "Do you want to continue? [Y/n] " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo "Aborting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Installing CuteKit..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p .cutekit
|
||||||
|
if [ ! -d .cutekit/venv ]; then
|
||||||
|
echo "Setting up Python virtual environment..."
|
||||||
|
$CUTEKIT_PYTHON -m venv .cutekit/venv
|
||||||
|
fi
|
||||||
|
source .cutekit/venv/bin/activate
|
||||||
|
|
||||||
|
echo "Downloading CuteKit..."
|
||||||
|
if [ ! -d .cutekit/tools/cutekit ]; then
|
||||||
|
git clone --depth 1 https://github.com/cute-engineering/cutekit .cutekit/tools/cutekit --branch "$CUTEKIT_VERSION"
|
||||||
|
else
|
||||||
|
echo "CuteKit already downloaded."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Installing Tools..."
|
||||||
|
$CUTEKIT_PYTHON -m pip install -e .cutekit/tools/cutekit
|
||||||
|
|
||||||
|
echo "Installing plugins requirements..."
|
||||||
|
if [ -f "meta/plugins/requirements.txt" ]; then
|
||||||
|
echo "Root plugin requirements found."
|
||||||
|
$CUTEKIT_PYTHON -m pip install -r meta/plugins/requirements.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
for extern in meta/externs/*; do
|
||||||
|
if [ -f "$extern/meta/plugins/requirements.txt" ]; then
|
||||||
|
echo "Plugin requirements found in $extern."
|
||||||
|
$CUTEKIT_PYTHON -m pip install -r "$extern/meta/plugins/requirements.txt"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
touch .cutekit/tools/ready
|
||||||
|
echo "Done!"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$1" == "tools" -a "$2" == "setup" ]; then
|
||||||
|
echo "Tools already installed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
source .cutekit/venv/bin/activate
|
||||||
|
export PATH="$PATH:.cutekit/venv/bin"
|
||||||
|
|
||||||
|
$CUTEKIT_PYTHON -m cutekit $@
|
||||||
|
|
|
@ -3,32 +3,121 @@ import logging
|
||||||
import dataclasses as dt
|
import dataclasses as dt
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TextIO, Union
|
import sys
|
||||||
|
from typing import Callable, Literal, TextIO, Union
|
||||||
|
|
||||||
from . import shell, rules, model, ninja, const, cli
|
from . import shell, rules, model, ninja, const, cli, vt100
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def aggregateCincs(target: model.Target, registry: model.Registry) -> set[str]:
|
@dt.dataclass
|
||||||
|
class Scope:
|
||||||
|
registry: model.Registry
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def use(args: cli.Args, props: model.Props = {}) -> "Scope":
|
||||||
|
registry = model.Registry.use(args, props)
|
||||||
|
return Scope(registry)
|
||||||
|
|
||||||
|
def key(self) -> str:
|
||||||
|
return self.registry.project.id
|
||||||
|
|
||||||
|
def openTargetScope(self, t: model.Target):
|
||||||
|
return TargetScope(self.registry, t)
|
||||||
|
|
||||||
|
|
||||||
|
@dt.dataclass
|
||||||
|
class TargetScope(Scope):
|
||||||
|
registry: model.Registry
|
||||||
|
target: model.Target
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def use(args: cli.Args, props: model.Props = {}) -> "TargetScope":
|
||||||
|
registry = model.Registry.use(args, props)
|
||||||
|
target = model.Target.use(args, props)
|
||||||
|
return TargetScope(registry, target)
|
||||||
|
|
||||||
|
def key(self) -> str:
|
||||||
|
return super().key() + "/" + self.target.id + "/" + self.target.hashid
|
||||||
|
|
||||||
|
def openComponentScope(self, c: model.Component):
|
||||||
|
return ComponentScope(self.registry, self.target, c)
|
||||||
|
|
||||||
|
|
||||||
|
@dt.dataclass
|
||||||
|
class ComponentScope(TargetScope):
|
||||||
|
component: model.Component
|
||||||
|
|
||||||
|
def key(self) -> str:
|
||||||
|
return super().key() + "/" + self.component.id
|
||||||
|
|
||||||
|
def openComponentScope(self, c: model.Component):
|
||||||
|
return ComponentScope(self.registry, self.target, c)
|
||||||
|
|
||||||
|
def openProductScope(self, path: Path):
|
||||||
|
return ProductScope(self.registry, self.target, self.component, path)
|
||||||
|
|
||||||
|
|
||||||
|
@dt.dataclass
|
||||||
|
class ProductScope(ComponentScope):
|
||||||
|
path: Path
|
||||||
|
|
||||||
|
|
||||||
|
# --- Variables -------------------------------------------------------------- #
|
||||||
|
|
||||||
|
Compute = Callable[[TargetScope], list[str]]
|
||||||
|
_vars: dict[str, Compute] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def var(name: str) -> Callable[[Compute], Compute]:
|
||||||
|
def decorator(func: Compute):
|
||||||
|
_vars[name] = func
|
||||||
|
return func
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
@var("builddir")
|
||||||
|
def _computeBuilddir(scope: TargetScope) -> list[str]:
|
||||||
|
"""
|
||||||
|
This variable is needed by ninja to know where to put
|
||||||
|
the .ninja_log file.
|
||||||
|
"""
|
||||||
|
return [scope.target.builddir]
|
||||||
|
|
||||||
|
|
||||||
|
@var("hashid")
|
||||||
|
def _computeHashid(scope: TargetScope) -> list[str]:
|
||||||
|
return [scope.target.hashid]
|
||||||
|
|
||||||
|
|
||||||
|
@var("cincs")
|
||||||
|
def _computeCinc(scope: TargetScope) -> list[str]:
|
||||||
res = set()
|
res = set()
|
||||||
|
|
||||||
for c in registry.iterEnabled(target):
|
for c in scope.registry.iterEnabled(scope.target):
|
||||||
if "cpp-root-include" in c.props:
|
if "cpp-root-include" in c.props:
|
||||||
res.add(c.dirname())
|
res.add(c.dirname())
|
||||||
|
elif "cpp-excluded" in c.props:
|
||||||
|
pass
|
||||||
elif c.type == model.Kind.LIB:
|
elif c.type == model.Kind.LIB:
|
||||||
res.add(str(Path(c.dirname()).parent))
|
res.add(str(Path(c.dirname()).parent))
|
||||||
|
|
||||||
return set(map(lambda i: f"-I{i}", res))
|
return sorted(map(lambda i: f"-I{i}", res))
|
||||||
|
|
||||||
|
|
||||||
def aggregateCdefs(target: model.Target) -> set[str]:
|
@var("cdefs")
|
||||||
|
def _computeCdef(scope: TargetScope) -> list[str]:
|
||||||
res = set()
|
res = set()
|
||||||
|
|
||||||
def sanatize(s: str) -> str:
|
def sanatize(s: str) -> str:
|
||||||
return s.lower().replace(" ", "_").replace("-", "_").replace(".", "_")
|
TO_REPLACE = [" ", "-", "."] # -> "_"
|
||||||
|
for r in TO_REPLACE:
|
||||||
|
s = s.replace(r, "_")
|
||||||
|
return "".join(filter(lambda c: c.isalnum() or c == "_", s))
|
||||||
|
|
||||||
for k, v in target.props.items():
|
for k, v in scope.target.props.items():
|
||||||
if isinstance(v, bool):
|
if isinstance(v, bool):
|
||||||
if v:
|
if v:
|
||||||
res.add(f"-D__ck_{sanatize(k)}__")
|
res.add(f"-D__ck_{sanatize(k)}__")
|
||||||
|
@ -36,40 +125,60 @@ def aggregateCdefs(target: model.Target) -> set[str]:
|
||||||
res.add(f"-D__ck_{sanatize(k)}_{sanatize(str(v))}__")
|
res.add(f"-D__ck_{sanatize(k)}_{sanatize(str(v))}__")
|
||||||
res.add(f"-D__ck_{sanatize(k)}_value={str(v)}")
|
res.add(f"-D__ck_{sanatize(k)}_value={str(v)}")
|
||||||
|
|
||||||
return res
|
return sorted(res)
|
||||||
|
|
||||||
|
|
||||||
def buildpath(target: model.Target, component: model.Component, path) -> Path:
|
def buildpath(scope: ComponentScope, path) -> Path:
|
||||||
return Path(target.builddir) / component.id / path
|
return Path(scope.target.builddir) / scope.component.id / path
|
||||||
|
|
||||||
|
|
||||||
# --- Compilation ------------------------------------------------------------ #
|
# --- Compilation ------------------------------------------------------------ #
|
||||||
|
|
||||||
|
|
||||||
def wilcard(component: model.Component, wildcards: list[str]) -> list[str]:
|
def subdirs(scope: ComponentScope) -> list[str]:
|
||||||
dirs = [component.dirname()] + list(
|
component = scope.component
|
||||||
map(lambda d: os.path.join(component.dirname(), d), component.subdirs)
|
result = [component.dirname()]
|
||||||
)
|
|
||||||
return shell.find(dirs, list(wildcards), recusive=False)
|
for subs in component.subdirs:
|
||||||
|
result.append(os.path.join(component.dirname(), subs))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def wilcard(scope: ComponentScope, wildcards: list[str]) -> list[str]:
|
||||||
|
return shell.find(subdirs(scope), list(wildcards), recusive=False)
|
||||||
|
|
||||||
|
|
||||||
def compile(
|
def compile(
|
||||||
w: ninja.Writer,
|
w: ninja.Writer, scope: ComponentScope, rule: str, srcs: list[str]
|
||||||
target: model.Target,
|
|
||||||
component: model.Component,
|
|
||||||
rule: str,
|
|
||||||
srcs: list[str],
|
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
res: list[str] = []
|
res: list[str] = []
|
||||||
for src in srcs:
|
for src in srcs:
|
||||||
rel = Path(src).relative_to(component.dirname())
|
rel = Path(src).relative_to(scope.component.dirname())
|
||||||
dest = buildpath(target, component, "obj") / rel.with_suffix(".o")
|
dest = buildpath(scope, path="__obj__") / rel.with_suffix(rel.suffix + ".o")
|
||||||
t = target.tools[rule]
|
t = scope.target.tools[rule]
|
||||||
w.build(str(dest), rule, inputs=src, order_only=t.files)
|
w.build(
|
||||||
|
str(dest),
|
||||||
|
rule,
|
||||||
|
inputs=src,
|
||||||
|
order_only=t.files,
|
||||||
|
variables={
|
||||||
|
"ck_target": scope.target.id,
|
||||||
|
"ck_component": scope.component.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
res.append(str(dest))
|
res.append(str(dest))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def compileObjs(w: ninja.Writer, scope: ComponentScope) -> list[str]:
|
||||||
|
objs = []
|
||||||
|
for rule in rules.rules.values():
|
||||||
|
if rule.id not in ["cp", "ld", "ar"]:
|
||||||
|
objs += compile(w, scope, rule.id, wilcard(scope, rule.fileIn))
|
||||||
|
return objs
|
||||||
|
|
||||||
|
|
||||||
# --- Ressources ------------------------------------------------------------- #
|
# --- Ressources ------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
@ -79,14 +188,21 @@ def listRes(component: model.Component) -> list[str]:
|
||||||
|
|
||||||
def compileRes(
|
def compileRes(
|
||||||
w: ninja.Writer,
|
w: ninja.Writer,
|
||||||
target: model.Target,
|
scope: ComponentScope,
|
||||||
component: model.Component,
|
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
res: list[str] = []
|
res: list[str] = []
|
||||||
for r in listRes(component):
|
for r in listRes(scope.component):
|
||||||
rel = Path(r).relative_to(component.subpath("res"))
|
rel = Path(r).relative_to(scope.component.subpath("res"))
|
||||||
dest = buildpath(target, component, "res") / rel
|
dest = buildpath(scope, "__res__") / rel
|
||||||
w.build(str(dest), "cp", r)
|
w.build(
|
||||||
|
str(dest),
|
||||||
|
"cp",
|
||||||
|
r,
|
||||||
|
variables={
|
||||||
|
"ck_target": scope.target.id,
|
||||||
|
"ck_component": scope.component.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
res.append(str(dest))
|
res.append(str(dest))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
@ -94,204 +210,252 @@ def compileRes(
|
||||||
# --- Linking ---------------------------------------------------------------- #
|
# --- Linking ---------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
def outfile(target: model.Target, component: model.Component) -> str:
|
def outfile(scope: ComponentScope) -> str:
|
||||||
if component.type == model.Kind.LIB:
|
if scope.component.type == model.Kind.LIB:
|
||||||
return str(buildpath(target, component, f"lib/{component.id}.a"))
|
return str(buildpath(scope, f"__lib__/{scope.component.id}.a"))
|
||||||
else:
|
else:
|
||||||
return str(buildpath(target, component, f"bin/{component.id}.out"))
|
return str(buildpath(scope, f"__bin__/{scope.component.id}.out"))
|
||||||
|
|
||||||
|
|
||||||
def collectLibs(
|
def collectLibs(
|
||||||
registry: model.Registry, target: model.Target, component: model.Component
|
scope: ComponentScope,
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
res: list[str] = []
|
res: list[str] = []
|
||||||
for r in component.resolved[target.id].resolved:
|
for r in scope.component.resolved[scope.target.id].required:
|
||||||
req = registry.lookup(r, model.Component)
|
req = scope.registry.lookup(r, model.Component)
|
||||||
assert req is not None # model.Resolver has already checked this
|
assert req is not None # model.Resolver has already checked this
|
||||||
|
|
||||||
if r == component.id:
|
if r == scope.component.id:
|
||||||
continue
|
continue
|
||||||
if not req.type == model.Kind.LIB:
|
if not req.type == model.Kind.LIB:
|
||||||
raise RuntimeError(f"Component {r} is not a library")
|
raise RuntimeError(f"Component {r} is not a library")
|
||||||
res.append(outfile(target, req))
|
res.append(outfile(scope.openComponentScope(req)))
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def collectInjectedLibs(scope: ComponentScope) -> list[str]:
|
||||||
|
res: list[str] = []
|
||||||
|
for r in scope.component.resolved[scope.target.id].injected:
|
||||||
|
req = scope.registry.lookup(r, model.Component)
|
||||||
|
assert req is not None # model.Resolver has already checked this
|
||||||
|
|
||||||
|
if r == scope.component.id:
|
||||||
|
continue
|
||||||
|
if not req.type == model.Kind.LIB:
|
||||||
|
raise RuntimeError(f"Component {r} is not a library")
|
||||||
|
res.append(outfile(scope.openComponentScope(req)))
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
def link(
|
def link(
|
||||||
w: ninja.Writer,
|
w: ninja.Writer,
|
||||||
registry: model.Registry,
|
scope: ComponentScope,
|
||||||
target: model.Target,
|
|
||||||
component: model.Component,
|
|
||||||
) -> str:
|
) -> str:
|
||||||
w.newline()
|
w.newline()
|
||||||
out = outfile(target, component)
|
out = outfile(scope)
|
||||||
|
|
||||||
objs = []
|
res = compileRes(w, scope)
|
||||||
objs += compile(w, target, component, "cc", wilcard(component, ["*.c"]))
|
objs = compileObjs(w, scope)
|
||||||
objs += compile(
|
if scope.component.type == model.Kind.LIB:
|
||||||
w, target, component, "cxx", wilcard(component, ["*.cpp", "*.cc", "*.cxx"])
|
w.build(
|
||||||
)
|
out,
|
||||||
objs += compile(
|
"ar",
|
||||||
w, target, component, "as", wilcard(component, ["*.s", "*.asm", "*.S"])
|
objs,
|
||||||
)
|
implicit=res,
|
||||||
|
variables={
|
||||||
res = compileRes(w, target, component)
|
"ck_target": scope.target.id,
|
||||||
libs = collectLibs(registry, target, component)
|
"ck_component": scope.component.id,
|
||||||
if component.type == model.Kind.LIB:
|
},
|
||||||
w.build(out, "ar", objs, implicit=res)
|
)
|
||||||
else:
|
else:
|
||||||
w.build(out, "ld", objs + libs, implicit=res)
|
wholeLibs = collectInjectedLibs(scope)
|
||||||
|
libs = collectLibs(scope)
|
||||||
|
w.build(
|
||||||
|
out,
|
||||||
|
"ld",
|
||||||
|
objs + wholeLibs + libs,
|
||||||
|
variables={
|
||||||
|
"objs": " ".join(objs),
|
||||||
|
"wholeLibs": " ".join(wholeLibs),
|
||||||
|
"libs": " ".join(libs),
|
||||||
|
"ck_target": scope.target.id,
|
||||||
|
"ck_component": scope.component.id,
|
||||||
|
},
|
||||||
|
implicit=res,
|
||||||
|
)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
# --- Phony ------------------------------------------------------------------ #
|
# --- Phony ------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
|
||||||
def all(w: ninja.Writer, registry: model.Registry, target: model.Target) -> list[str]:
|
def all(w: ninja.Writer, scope: TargetScope) -> list[str]:
|
||||||
all: list[str] = []
|
all: list[str] = []
|
||||||
for c in registry.iterEnabled(target):
|
for c in scope.registry.iterEnabled(scope.target):
|
||||||
all.append(link(w, registry, target, c))
|
all.append(link(w, scope.openComponentScope(c)))
|
||||||
w.build("all", "phony", all)
|
w.build("all", "phony", all)
|
||||||
w.default("all")
|
w.default("all")
|
||||||
return all
|
return all
|
||||||
|
|
||||||
|
|
||||||
def gen(out: TextIO, target: model.Target, registry: model.Registry):
|
def applyExtraProps(scope: TargetScope, name: str, var: list[str]) -> list[str]:
|
||||||
|
target: model.Target = scope.target
|
||||||
|
extra = target.props.get(f"ck-{name}-extra", None)
|
||||||
|
if extra:
|
||||||
|
var += extra.split(" ")
|
||||||
|
override = target.props.get(f"ck-{name}-override")
|
||||||
|
if override:
|
||||||
|
var = override.split(" ")
|
||||||
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
def gen(out: TextIO, scope: TargetScope):
|
||||||
w = ninja.Writer(out)
|
w = ninja.Writer(out)
|
||||||
|
target: model.Target = scope.target
|
||||||
|
|
||||||
w.comment("File generated by the build system, do not edit")
|
w.comment("File generated by the build system, do not edit")
|
||||||
w.newline()
|
w.newline()
|
||||||
|
|
||||||
w.variable("builddir", target.builddir)
|
w.separator("Variables")
|
||||||
w.variable("hashid", target.hashid)
|
for name, compute in _vars.items():
|
||||||
|
w.variable(name, applyExtraProps(scope, name, compute(scope)))
|
||||||
|
w.newline()
|
||||||
|
|
||||||
w.separator("Tools")
|
w.separator("Tools")
|
||||||
|
|
||||||
w.variable("cincs", " ".join(aggregateCincs(target, registry)))
|
for i in scope.target.tools:
|
||||||
w.variable("cdefs", " ".join(aggregateCdefs(target)))
|
tool = scope.target.tools[i]
|
||||||
w.newline()
|
|
||||||
|
|
||||||
for i in target.tools:
|
|
||||||
tool = target.tools[i]
|
|
||||||
rule = rules.rules[i]
|
rule = rules.rules[i]
|
||||||
w.variable(i, tool.cmd)
|
w.variable(i, tool.cmd)
|
||||||
w.variable(i + "flags", " ".join(rule.args + tool.args))
|
w.variable(
|
||||||
|
i + "flags",
|
||||||
|
" ".join(applyExtraProps(scope, i + "flags", rule.args + tool.args)),
|
||||||
|
)
|
||||||
w.rule(
|
w.rule(
|
||||||
i,
|
i,
|
||||||
f"{tool.cmd} {rule.rule.replace('$flags',f'${i}flags')}",
|
f"{tool.cmd} {(tool.rule or rule.rule).replace('$flags',f'${i}flags')}",
|
||||||
|
description=f"{vt100.BLUE}$ck_target{vt100.RESET}/{vt100.CYAN}$ck_component{vt100.RESET}: {vt100.YELLOW}{i} {vt100.FAINT + vt100.WHITE}$out...{vt100.RESET}",
|
||||||
depfile=rule.deps,
|
depfile=rule.deps,
|
||||||
)
|
)
|
||||||
w.newline()
|
w.newline()
|
||||||
|
|
||||||
w.separator("Build")
|
w.separator("Build")
|
||||||
|
|
||||||
all(w, registry, target)
|
all(w, scope)
|
||||||
|
|
||||||
|
|
||||||
@dt.dataclass
|
|
||||||
class Product:
|
|
||||||
path: Path
|
|
||||||
target: model.Target
|
|
||||||
component: model.Component
|
|
||||||
|
|
||||||
|
|
||||||
def build(
|
def build(
|
||||||
target: model.Target,
|
scope: TargetScope,
|
||||||
registry: model.Registry,
|
components: Union[list[model.Component], model.Component, Literal["all"]] = "all",
|
||||||
components: Union[list[model.Component], model.Component, None] = None,
|
) -> list[ProductScope]:
|
||||||
) -> list[Product]:
|
|
||||||
all = False
|
all = False
|
||||||
shell.mkdir(target.builddir)
|
shell.mkdir(scope.target.builddir)
|
||||||
ninjaPath = os.path.join(target.builddir, "build.ninja")
|
ninjaPath = os.path.join(scope.target.builddir, "build.ninja")
|
||||||
with open(ninjaPath, "w") as f:
|
|
||||||
gen(f, target, registry)
|
|
||||||
|
|
||||||
if components is None:
|
with open(ninjaPath, "w") as f:
|
||||||
|
gen(f, scope)
|
||||||
|
|
||||||
|
if components == "all":
|
||||||
all = True
|
all = True
|
||||||
components = list(registry.iterEnabled(target))
|
components = list(scope.registry.iterEnabled(scope.target))
|
||||||
|
|
||||||
if isinstance(components, model.Component):
|
if isinstance(components, model.Component):
|
||||||
components = [components]
|
components = [components]
|
||||||
|
|
||||||
products: list[Product] = []
|
products: list[ProductScope] = []
|
||||||
for c in components:
|
for c in components:
|
||||||
r = c.resolved[target.id]
|
s = scope.openComponentScope(c)
|
||||||
|
r = c.resolved[scope.target.id]
|
||||||
if not r.enabled:
|
if not r.enabled:
|
||||||
raise RuntimeError(f"Component {c.id} is disabled: {r.reason}")
|
raise RuntimeError(f"Component {c.id} is disabled: {r.reason}")
|
||||||
|
|
||||||
products.append(
|
products.append(s.openProductScope(Path(outfile(scope.openComponentScope(c)))))
|
||||||
Product(
|
|
||||||
path=Path(outfile(target, c)),
|
|
||||||
target=target,
|
|
||||||
component=c,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
outs = list(map(lambda p: str(p.path), products))
|
outs = list(map(lambda p: str(p.path), products))
|
||||||
if all:
|
|
||||||
shell.exec("ninja", "-f", ninjaPath)
|
shell.exec("ninja", "-f", ninjaPath, *(outs if not all else []))
|
||||||
else:
|
|
||||||
shell.exec("ninja", "-f", ninjaPath, *outs)
|
|
||||||
return products
|
return products
|
||||||
|
|
||||||
|
|
||||||
# --- Commands --------------------------------------------------------------- #
|
# --- Commands --------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
@cli.command("b", "build", "Build a component or all components")
|
@cli.command("b", "builder", "Build/Run/Clean a component or all components")
|
||||||
def buildCmd(args: cli.Args):
|
def _(args: cli.Args):
|
||||||
registry = model.Registry.use(args)
|
pass
|
||||||
target = model.Target.use(args)
|
|
||||||
|
|
||||||
|
@cli.command("b", "builder/build", "Build a component or all components")
|
||||||
|
def _(args: cli.Args):
|
||||||
|
scope = TargetScope.use(args)
|
||||||
componentSpec = args.consumeArg()
|
componentSpec = args.consumeArg()
|
||||||
if componentSpec is None:
|
component = None
|
||||||
raise RuntimeError("No component specified")
|
if componentSpec is not None:
|
||||||
component = registry.lookup(componentSpec, model.Component)
|
component = scope.registry.lookup(componentSpec, model.Component)
|
||||||
build(target, registry, component)[0]
|
build(scope, component if component is not None else "all")[0]
|
||||||
|
|
||||||
|
|
||||||
@cli.command("r", "run", "Run a component")
|
@cli.command("r", "builder/run", "Run a component")
|
||||||
def runCmd(args: cli.Args):
|
def runCmd(args: cli.Args):
|
||||||
registry = model.Registry.use(args)
|
|
||||||
target = model.Target.use(args)
|
|
||||||
debug = args.consumeOpt("debug", False) is True
|
debug = args.consumeOpt("debug", False) is True
|
||||||
|
wait = args.consumeOpt("wait", False) is True
|
||||||
|
debugger = str(args.consumeOpt("debugger", "lldb"))
|
||||||
|
|
||||||
|
profile = args.consumeOpt("profile", False) is True
|
||||||
|
what = str(args.consumeOpt("what", "cpu"))
|
||||||
|
rate = int(args.consumeOpt("rate", 1000))
|
||||||
|
|
||||||
componentSpec = args.consumeArg() or "__main__"
|
componentSpec = args.consumeArg() or "__main__"
|
||||||
component = registry.lookup(componentSpec, model.Component, includeProvides=True)
|
scope = TargetScope.use(args, {"debug": debug})
|
||||||
|
|
||||||
|
component = scope.registry.lookup(
|
||||||
|
componentSpec, model.Component, includeProvides=True
|
||||||
|
)
|
||||||
if component is None:
|
if component is None:
|
||||||
raise RuntimeError(f"Component {componentSpec} not found")
|
raise RuntimeError(f"Component {componentSpec} not found")
|
||||||
|
|
||||||
product = build(target, registry, component)[0]
|
product = build(scope, component)[0]
|
||||||
|
|
||||||
os.environ["CK_TARGET"] = target.id
|
os.environ["CK_TARGET"] = product.target.id
|
||||||
|
os.environ["CK_BUILDDIR"] = product.target.builddir
|
||||||
os.environ["CK_COMPONENT"] = product.component.id
|
os.environ["CK_COMPONENT"] = product.component.id
|
||||||
os.environ["CK_BUILDDIR"] = target.builddir
|
|
||||||
|
|
||||||
shell.exec(*(["lldb", "-o", "run"] if debug else []), str(product.path), *args.args)
|
command = [str(product.path), *args.extra]
|
||||||
|
|
||||||
|
if debug:
|
||||||
|
shell.debug(command, debugger=debugger, wait=wait)
|
||||||
|
elif profile:
|
||||||
|
shell.profile(command, what=what, rate=rate)
|
||||||
|
else:
|
||||||
|
shell.exec(*command)
|
||||||
|
|
||||||
|
|
||||||
@cli.command("t", "test", "Run all test targets")
|
@cli.command("t", "builder/test", "Run all test targets")
|
||||||
def testCmd(args: cli.Args):
|
def _(args: cli.Args):
|
||||||
# This is just a wrapper around the `run` command that try
|
# This is just a wrapper around the `run` command that try
|
||||||
# to run a special hook component named __tests__.
|
# to run a special hook component named __tests__.
|
||||||
args.args.insert(0, "__tests__")
|
args.args.insert(0, "__tests__")
|
||||||
runCmd(args)
|
runCmd(args)
|
||||||
|
|
||||||
|
|
||||||
@cli.command("d", "debug", "Debug a component")
|
@cli.command("d", "builder/debug", "Debug a component")
|
||||||
def debugCmd(args: cli.Args):
|
def _(args: cli.Args):
|
||||||
# This is just a wrapper around the `run` command that
|
# This is just a wrapper around the `run` command that
|
||||||
# always enable debug mode.
|
# always enable debug mode.
|
||||||
args.opts["debug"] = True
|
args.opts["debug"] = True
|
||||||
runCmd(args)
|
runCmd(args)
|
||||||
|
|
||||||
|
|
||||||
@cli.command("c", "clean", "Clean build files")
|
@cli.command("c", "builder/clean", "Clean build files")
|
||||||
def cleanCmd(args: cli.Args):
|
def _(args: cli.Args):
|
||||||
model.Project.use(args)
|
model.Project.use(args)
|
||||||
shell.rmrf(const.BUILD_DIR)
|
shell.rmrf(const.BUILD_DIR)
|
||||||
|
|
||||||
|
|
||||||
@cli.command("n", "nuke", "Clean all build files and caches")
|
@cli.command("n", "builder/nuke", "Clean all build files and caches")
|
||||||
def nukeCmd(args: cli.Args):
|
def _(args: cli.Args):
|
||||||
model.Project.use(args)
|
model.Project.use(args)
|
||||||
shell.rmrf(const.PROJECT_CK_DIR)
|
shell.rmrf(const.PROJECT_CK_DIR)
|
||||||
|
|
|
@ -16,10 +16,12 @@ _logger = logging.getLogger(__name__)
|
||||||
class Args:
|
class Args:
|
||||||
opts: dict[str, Value]
|
opts: dict[str, Value]
|
||||||
args: list[str]
|
args: list[str]
|
||||||
|
extra: list[str]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.opts = {}
|
self.opts = {}
|
||||||
self.args = []
|
self.args = []
|
||||||
|
self.extra = []
|
||||||
|
|
||||||
def consumePrefix(self, prefix: str) -> dict[str, Value]:
|
def consumePrefix(self, prefix: str) -> dict[str, Value]:
|
||||||
result: dict[str, Value] = {}
|
result: dict[str, Value] = {}
|
||||||
|
@ -56,13 +58,17 @@ class Args:
|
||||||
def parse(args: list[str]) -> Args:
|
def parse(args: list[str]) -> Args:
|
||||||
result = Args()
|
result = Args()
|
||||||
|
|
||||||
for arg in args:
|
for i in range(len(args)):
|
||||||
if arg.startswith("--"):
|
arg = args[i]
|
||||||
|
if arg.startswith("--") and not arg == "--":
|
||||||
if "=" in arg:
|
if "=" in arg:
|
||||||
key, value = arg[2:].split("=", 1)
|
key, value = arg[2:].split("=", 1)
|
||||||
result.opts[key] = value
|
result.opts[key] = value
|
||||||
else:
|
else:
|
||||||
result.opts[arg[2:]] = True
|
result.opts[arg[2:]] = True
|
||||||
|
elif arg == "--":
|
||||||
|
result.extra += args[i + 1 :]
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
result.args.append(arg)
|
result.args.append(arg)
|
||||||
|
|
||||||
|
@ -80,8 +86,10 @@ class Command:
|
||||||
isPlugin: bool
|
isPlugin: bool
|
||||||
callback: Callback
|
callback: Callback
|
||||||
|
|
||||||
|
subcommands: dict[str, "Command"] = dt.field(default_factory=dict)
|
||||||
|
|
||||||
commands: list[Command] = []
|
|
||||||
|
commands: dict[str, Command] = {}
|
||||||
|
|
||||||
|
|
||||||
def command(shortName: Optional[str], longName: str, helpText: str):
|
def command(shortName: Optional[str], longName: str, helpText: str):
|
||||||
|
@ -90,15 +98,18 @@ def command(shortName: Optional[str], longName: str, helpText: str):
|
||||||
|
|
||||||
def wrap(fn: Callable[[Args], None]):
|
def wrap(fn: Callable[[Args], None]):
|
||||||
_logger.debug(f"Registering command {longName}")
|
_logger.debug(f"Registering command {longName}")
|
||||||
commands.append(
|
path = longName.split("/")
|
||||||
Command(
|
parent = commands
|
||||||
shortName,
|
for p in path[:-1]:
|
||||||
longName,
|
parent = parent[p].subcommands
|
||||||
helpText,
|
parent[path[-1]] = Command(
|
||||||
Path(calframe[1].filename).parent != Path(__file__).parent,
|
shortName,
|
||||||
fn,
|
path[-1],
|
||||||
)
|
helpText,
|
||||||
|
Path(calframe[1].filename).parent != Path(__file__).parent,
|
||||||
|
fn,
|
||||||
)
|
)
|
||||||
|
|
||||||
return fn
|
return fn
|
||||||
|
|
||||||
return wrap
|
return wrap
|
||||||
|
@ -116,6 +127,28 @@ def error(msg: str) -> None:
|
||||||
print(f"{vt100.RED}Error:{vt100.RESET} {msg}\n", file=sys.stderr)
|
print(f"{vt100.RED}Error:{vt100.RESET} {msg}\n", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def warning(msg: str) -> None:
|
||||||
|
print(f"{vt100.YELLOW}Warning:{vt100.RESET} {msg}\n", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def ask(msg: str, default: Optional[bool] = None) -> bool:
|
||||||
|
if default is None:
|
||||||
|
msg = f"{msg} [y/n] "
|
||||||
|
elif default:
|
||||||
|
msg = f"{msg} [Y/n] "
|
||||||
|
else:
|
||||||
|
msg = f"{msg} [y/N] "
|
||||||
|
|
||||||
|
while True:
|
||||||
|
result = input(msg).lower()
|
||||||
|
if result in ("y", "yes"):
|
||||||
|
return True
|
||||||
|
elif result in ("n", "no"):
|
||||||
|
return False
|
||||||
|
elif result == "" and default is not None:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
@command("h", "help", "Show this help message")
|
@command("h", "help", "Show this help message")
|
||||||
def helpCmd(args: Args):
|
def helpCmd(args: Args):
|
||||||
usage()
|
usage()
|
||||||
|
@ -127,8 +160,8 @@ def helpCmd(args: Args):
|
||||||
|
|
||||||
print()
|
print()
|
||||||
vt100.title("Commands")
|
vt100.title("Commands")
|
||||||
for cmd in sorted(commands, key=lambda c: c.longName):
|
for cmd in sorted(commands.values(), key=lambda c: c.longName):
|
||||||
if cmd.longName.startswith("_"):
|
if cmd.longName.startswith("_") or len(cmd.subcommands) > 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
pluginText = ""
|
pluginText = ""
|
||||||
|
@ -139,6 +172,21 @@ def helpCmd(args: Args):
|
||||||
f" {vt100.GREEN}{cmd.shortName or ' '}{vt100.RESET} {cmd.longName} - {cmd.helpText} {pluginText}"
|
f" {vt100.GREEN}{cmd.shortName or ' '}{vt100.RESET} {cmd.longName} - {cmd.helpText} {pluginText}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for cmd in sorted(commands.values(), key=lambda c: c.longName):
|
||||||
|
if cmd.longName.startswith("_") or len(cmd.subcommands) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
print()
|
||||||
|
vt100.title(f"{cmd.longName.capitalize()} - {cmd.helpText}")
|
||||||
|
for subcmd in sorted(cmd.subcommands.values(), key=lambda c: c.longName):
|
||||||
|
pluginText = ""
|
||||||
|
if subcmd.isPlugin:
|
||||||
|
pluginText = f"{vt100.CYAN}(plugin){vt100.RESET}"
|
||||||
|
|
||||||
|
print(
|
||||||
|
f" {vt100.GREEN}{subcmd.shortName or ' '}{vt100.RESET} {subcmd.longName} - {subcmd.helpText} {pluginText}"
|
||||||
|
)
|
||||||
|
|
||||||
print()
|
print()
|
||||||
vt100.title("Logging")
|
vt100.title("Logging")
|
||||||
print(" Logs are stored in:")
|
print(" Logs are stored in:")
|
||||||
|
@ -151,15 +199,19 @@ def versionCmd(args: Args):
|
||||||
print(f"CuteKit v{const.VERSION_STR}")
|
print(f"CuteKit v{const.VERSION_STR}")
|
||||||
|
|
||||||
|
|
||||||
def exec(args: Args):
|
def exec(args: Args, cmds=commands):
|
||||||
cmd = args.consumeArg()
|
cmd = args.consumeArg()
|
||||||
|
|
||||||
if cmd is None:
|
if cmd is None:
|
||||||
raise RuntimeError("No command specified")
|
raise RuntimeError("No command specified")
|
||||||
|
|
||||||
for c in commands:
|
for c in cmds.values():
|
||||||
if c.shortName == cmd or c.longName == cmd:
|
if c.shortName == cmd or c.longName == cmd:
|
||||||
c.callback(args)
|
if len(c.subcommands) > 0:
|
||||||
return
|
exec(args, c.subcommands)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
c.callback(args)
|
||||||
|
return
|
||||||
|
|
||||||
raise RuntimeError(f"Unknown command {cmd}")
|
raise RuntimeError(f"Unknown command {cmd}")
|
||||||
|
|
572
cutekit/cli2.py
Normal file
572
cutekit/cli2.py
Normal file
|
@ -0,0 +1,572 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
import typing as tp
|
||||||
|
import dataclasses as dt
|
||||||
|
|
||||||
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
|
|
||||||
|
T = tp.TypeVar("T")
|
||||||
|
|
||||||
|
# --- Scan -------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
class Scan:
|
||||||
|
_src: str
|
||||||
|
_off: int
|
||||||
|
_save: list[int]
|
||||||
|
|
||||||
|
def __init__(self, src: str, off: int = 0):
|
||||||
|
self._src = src
|
||||||
|
self._off = 0
|
||||||
|
self._save = []
|
||||||
|
|
||||||
|
def curr(self) -> str:
|
||||||
|
if self.eof():
|
||||||
|
return "\0"
|
||||||
|
return self._src[self._off]
|
||||||
|
|
||||||
|
def next(self) -> str:
|
||||||
|
if self.eof():
|
||||||
|
return "\0"
|
||||||
|
|
||||||
|
self._off += 1
|
||||||
|
return self.curr()
|
||||||
|
|
||||||
|
def peek(self, off: int = 1) -> str:
|
||||||
|
if self._off + off >= len(self._src):
|
||||||
|
return "\0"
|
||||||
|
|
||||||
|
return self._src[self._off + off]
|
||||||
|
|
||||||
|
def eof(self) -> bool:
|
||||||
|
return self._off >= len(self._src)
|
||||||
|
|
||||||
|
def skipStr(self, s: str) -> bool:
|
||||||
|
if self._src[self._off :].startswith(s):
|
||||||
|
self._off += len(s)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def isStr(self, s: str) -> bool:
|
||||||
|
self.save()
|
||||||
|
if self.skipStr(s):
|
||||||
|
self.restore()
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.restore()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def save(self) -> None:
|
||||||
|
self._save.append(self._off)
|
||||||
|
|
||||||
|
def restore(self) -> None:
|
||||||
|
self._off = self._save.pop()
|
||||||
|
|
||||||
|
def skipWhitespace(self) -> bool:
|
||||||
|
result = False
|
||||||
|
while not self.eof() and self.curr().isspace():
|
||||||
|
self.next()
|
||||||
|
result = True
|
||||||
|
return result
|
||||||
|
|
||||||
|
def skipSeparator(self, sep: str) -> bool:
|
||||||
|
self.save()
|
||||||
|
self.skipWhitespace()
|
||||||
|
if self.skipStr(sep):
|
||||||
|
self.skipWhitespace()
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.restore()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def isSeparator(self, sep: str) -> bool:
|
||||||
|
self.save()
|
||||||
|
self.skipWhitespace()
|
||||||
|
if self.skipStr(sep):
|
||||||
|
self.skipWhitespace()
|
||||||
|
self.restore()
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.restore()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def skipKeyword(self, keyword: str) -> bool:
|
||||||
|
self.save()
|
||||||
|
self.skipWhitespace()
|
||||||
|
if self.skipStr(keyword) and not self.curr().isalnum():
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.restore()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def isKeyword(self, keyword: str) -> bool:
|
||||||
|
self.save()
|
||||||
|
self.skipWhitespace()
|
||||||
|
if self.skipStr(keyword) and not self.curr().isalnum():
|
||||||
|
self.restore()
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.restore()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# --- Parser ------------------------------------------------------------ #
|
||||||
|
|
||||||
|
PrimitiveValue = str | bool | int
|
||||||
|
Object = dict[str, PrimitiveValue]
|
||||||
|
List = list[PrimitiveValue]
|
||||||
|
Value = str | bool | int | Object | List
|
||||||
|
|
||||||
|
|
||||||
|
@dt.dataclass
|
||||||
|
class Token:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dt.dataclass
|
||||||
|
class ArgumentToken(Token):
|
||||||
|
key: str
|
||||||
|
subkey: Optional[str]
|
||||||
|
value: Value
|
||||||
|
short: bool
|
||||||
|
|
||||||
|
|
||||||
|
@dt.dataclass
|
||||||
|
class OperandToken(Token):
|
||||||
|
value: str
|
||||||
|
|
||||||
|
|
||||||
|
@dt.dataclass
|
||||||
|
class ExtraToken(Token):
|
||||||
|
args: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
def _parseIdent(s: Scan) -> str:
|
||||||
|
res = ""
|
||||||
|
while not s.eof() and (s.curr().isalnum() or s.curr() in "_-+"):
|
||||||
|
res += s.curr()
|
||||||
|
s.next()
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def _parseUntilComma(s: Scan) -> str:
|
||||||
|
res = ""
|
||||||
|
while not s.eof() and s.curr() != ",":
|
||||||
|
res += s.curr()
|
||||||
|
s.next()
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def _expectIdent(s: Scan) -> str:
|
||||||
|
res = _parseIdent(s)
|
||||||
|
if len(res) == 0:
|
||||||
|
raise RuntimeError("Expected identifier")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def _parseString(s: Scan, quote: str) -> str:
|
||||||
|
s.skipStr(quote)
|
||||||
|
res = ""
|
||||||
|
escaped = False
|
||||||
|
while not s.eof():
|
||||||
|
c = s.curr()
|
||||||
|
if escaped:
|
||||||
|
res += c
|
||||||
|
escaped = False
|
||||||
|
elif c == "\\":
|
||||||
|
escaped = True
|
||||||
|
elif c == quote:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
res += c
|
||||||
|
s.next()
|
||||||
|
if not s.skipStr(quote):
|
||||||
|
raise RuntimeError("Unterminated string")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def _tryParseInt(ident) -> Optional[int]:
|
||||||
|
try:
|
||||||
|
return int(ident)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _parsePrimitive(s: Scan) -> PrimitiveValue:
|
||||||
|
if s.curr() == '"':
|
||||||
|
return _parseString(s, '"')
|
||||||
|
elif s.curr() == "'":
|
||||||
|
return _parseString(s, "'")
|
||||||
|
else:
|
||||||
|
ident = _parseUntilComma(s)
|
||||||
|
|
||||||
|
if ident in ("true", "True", "y", "yes", "Y", "Yes"):
|
||||||
|
return True
|
||||||
|
elif ident in ("false", "False", "n", "no", "N", "No"):
|
||||||
|
return False
|
||||||
|
elif n := _tryParseInt(ident):
|
||||||
|
return n
|
||||||
|
else:
|
||||||
|
return ident
|
||||||
|
|
||||||
|
|
||||||
|
def _parseValue(s: Scan) -> Value:
|
||||||
|
lhs = _parsePrimitive(s)
|
||||||
|
if s.eof():
|
||||||
|
return lhs
|
||||||
|
values: List = [lhs]
|
||||||
|
while not s.eof() and s.skipStr(","):
|
||||||
|
values.append(_parsePrimitive(s))
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def parseValue(s: str) -> Value:
|
||||||
|
return _parseValue(Scan(s))
|
||||||
|
|
||||||
|
|
||||||
|
def parseArg(arg: str) -> list[Token]:
|
||||||
|
s = Scan(arg)
|
||||||
|
if s.skipStr("--"):
|
||||||
|
key = _expectIdent(s)
|
||||||
|
subkey = ""
|
||||||
|
if s.skipStr(":"):
|
||||||
|
subkey = _expectIdent(s)
|
||||||
|
if s.skipStr("="):
|
||||||
|
value = _parseValue(s)
|
||||||
|
else:
|
||||||
|
value = True
|
||||||
|
return [ArgumentToken(key, subkey, value, False)]
|
||||||
|
elif s.skipStr("-"):
|
||||||
|
res = []
|
||||||
|
while not s.eof():
|
||||||
|
key = s.curr()
|
||||||
|
if not key.isalnum():
|
||||||
|
raise RuntimeError("Expected alphanumeric")
|
||||||
|
s.next()
|
||||||
|
res.append(ArgumentToken(key, None, True, True))
|
||||||
|
return res
|
||||||
|
else:
|
||||||
|
return [OperandToken(arg)]
|
||||||
|
|
||||||
|
|
||||||
|
def parseArgs(args: list[str]) -> list[Token]:
|
||||||
|
res: list[Token] = []
|
||||||
|
while len(args) > 0:
|
||||||
|
arg = args.pop(0)
|
||||||
|
if arg == "--":
|
||||||
|
res.append(ExtraToken(args))
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
res.extend(parseArg(arg))
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
# --- Schema ----------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
class FieldKind(Enum):
|
||||||
|
FLAG = 0
|
||||||
|
OPERAND = 1
|
||||||
|
EXTRA = 2
|
||||||
|
|
||||||
|
|
||||||
|
@dt.dataclass
|
||||||
|
class Field:
|
||||||
|
kind: FieldKind
|
||||||
|
shortName: Optional[str]
|
||||||
|
longName: str
|
||||||
|
description: str = ""
|
||||||
|
default: Any = None
|
||||||
|
|
||||||
|
_fieldName: str | None = dt.field(init=False, default=None)
|
||||||
|
_fieldType: type | None = dt.field(init=False, default=None)
|
||||||
|
|
||||||
|
def bind(self, typ: type, name: str):
|
||||||
|
self._fieldName = name
|
||||||
|
self._fieldType = typ.__annotations__[name]
|
||||||
|
if self.longName is None:
|
||||||
|
self.longName = name
|
||||||
|
|
||||||
|
def defaultValue(self) -> Any:
|
||||||
|
if self._fieldType is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self.default is not None:
|
||||||
|
return self.default
|
||||||
|
|
||||||
|
if self._fieldType == bool:
|
||||||
|
return False
|
||||||
|
elif self._fieldType == int:
|
||||||
|
return 0
|
||||||
|
elif self._fieldType == str:
|
||||||
|
return ""
|
||||||
|
elif self._fieldType == list:
|
||||||
|
return []
|
||||||
|
elif self._fieldType == dict:
|
||||||
|
return {}
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def setDefault(self, obj: Any):
|
||||||
|
if self._fieldName:
|
||||||
|
setattr(obj, self._fieldName, self.default)
|
||||||
|
|
||||||
|
def putValue(self, obj: Any, value: Any, subkey: Optional[str] = None):
|
||||||
|
if self._fieldName is None:
|
||||||
|
raise ValueError("Field type is not defined")
|
||||||
|
setattr(obj, self._fieldName, value)
|
||||||
|
|
||||||
|
def getAttr(self, obj: Any) -> Any:
|
||||||
|
if self._fieldName is None:
|
||||||
|
raise ValueError("Field name is not defined")
|
||||||
|
return getattr(obj, self._fieldName)
|
||||||
|
|
||||||
|
|
||||||
|
def arg(
|
||||||
|
shortName: str | None = None,
|
||||||
|
longName: str = "",
|
||||||
|
description: str = "",
|
||||||
|
default: Any = None,
|
||||||
|
) -> Any:
|
||||||
|
return Field(FieldKind.FLAG, shortName, longName, description, default)
|
||||||
|
|
||||||
|
|
||||||
|
def operand(longName: str = "", description: str = "") -> Any:
|
||||||
|
return Field(FieldKind.OPERAND, None, longName, description)
|
||||||
|
|
||||||
|
|
||||||
|
def extra(longName: str = "", description: str = "") -> Any:
|
||||||
|
return Field(FieldKind.EXTRA, None, longName, description)
|
||||||
|
|
||||||
|
|
||||||
|
@dt.dataclass
|
||||||
|
class Schema:
|
||||||
|
typ: Optional[type] = None
|
||||||
|
args: list[Field] = dt.field(default_factory=list)
|
||||||
|
operands: list[Field] = dt.field(default_factory=list)
|
||||||
|
extras: Optional[Field] = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def extract(typ: type) -> "Schema":
|
||||||
|
s = Schema(typ)
|
||||||
|
|
||||||
|
for f in typ.__annotations__.keys():
|
||||||
|
field = getattr(typ, f, None)
|
||||||
|
|
||||||
|
if field is None:
|
||||||
|
raise ValueError(f"Field '{f}' is not defined")
|
||||||
|
|
||||||
|
if not isinstance(field, Field):
|
||||||
|
raise ValueError(f"Field '{f}' is not a Field")
|
||||||
|
|
||||||
|
field.bind(typ, f)
|
||||||
|
|
||||||
|
if field.type == FieldKind.FLAG:
|
||||||
|
s.args.append(field)
|
||||||
|
elif field.type == FieldKind.OPERAND:
|
||||||
|
s.operands.append(field)
|
||||||
|
elif field.type == FieldKind.EXTRA:
|
||||||
|
if s.extras:
|
||||||
|
raise ValueError("Only one extra argument is allowed")
|
||||||
|
s.extras = field
|
||||||
|
|
||||||
|
return s
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def extractFromCallable(fn: tp.Callable) -> "Schema":
|
||||||
|
typ: type | None = (
|
||||||
|
None
|
||||||
|
if len(fn.__annotations__) == 0
|
||||||
|
else next(iter(fn.__annotations__.values()))
|
||||||
|
)
|
||||||
|
|
||||||
|
if typ is None:
|
||||||
|
return Schema()
|
||||||
|
|
||||||
|
return Schema.extract(typ)
|
||||||
|
|
||||||
|
def usage(self) -> str:
|
||||||
|
res = ""
|
||||||
|
for arg in self.args:
|
||||||
|
flag = ""
|
||||||
|
if arg.shortName:
|
||||||
|
flag += f"-{arg.shortName}"
|
||||||
|
|
||||||
|
if arg.longName:
|
||||||
|
if flag:
|
||||||
|
flag += ", "
|
||||||
|
flag += f"--{arg.longName}"
|
||||||
|
res += f"[{flag}] "
|
||||||
|
for operand in self.operands:
|
||||||
|
res += f"<{operand.longName}> "
|
||||||
|
if self.extras:
|
||||||
|
res += f"[-- {self.extras.longName}]"
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _lookupArg(self, key: str, short: bool) -> Field:
|
||||||
|
for arg in self.args:
|
||||||
|
if short and arg.shortName == key:
|
||||||
|
return arg
|
||||||
|
elif not short and arg.longName == key:
|
||||||
|
return arg
|
||||||
|
raise ValueError(f"Unknown argument '{key}'")
|
||||||
|
|
||||||
|
def _setOperand(self, tok: OperandToken):
|
||||||
|
return
|
||||||
|
|
||||||
|
def _instanciate(self) -> Any:
|
||||||
|
if self.typ is None:
|
||||||
|
return None
|
||||||
|
res = self.typ()
|
||||||
|
for arg in self.args:
|
||||||
|
arg.setDefault(res)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def parse(self, args: list[str]) -> Any:
|
||||||
|
res = self._instanciate()
|
||||||
|
if res is None:
|
||||||
|
if len(args) > 0:
|
||||||
|
raise ValueError("Unexpected arguments")
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
stack = args[:]
|
||||||
|
while len(stack) > 0:
|
||||||
|
if stack[0] == "--":
|
||||||
|
if not self.extras:
|
||||||
|
raise ValueError("Unexpected '--'")
|
||||||
|
self._setExtra(res, stack.pop(0))
|
||||||
|
break
|
||||||
|
|
||||||
|
toks = parseArg(stack.pop(0))
|
||||||
|
for tok in toks:
|
||||||
|
if isinstance(tok, ArgumentToken):
|
||||||
|
arg = self._lookupArg(tok.key, tok.short)
|
||||||
|
arg.putValue(res, tok.value, tok.subkey)
|
||||||
|
elif isinstance(tok, OperandToken):
|
||||||
|
self._setOperand(tok)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unexpected token: {type(tok)}")
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@dt.dataclass
|
||||||
|
class Command:
|
||||||
|
shortName: Optional[str]
|
||||||
|
longName: str
|
||||||
|
description: str = ""
|
||||||
|
schema: Optional[Schema] = None
|
||||||
|
callable: Optional[tp.Callable] = None
|
||||||
|
subcommands: dict[str, "Command"] = dt.field(default_factory=dict)
|
||||||
|
populated: bool = False
|
||||||
|
|
||||||
|
def _spliceArgs(self, args: list[str]) -> tuple[list[str], list[str]]:
|
||||||
|
rest = args[:]
|
||||||
|
curr = []
|
||||||
|
if len(self.subcommands) > 0:
|
||||||
|
while len(rest) > 0 and rest[0].startswith("-") and rest[0] != "--":
|
||||||
|
curr.append(rest.pop(0))
|
||||||
|
else:
|
||||||
|
curr = rest
|
||||||
|
return curr, rest
|
||||||
|
|
||||||
|
def help(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def usage(self) -> str:
|
||||||
|
res = " "
|
||||||
|
if self.schema:
|
||||||
|
res += self.schema.usage()
|
||||||
|
|
||||||
|
if len(self.subcommands) == 1:
|
||||||
|
sub = next(iter(self.subcommands.values()))
|
||||||
|
res += sub.longName + sub.usage()
|
||||||
|
elif len(self.subcommands) > 0:
|
||||||
|
res += "{"
|
||||||
|
first = True
|
||||||
|
for name, cmd in self.subcommands.items():
|
||||||
|
if not first:
|
||||||
|
res += " | "
|
||||||
|
else:
|
||||||
|
res += f"{name}"
|
||||||
|
res += "}"
|
||||||
|
|
||||||
|
res += "[args...]"
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
def eval(self, args: list[str]):
|
||||||
|
cmd = args.pop(0)
|
||||||
|
curr, rest = self._spliceArgs(args)
|
||||||
|
if "-h" in curr or "--help" in curr:
|
||||||
|
self.help()
|
||||||
|
return
|
||||||
|
if "-u" in curr or "--usage" in curr:
|
||||||
|
print("Usage: " + cmd + self.usage(), end="\n\n")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.schema and self.callable:
|
||||||
|
args = self.schema.parse(curr)
|
||||||
|
self.callable(args)
|
||||||
|
elif self.callable:
|
||||||
|
self.callable()
|
||||||
|
|
||||||
|
if self.subcommands:
|
||||||
|
if len(rest) == 0 and not self.populated:
|
||||||
|
raise ValueError("Expected subcommand")
|
||||||
|
elif rest[0] in self.subcommands:
|
||||||
|
print("eval", rest[0])
|
||||||
|
self.subcommands[rest[0]].eval(rest)
|
||||||
|
elif len(rest) > 0:
|
||||||
|
raise ValueError(f"Unknown subcommand '{rest[0]}'")
|
||||||
|
elif len(rest) > 0:
|
||||||
|
raise ValueError(f"Unknown operand '{rest[0]}'")
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
print("Error: " + str(e))
|
||||||
|
print("Usage: " + cmd + self.usage(), end="\n\n")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
_root = Command(None, sys.argv[0])
|
||||||
|
|
||||||
|
|
||||||
|
def _splitPath(path: str) -> list[str]:
|
||||||
|
if path == "/":
|
||||||
|
return []
|
||||||
|
return path.split("/")
|
||||||
|
|
||||||
|
|
||||||
|
def _resolvePath(path: list[str]) -> Command:
|
||||||
|
if path == "/":
|
||||||
|
return _root
|
||||||
|
cmd = _root
|
||||||
|
for name in path:
|
||||||
|
if name not in cmd.subcommands:
|
||||||
|
cmd.subcommands[name] = Command(None, name)
|
||||||
|
cmd = cmd.subcommands[name]
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
|
def command(shortName: str, longName: str, description: str = "") -> Callable:
|
||||||
|
def wrap(fn: Callable):
|
||||||
|
schema = Schema.extractFromCallable(fn)
|
||||||
|
path = _splitPath(longName)
|
||||||
|
cmd = _resolvePath(path)
|
||||||
|
if cmd.populated:
|
||||||
|
raise ValueError(f"Command '{longName}' is already defined")
|
||||||
|
cmd.shortName = shortName
|
||||||
|
cmd.longName = path[-1]
|
||||||
|
cmd.description = description
|
||||||
|
cmd.schema = schema
|
||||||
|
cmd.callable = fn
|
||||||
|
cmd.populated = True
|
||||||
|
return fn
|
||||||
|
|
||||||
|
return wrap
|
|
@ -1,7 +1,21 @@
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
VERSION = (0, 6, 0, "dev")
|
from . import utils
|
||||||
|
|
||||||
|
|
||||||
|
class Uninitialized:
|
||||||
|
def __repr__(self):
|
||||||
|
raise Exception("Uninitialized constant")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
raise Exception("Uninitialized constant")
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
raise Exception("Uninitialized constant")
|
||||||
|
|
||||||
|
|
||||||
|
VERSION = (0, 7, 0, "dev")
|
||||||
VERSION_STR = f"{VERSION[0]}.{VERSION[1]}.{VERSION[2]}{'-' + VERSION[3] if len(VERSION) >= 4 else ''}"
|
VERSION_STR = f"{VERSION[0]}.{VERSION[1]}.{VERSION[2]}{'-' + VERSION[3] if len(VERSION) >= 4 else ''}"
|
||||||
MODULE_DIR = os.path.dirname(os.path.realpath(__file__))
|
MODULE_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||||
ARGV0 = os.path.basename(sys.argv[0])
|
ARGV0 = os.path.basename(sys.argv[0])
|
||||||
|
@ -10,10 +24,24 @@ GLOBAL_CK_DIR = os.path.join(os.path.expanduser("~"), ".cutekit")
|
||||||
BUILD_DIR = os.path.join(PROJECT_CK_DIR, "build")
|
BUILD_DIR = os.path.join(PROJECT_CK_DIR, "build")
|
||||||
CACHE_DIR = os.path.join(PROJECT_CK_DIR, "cache")
|
CACHE_DIR = os.path.join(PROJECT_CK_DIR, "cache")
|
||||||
EXTERN_DIR = os.path.join(PROJECT_CK_DIR, "extern")
|
EXTERN_DIR = os.path.join(PROJECT_CK_DIR, "extern")
|
||||||
|
TMP_DIR = os.path.join(PROJECT_CK_DIR, "tmp")
|
||||||
SRC_DIR = "src"
|
SRC_DIR = "src"
|
||||||
META_DIR = "meta"
|
META_DIR = "meta"
|
||||||
TARGETS_DIR = os.path.join(META_DIR, "targets")
|
TARGETS_DIR = os.path.join(META_DIR, "targets")
|
||||||
DEFAULT_REPO_TEMPLATES = "cute-engineering/cutekit-templates"
|
DEFAULT_REPO_TEMPLATES = "cute-engineering/cutekit-templates"
|
||||||
DESCRIPTION = "A build system and package manager for low-level software development"
|
DESCRIPTION = "A build system and package manager for low-level software development"
|
||||||
PROJECT_LOG_FILE = os.path.join(PROJECT_CK_DIR, "cutekit.log")
|
PROJECT_LOG_FILE = os.path.join(PROJECT_CK_DIR, "cutekit.log")
|
||||||
GLOBAL_LOG_FILE = os.path.join(os.path.expanduser("~"), ".cutekit", "cutekit.log")
|
GLOBAL_LOG_FILE: str = os.path.join(os.path.expanduser("~"), ".cutekit", "cutekit.log")
|
||||||
|
HOSTID: str | Uninitialized = Uninitialized()
|
||||||
|
|
||||||
|
|
||||||
|
def setup():
|
||||||
|
global HOSTID
|
||||||
|
hostIdPath = GLOBAL_CK_DIR + "/hostid"
|
||||||
|
if os.path.exists(hostIdPath):
|
||||||
|
with open(hostIdPath, "r") as f:
|
||||||
|
HOSTID = f.read().strip()
|
||||||
|
else:
|
||||||
|
HOSTID = utils.randomHash()
|
||||||
|
with open(hostIdPath, "w") as f:
|
||||||
|
f.write(HOSTID)
|
||||||
|
|
44
cutekit/entrypoint.sh
Executable file
44
cutekit/entrypoint.sh
Executable file
|
@ -0,0 +1,44 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# This script makes sure that the virtual environment is
|
||||||
|
# set up and that the plugins requirements are installed.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ -z "$CUTEKIT_PYTHON" ]; then
|
||||||
|
if command -v python3.11 &> /dev/null; then
|
||||||
|
export CUTEKIT_PYTHON="python3.11"
|
||||||
|
else
|
||||||
|
export CUTEKIT_PYTHON="python3"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "/tools/venv" ]; then
|
||||||
|
echo "Creating virtual environment..."
|
||||||
|
|
||||||
|
$CUTEKIT_PYTHON -m venv /tools/venv
|
||||||
|
source /tools/venv/bin/activate
|
||||||
|
$CUTEKIT_PYTHON -m ensurepip
|
||||||
|
$CUTEKIT_PYTHON -m pip install -r /tools/cutekit/requirements.txt
|
||||||
|
|
||||||
|
echo "Installing plugins requirements..."
|
||||||
|
if [ -f "/project/meta/plugins/requirements.txt" ]; then
|
||||||
|
echo "Root plugin requirements found."
|
||||||
|
$CUTEKIT_PYTHON -m pip install -r /project/meta/plugins/requirements.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
for extern in /project/meta/externs/*; do
|
||||||
|
if [ -f "$extern/meta/plugins/requirements.txt" ]; then
|
||||||
|
echo "Plugin requirements found in $extern."
|
||||||
|
$CUTEKIT_PYTHON -m pip install -r "$extern/meta/plugins/requirements.txt"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Virtual environment created."
|
||||||
|
else
|
||||||
|
source /tools/venv/bin/activate
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd /project
|
||||||
|
export PYTHONPATH=/tools
|
||||||
|
$CUTEKIT_PYTHON -m cutekit $@
|
|
@ -36,7 +36,7 @@ def view(
|
||||||
if (
|
if (
|
||||||
scopeInstance is not None
|
scopeInstance is not None
|
||||||
and component.id != scope
|
and component.id != scope
|
||||||
and component.id not in scopeInstance.resolved[target.id].resolved
|
and component.id not in scopeInstance.resolved[target.id].required
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ def view(
|
||||||
|
|
||||||
g.node(
|
g.node(
|
||||||
component.id,
|
component.id,
|
||||||
f"<<B>{component.id}</B><BR/>{vt100.wordwrap(component.decription, 40,newline='<BR/>')}>",
|
f"<<B>{component.id}</B><BR/>{vt100.wordwrap(component.description, 40,newline='<BR/>')}>",
|
||||||
shape=shape,
|
shape=shape,
|
||||||
style="filled",
|
style="filled",
|
||||||
fillcolor=fillcolor,
|
fillcolor=fillcolor,
|
||||||
|
@ -67,7 +67,7 @@ def view(
|
||||||
elif showDisabled:
|
elif showDisabled:
|
||||||
g.node(
|
g.node(
|
||||||
component.id,
|
component.id,
|
||||||
f"<<B>{component.id}</B><BR/>{vt100.wordwrap(component.decription, 40,newline='<BR/>')}<BR/><BR/><I>{vt100.wordwrap(str(component.resolved[target.id].reason), 40,newline='<BR/>')}</I>>",
|
f"<<B>{component.id}</B><BR/>{vt100.wordwrap(component.description, 40,newline='<BR/>')}<BR/><BR/><I>{vt100.wordwrap(str(component.resolved[target.id].reason), 40,newline='<BR/>')}</I>>",
|
||||||
shape="plaintext",
|
shape="plaintext",
|
||||||
style="filled",
|
style="filled",
|
||||||
fontcolor="#999999",
|
fontcolor="#999999",
|
||||||
|
@ -84,7 +84,7 @@ def view(
|
||||||
|
|
||||||
|
|
||||||
@cli.command("g", "graph", "Show the dependency graph")
|
@cli.command("g", "graph", "Show the dependency graph")
|
||||||
def graphCmd(args: cli.Args):
|
def _(args: cli.Args):
|
||||||
registry = model.Registry.use(args)
|
registry = model.Registry.use(args)
|
||||||
target = model.Target.use(args)
|
target = model.Target.use(args)
|
||||||
|
|
||||||
|
|
|
@ -57,15 +57,33 @@ def makeMixinTune(tune: str) -> Mixin:
|
||||||
return mixinTune
|
return mixinTune
|
||||||
|
|
||||||
|
|
||||||
|
def combineMixins(*mixins: Mixin) -> Mixin:
|
||||||
|
def combined(target: model.Target, tools: model.Tools) -> model.Tools:
|
||||||
|
for mixin in mixins:
|
||||||
|
tools = mixin(target, tools)
|
||||||
|
return tools
|
||||||
|
|
||||||
|
return combined
|
||||||
|
|
||||||
|
|
||||||
mixins: dict[str, Mixin] = {
|
mixins: dict[str, Mixin] = {
|
||||||
"cache": mixinCache,
|
"cache": mixinCache,
|
||||||
"debug": mixinDebug,
|
"debug": mixinDebug,
|
||||||
"asan": makeMixinSan("address"),
|
"asan": combineMixins(makeMixinSan("address"), makeMixinSan("leak")),
|
||||||
"msan": makeMixinSan("memory"),
|
"msan": makeMixinSan("memory"),
|
||||||
"tsan": makeMixinSan("thread"),
|
"tsan": makeMixinSan("thread"),
|
||||||
"ubsan": makeMixinSan("undefined"),
|
"ubsan": makeMixinSan("undefined"),
|
||||||
|
"lsan": makeMixinSan("leak"),
|
||||||
|
"san": combineMixins(
|
||||||
|
makeMixinSan("address"),
|
||||||
|
makeMixinSan("undefined"),
|
||||||
|
makeMixinSan("leak"),
|
||||||
|
),
|
||||||
"tune": makeMixinTune("native"),
|
"tune": makeMixinTune("native"),
|
||||||
"fast": makeMixinOptimize("fast"),
|
"release": combineMixins(
|
||||||
|
makeMixinOptimize("3"),
|
||||||
|
makeMixinTune("native"),
|
||||||
|
),
|
||||||
"o3": makeMixinOptimize("3"),
|
"o3": makeMixinOptimize("3"),
|
||||||
"o2": makeMixinOptimize("2"),
|
"o2": makeMixinOptimize("2"),
|
||||||
"o1": makeMixinOptimize("1"),
|
"o1": makeMixinOptimize("1"),
|
||||||
|
|
104
cutekit/model.py
104
cutekit/model.py
|
@ -7,6 +7,7 @@ from enum import Enum
|
||||||
from typing import Any, Generator, Optional, Type, cast
|
from typing import Any, Generator, Optional, Type, cast
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from dataclasses_json import DataClassJsonMixin
|
from dataclasses_json import DataClassJsonMixin
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from cutekit import const, shell
|
from cutekit import const, shell
|
||||||
|
|
||||||
|
@ -71,7 +72,7 @@ class Manifest(DataClassJsonMixin):
|
||||||
"""
|
"""
|
||||||
Return the directory of the manifest
|
Return the directory of the manifest
|
||||||
"""
|
"""
|
||||||
return os.path.dirname(self.path)
|
return os.path.relpath(os.path.dirname(self.path), Path.cwd())
|
||||||
|
|
||||||
def subpath(self, path) -> Path:
|
def subpath(self, path) -> Path:
|
||||||
return Path(self.dirname()) / path
|
return Path(self.dirname()) / path
|
||||||
|
@ -96,6 +97,7 @@ _project: Optional["Project"] = None
|
||||||
class Extern(DataClassJsonMixin):
|
class Extern(DataClassJsonMixin):
|
||||||
git: str
|
git: str
|
||||||
tag: str
|
tag: str
|
||||||
|
deep: bool = dt.field(default=False)
|
||||||
|
|
||||||
|
|
||||||
@dt.dataclass
|
@dt.dataclass
|
||||||
|
@ -121,19 +123,18 @@ class Project(Manifest):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def ensure() -> "Project":
|
def ensure() -> "Project":
|
||||||
|
"""
|
||||||
|
Ensure that a project exists in the current directory or any parent directory
|
||||||
|
and chdir to the root of the project.
|
||||||
|
"""
|
||||||
project = Project.topmost()
|
project = Project.topmost()
|
||||||
if project is None:
|
if project is None:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"No project found in this directory or any parent directory"
|
"No project found in this directory or any parent directory"
|
||||||
)
|
)
|
||||||
|
os.chdir(project.dirname())
|
||||||
return project
|
return project
|
||||||
|
|
||||||
def chdir(self):
|
|
||||||
"""
|
|
||||||
Change the current working directory to the root of the project
|
|
||||||
"""
|
|
||||||
os.chdir(self.dirname())
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def at(path: Path) -> Optional["Project"]:
|
def at(path: Path) -> Optional["Project"]:
|
||||||
projectManifest = Manifest.tryLoad(path / "project")
|
projectManifest = Manifest.tryLoad(path / "project")
|
||||||
|
@ -151,17 +152,20 @@ class Project(Manifest):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
print(f"Installing {extSpec}-{ext.tag} from {ext.git}...")
|
print(f"Installing {extSpec}-{ext.tag} from {ext.git}...")
|
||||||
shell.popen(
|
cmd = [
|
||||||
"git",
|
"git",
|
||||||
"clone",
|
"clone",
|
||||||
"--quiet",
|
"--quiet",
|
||||||
"--depth",
|
|
||||||
"1",
|
|
||||||
"--branch",
|
"--branch",
|
||||||
ext.tag,
|
ext.tag,
|
||||||
ext.git,
|
ext.git,
|
||||||
extPath,
|
extPath,
|
||||||
)
|
]
|
||||||
|
|
||||||
|
if not ext.deep:
|
||||||
|
cmd += ["--depth", "1"]
|
||||||
|
|
||||||
|
shell.exec(*cmd, quiet=True)
|
||||||
project = Project.at(Path(extPath))
|
project = Project.at(Path(extPath))
|
||||||
if project is not None:
|
if project is not None:
|
||||||
Project.fetchs(project.extern)
|
Project.fetchs(project.extern)
|
||||||
|
@ -174,14 +178,19 @@ class Project(Manifest):
|
||||||
return _project
|
return _project
|
||||||
|
|
||||||
|
|
||||||
@cli.command("i", "install", "Install required external packages")
|
@cli.command("m", "model", "Manage the model")
|
||||||
def installCmd(args: cli.Args):
|
def _(args: cli.Args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("i", "model/install", "Install required external packages")
|
||||||
|
def _(args: cli.Args):
|
||||||
project = Project.use(args)
|
project = Project.use(args)
|
||||||
Project.fetchs(project.extern)
|
Project.fetchs(project.extern)
|
||||||
|
|
||||||
|
|
||||||
@cli.command("I", "init", "Initialize a new project")
|
@cli.command("I", "model/init", "Initialize a new project")
|
||||||
def initCmd(args: cli.Args):
|
def _(args: cli.Args):
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
repo = args.consumeOpt("repo", const.DEFAULT_REPO_TEMPLATES)
|
repo = args.consumeOpt("repo", const.DEFAULT_REPO_TEMPLATES)
|
||||||
|
@ -244,6 +253,7 @@ class Tool(DataClassJsonMixin):
|
||||||
cmd: str = dt.field(default="")
|
cmd: str = dt.field(default="")
|
||||||
args: list[str] = dt.field(default_factory=list)
|
args: list[str] = dt.field(default_factory=list)
|
||||||
files: list[str] = dt.field(default_factory=list)
|
files: list[str] = dt.field(default_factory=list)
|
||||||
|
rule: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
Tools = dict[str, Tool]
|
Tools = dict[str, Tool]
|
||||||
|
@ -259,17 +269,26 @@ class Target(Manifest):
|
||||||
tools: Tools = dt.field(default_factory=dict)
|
tools: Tools = dt.field(default_factory=dict)
|
||||||
routing: dict[str, str] = dt.field(default_factory=dict)
|
routing: dict[str, str] = dt.field(default_factory=dict)
|
||||||
|
|
||||||
|
_hashid: Optional[str] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hashid(self) -> str:
|
def hashid(self) -> str:
|
||||||
return utils.hash((self.props, [v.to_dict() for k, v in self.tools.items()]))
|
if self._hashid is None:
|
||||||
|
self._hashid = utils.hash(
|
||||||
|
(self.props, [v.to_dict() for k, v in self.tools.items()])
|
||||||
|
)
|
||||||
|
return self._hashid
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def builddir(self) -> str:
|
def builddir(self) -> str:
|
||||||
return os.path.join(const.BUILD_DIR, f"{self.id}-{self.hashid[:8]}")
|
postfix = f"-{self.hashid[:8]}"
|
||||||
|
if self.props.get("host"):
|
||||||
|
postfix += f"-{str(const.HOSTID)[:8]}"
|
||||||
|
return os.path.join(const.BUILD_DIR, f"{self.id}{postfix}")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def use(args: cli.Args) -> "Target":
|
def use(args: cli.Args, props: Props = {}) -> "Target":
|
||||||
registry = Registry.use(args)
|
registry = Registry.use(args, props)
|
||||||
targetSpec = str(args.consumeOpt("target", "host-" + shell.uname().machine))
|
targetSpec = str(args.consumeOpt("target", "host-" + shell.uname().machine))
|
||||||
return registry.ensure(targetSpec, Target)
|
return registry.ensure(targetSpec, Target)
|
||||||
|
|
||||||
|
@ -290,7 +309,8 @@ class Target(Manifest):
|
||||||
@dt.dataclass
|
@dt.dataclass
|
||||||
class Resolved:
|
class Resolved:
|
||||||
reason: Optional[str] = None
|
reason: Optional[str] = None
|
||||||
resolved: list[str] = dt.field(default_factory=list)
|
required: list[str] = dt.field(default_factory=list)
|
||||||
|
injected: list[str] = dt.field(default_factory=list)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def enabled(self) -> bool:
|
def enabled(self) -> bool:
|
||||||
|
@ -299,7 +319,7 @@ class Resolved:
|
||||||
|
|
||||||
@dt.dataclass
|
@dt.dataclass
|
||||||
class Component(Manifest):
|
class Component(Manifest):
|
||||||
decription: str = dt.field(default="(No description)")
|
description: str = dt.field(default="(No description)")
|
||||||
props: Props = dt.field(default_factory=dict)
|
props: Props = dt.field(default_factory=dict)
|
||||||
tools: Tools = dt.field(default_factory=dict)
|
tools: Tools = dt.field(default_factory=dict)
|
||||||
enableIf: dict[str, list[Any]] = dt.field(default_factory=dict)
|
enableIf: dict[str, list[Any]] = dt.field(default_factory=dict)
|
||||||
|
@ -437,11 +457,11 @@ class Resolver:
|
||||||
self._cache[keep] = Resolved(reason=reqResolved.reason)
|
self._cache[keep] = Resolved(reason=reqResolved.reason)
|
||||||
return self._cache[keep]
|
return self._cache[keep]
|
||||||
|
|
||||||
result.extend(reqResolved.resolved)
|
result.extend(reqResolved.required)
|
||||||
|
|
||||||
stack.pop()
|
stack.pop()
|
||||||
result.insert(0, keep)
|
result.insert(0, keep)
|
||||||
self._cache[keep] = Resolved(resolved=utils.uniq(result))
|
self._cache[keep] = Resolved(required=utils.uniqPreserveOrder(result))
|
||||||
return self._cache[keep]
|
return self._cache[keep]
|
||||||
|
|
||||||
|
|
||||||
|
@ -516,7 +536,7 @@ class Registry(DataClassJsonMixin):
|
||||||
return m
|
return m
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def use(args: cli.Args) -> "Registry":
|
def use(args: cli.Args, props: Props = {}) -> "Registry":
|
||||||
global _registry
|
global _registry
|
||||||
|
|
||||||
if _registry is not None:
|
if _registry is not None:
|
||||||
|
@ -526,8 +546,7 @@ class Registry(DataClassJsonMixin):
|
||||||
mixins = str(args.consumeOpt("mixins", "")).split(",")
|
mixins = str(args.consumeOpt("mixins", "")).split(",")
|
||||||
if mixins == [""]:
|
if mixins == [""]:
|
||||||
mixins = []
|
mixins = []
|
||||||
props = cast(dict[str, str], args.consumePrefix("prop:"))
|
props |= cast(dict[str, str], args.consumePrefix("prop:"))
|
||||||
|
|
||||||
_registry = Registry.load(project, mixins, props)
|
_registry = Registry.load(project, mixins, props)
|
||||||
return _registry
|
return _registry
|
||||||
|
|
||||||
|
@ -569,24 +588,33 @@ class Registry(DataClassJsonMixin):
|
||||||
# Resolve all dependencies for all targets
|
# Resolve all dependencies for all targets
|
||||||
for target in r.iter(Target):
|
for target in r.iter(Target):
|
||||||
target.props |= props
|
target.props |= props
|
||||||
resolver = Resolver(r, target)
|
|
||||||
|
|
||||||
# Apply injects
|
|
||||||
for c in r.iter(Component):
|
|
||||||
if c.isEnabled(target)[0]:
|
|
||||||
for inject in c.injects:
|
|
||||||
victim = r.lookup(inject, Component)
|
|
||||||
if not victim:
|
|
||||||
raise RuntimeError(f"Cannot find component '{inject}'")
|
|
||||||
victim.requires += [c.id]
|
|
||||||
|
|
||||||
# Resolve all components
|
# Resolve all components
|
||||||
|
resolver = Resolver(r, target)
|
||||||
for c in r.iter(Component):
|
for c in r.iter(Component):
|
||||||
resolved = resolver.resolve(c.id)
|
resolved = resolver.resolve(c.id)
|
||||||
if resolved.reason:
|
if resolved.reason:
|
||||||
_logger.info(f"Component '{c.id}' disabled: {resolved.reason}")
|
_logger.info(f"Component '{c.id}' disabled: {resolved.reason}")
|
||||||
c.resolved[target.id] = resolved
|
c.resolved[target.id] = resolved
|
||||||
|
|
||||||
|
# Apply injects
|
||||||
|
for c in r.iter(Component):
|
||||||
|
if c.resolved[target.id].enabled:
|
||||||
|
for inject in c.injects:
|
||||||
|
victim = r.lookup(inject, Component, includeProvides=True)
|
||||||
|
if not victim:
|
||||||
|
_logger.info(
|
||||||
|
f"Could not find component to inject '{inject}' with '{c.id}'"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
victim.resolved[target.id].injected.append(c.id)
|
||||||
|
victim.resolved[
|
||||||
|
target.id
|
||||||
|
].required = utils.uniqPreserveOrder(
|
||||||
|
c.resolved[target.id].required
|
||||||
|
+ victim.resolved[target.id].required
|
||||||
|
)
|
||||||
|
|
||||||
# Resolve tooling
|
# Resolve tooling
|
||||||
tools: Tools = target.tools
|
tools: Tools = target.tools
|
||||||
|
|
||||||
|
@ -610,8 +638,8 @@ class Registry(DataClassJsonMixin):
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
@cli.command("l", "list", "List all components and targets")
|
@cli.command("l", "model/list", "List all components and targets")
|
||||||
def listCmd(args: cli.Args):
|
def _(args: cli.Args):
|
||||||
registry = Registry.use(args)
|
registry = Registry.use(args)
|
||||||
|
|
||||||
components = list(registry.iter(Component))
|
components = list(registry.iter(Component))
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import os
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
from . import shell, model, const
|
from . import shell, model, const, cli
|
||||||
|
|
||||||
import importlib.util as importlib
|
import importlib.util as importlib
|
||||||
|
|
||||||
|
@ -14,10 +15,16 @@ def load(path: str):
|
||||||
|
|
||||||
if not spec or not spec.loader:
|
if not spec or not spec.loader:
|
||||||
_logger.error(f"Failed to load plugin {path}")
|
_logger.error(f"Failed to load plugin {path}")
|
||||||
return None
|
return
|
||||||
|
|
||||||
module = importlib.module_from_spec(spec)
|
module = importlib.module_from_spec(spec)
|
||||||
spec.loader.exec_module(module)
|
sys.modules["plugin"] = module
|
||||||
|
|
||||||
|
try:
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(f"Failed to load plugin {path}: {e}")
|
||||||
|
cli.warning(f"Plugin {path} loading skipped due to error")
|
||||||
|
|
||||||
|
|
||||||
def loadAll():
|
def loadAll():
|
||||||
|
@ -33,11 +40,17 @@ def loadAll():
|
||||||
|
|
||||||
for dirname in paths:
|
for dirname in paths:
|
||||||
pluginDir = os.path.join(project.dirname(), dirname, const.META_DIR, "plugins")
|
pluginDir = os.path.join(project.dirname(), dirname, const.META_DIR, "plugins")
|
||||||
|
pluginDir = os.path.normpath(pluginDir)
|
||||||
|
initFile = os.path.join(pluginDir, "__init__.py")
|
||||||
|
|
||||||
for files in shell.readdir(pluginDir):
|
if os.path.isfile(initFile):
|
||||||
if files.endswith(".py"):
|
load(initFile)
|
||||||
plugin = load(os.path.join(pluginDir, files))
|
else:
|
||||||
|
for files in shell.readdir(pluginDir):
|
||||||
|
if files.endswith(".py"):
|
||||||
|
load(os.path.join(pluginDir, files))
|
||||||
|
|
||||||
if plugin:
|
|
||||||
_logger.info(f"Loaded plugin {plugin.name}")
|
def setup(args: cli.Args):
|
||||||
plugin.init()
|
if not bool(args.consumeOpt("safemode", False)):
|
||||||
|
loadAll()
|
||||||
|
|
238
cutekit/pods.py
Normal file
238
cutekit/pods.py
Normal file
|
@ -0,0 +1,238 @@
|
||||||
|
import sys
|
||||||
|
from typing import Optional
|
||||||
|
import docker # type: ignore
|
||||||
|
import os
|
||||||
|
import dataclasses as dt
|
||||||
|
|
||||||
|
from . import cli, model, shell, vt100, const
|
||||||
|
|
||||||
|
|
||||||
|
podPrefix = "CK__"
|
||||||
|
projectRoot = "/project"
|
||||||
|
toolingRoot = "/tools"
|
||||||
|
defaultPodName = f"{podPrefix}default"
|
||||||
|
defaultPodImage = "ubuntu"
|
||||||
|
|
||||||
|
|
||||||
|
@dt.dataclass
|
||||||
|
class Image:
|
||||||
|
id: str
|
||||||
|
like: str
|
||||||
|
image: str
|
||||||
|
setup: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
@dt.dataclass
|
||||||
|
class Pod:
|
||||||
|
name: str
|
||||||
|
image: Image
|
||||||
|
|
||||||
|
|
||||||
|
IMAGES: dict[str, Image] = {
|
||||||
|
"ubuntu-jammy": Image(
|
||||||
|
"ubuntu-jammy",
|
||||||
|
"ubuntu",
|
||||||
|
"ubuntu:jammy",
|
||||||
|
[
|
||||||
|
"apt-get update",
|
||||||
|
"apt-get install -y python3.11 python3.11-venv ninja-build build-essential",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
"debian-bookworm": Image(
|
||||||
|
"debian-bookworm",
|
||||||
|
"debian",
|
||||||
|
"debian:bookworm",
|
||||||
|
[
|
||||||
|
"apt-get update",
|
||||||
|
"apt-get install -y python3 python3-pip python3-venv ninja-build build-essential",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
"alpine-3.18": Image(
|
||||||
|
"alpine-3.18",
|
||||||
|
"alpine",
|
||||||
|
"alpine:3.18",
|
||||||
|
[
|
||||||
|
"apk update",
|
||||||
|
"apk add python3 python3-dev py3-pip py3-virtualenv build-base linux-headers ninja make automake gcc g++ bash",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
"arch": Image(
|
||||||
|
"arch",
|
||||||
|
"arch",
|
||||||
|
"archlinux:latest",
|
||||||
|
[
|
||||||
|
"pacman -Syu --noconfirm",
|
||||||
|
"pacman -S --noconfirm python python-pip python-virtualenv ninja base-devel",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
"fedora-39": Image(
|
||||||
|
"fedora-39",
|
||||||
|
"fedora",
|
||||||
|
"fedora:39",
|
||||||
|
[
|
||||||
|
"dnf update -y",
|
||||||
|
"dnf install -y python3 python3-pip ninja-build make automake gcc gcc-c++ kernel-devel",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def setup(args: cli.Args):
|
||||||
|
"""
|
||||||
|
Reincarnate cutekit within a docker container, this is
|
||||||
|
useful for cross-compiling
|
||||||
|
"""
|
||||||
|
pod = args.consumeOpt("pod", False)
|
||||||
|
if not pod:
|
||||||
|
return
|
||||||
|
if isinstance(pod, str):
|
||||||
|
pod = pod.strip()
|
||||||
|
pod = podPrefix + pod
|
||||||
|
if pod is True:
|
||||||
|
pod = defaultPodName
|
||||||
|
assert isinstance(pod, str)
|
||||||
|
model.Project.ensure()
|
||||||
|
print(f"Reincarnating into pod '{pod[len(podPrefix) :]}'...")
|
||||||
|
try:
|
||||||
|
strippedArgsV = list(sys.argv[1:])
|
||||||
|
strippedArgsV = [arg for arg in strippedArgsV if not arg.startswith("--pod=")]
|
||||||
|
|
||||||
|
shell.exec(
|
||||||
|
"docker",
|
||||||
|
"exec",
|
||||||
|
"-w",
|
||||||
|
projectRoot,
|
||||||
|
"-i",
|
||||||
|
pod,
|
||||||
|
"/tools/cutekit/entrypoint.sh",
|
||||||
|
"--reincarnated",
|
||||||
|
*strippedArgsV,
|
||||||
|
)
|
||||||
|
sys.exit(0)
|
||||||
|
except Exception:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("p", "pod", "Manage pods")
|
||||||
|
def _(args: cli.Args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def tryDecode(data: Optional[bytes], default: str = "") -> str:
|
||||||
|
if data is None:
|
||||||
|
return default
|
||||||
|
return data.decode()
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("c", "pod/create", "Create a new pod")
|
||||||
|
def _(args: cli.Args):
|
||||||
|
"""
|
||||||
|
Create a new development pod with cutekit installed and the current
|
||||||
|
project mounted at /project
|
||||||
|
"""
|
||||||
|
project = model.Project.ensure()
|
||||||
|
|
||||||
|
name = str(args.consumeOpt("name", defaultPodName))
|
||||||
|
if not name.startswith(podPrefix):
|
||||||
|
name = f"{podPrefix}{name}"
|
||||||
|
image = IMAGES[str(args.consumeOpt("image", defaultPodImage))]
|
||||||
|
|
||||||
|
client = docker.from_env()
|
||||||
|
try:
|
||||||
|
existing = client.containers.get(name)
|
||||||
|
if cli.ask(f"Pod '{name[len(podPrefix):]}' already exists, kill it?", False):
|
||||||
|
existing.stop()
|
||||||
|
existing.remove()
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Pod '{name[len(podPrefix):]}' already exists")
|
||||||
|
except docker.errors.NotFound:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print(f"Starting pod '{name[len(podPrefix) :]}'...")
|
||||||
|
|
||||||
|
container = client.containers.run(
|
||||||
|
image.image,
|
||||||
|
"sleep infinity",
|
||||||
|
name=name,
|
||||||
|
volumes={
|
||||||
|
const.MODULE_DIR: {
|
||||||
|
"bind": toolingRoot + "/cutekit",
|
||||||
|
"mode": "ro",
|
||||||
|
},
|
||||||
|
os.path.abspath(project.dirname()): {"bind": projectRoot, "mode": "rw"},
|
||||||
|
},
|
||||||
|
detach=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Initializing pod '{name[len(podPrefix) :]}'...")
|
||||||
|
for cmd in image.setup:
|
||||||
|
print(vt100.p(cmd))
|
||||||
|
exitCode, output = container.exec_run(f"/bin/sh -c '{cmd}'", demux=True)
|
||||||
|
if exitCode != 0:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Failed to initialize pod with command '{cmd}':\n\nSTDOUT:\n{vt100.indent(vt100.wordwrap(tryDecode(output[0], '<empty>')))}\nSTDERR:\n{vt100.indent(vt100.wordwrap(tryDecode(output[1], '<empty>')))}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Created pod '{name[len(podPrefix) :]}' from image '{image.image}'")
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("k", "pod/kill", "Stop and remove a pod")
|
||||||
|
def _(args: cli.Args):
|
||||||
|
client = docker.from_env()
|
||||||
|
name = str(args.consumeOpt("name", defaultPodName))
|
||||||
|
all = args.consumeOpt("all", False) is True
|
||||||
|
if not name.startswith(podPrefix):
|
||||||
|
name = f"{podPrefix}{name}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
if all:
|
||||||
|
for container in client.containers.list(all=True):
|
||||||
|
if not container.name.startswith(podPrefix):
|
||||||
|
continue
|
||||||
|
container.stop()
|
||||||
|
container.remove()
|
||||||
|
print(f"Pod '{container.name[len(podPrefix) :]}' killed")
|
||||||
|
return
|
||||||
|
|
||||||
|
container = client.containers.get(name)
|
||||||
|
container.stop()
|
||||||
|
container.remove()
|
||||||
|
print(f"Pod '{name[len(podPrefix) :]}' killed")
|
||||||
|
except docker.errors.NotFound:
|
||||||
|
raise RuntimeError(f"Pod '{name[len(podPrefix):]}' does not exist")
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("s", "pod/shell", "Open a shell in a pod")
|
||||||
|
def _(args: cli.Args):
|
||||||
|
args.args.insert(0, "/bin/bash")
|
||||||
|
podExecCmd(args)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("l", "pod/list", "List all pods")
|
||||||
|
def _(args: cli.Args):
|
||||||
|
client = docker.from_env()
|
||||||
|
hasPods = False
|
||||||
|
for container in client.containers.list(all=True):
|
||||||
|
if not container.name.startswith(podPrefix):
|
||||||
|
continue
|
||||||
|
print(container.name[len(podPrefix) :], container.status)
|
||||||
|
hasPods = True
|
||||||
|
|
||||||
|
if not hasPods:
|
||||||
|
print(vt100.p("(No pod found)"))
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("e", "pod/exec", "Execute a command in a pod")
|
||||||
|
def podExecCmd(args: cli.Args):
|
||||||
|
name = str(args.consumeOpt("name", defaultPodName))
|
||||||
|
if not name.startswith(podPrefix):
|
||||||
|
name = f"{podPrefix}{name}"
|
||||||
|
|
||||||
|
cmd = args.consumeArg()
|
||||||
|
if cmd is None:
|
||||||
|
raise RuntimeError("Missing command to execute")
|
||||||
|
|
||||||
|
try:
|
||||||
|
shell.exec("docker", "exec", "-it", name, cmd, *args.extra)
|
||||||
|
except Exception:
|
||||||
|
raise RuntimeError(f"Pod '{name[len(podPrefix):]}' does not exist")
|
|
@ -1,3 +1,5 @@
|
||||||
requests ~= 2.31.0
|
requests ~= 2.31.0
|
||||||
graphviz ~= 0.20.1
|
graphviz ~= 0.20.1
|
||||||
dataclasses-json ~= 0.6.2
|
dataclasses-json ~= 0.6.2
|
||||||
|
docker ~= 6.1.3
|
||||||
|
asserts ~= 0.12.0
|
|
@ -1,52 +1,58 @@
|
||||||
|
import dataclasses as dt
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dt.dataclass
|
||||||
class Rule:
|
class Rule:
|
||||||
id: str
|
id: str
|
||||||
fileIn: list[str]
|
fileIn: list[str]
|
||||||
fileOut: list[str]
|
fileOut: str
|
||||||
rule: str
|
rule: str
|
||||||
args: list[str]
|
args: list[str] = dt.field(default_factory=list)
|
||||||
deps: Optional[str] = None
|
deps: list[str] = dt.field(default_factory=list)
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
id: str,
|
|
||||||
fileIn: list[str],
|
|
||||||
fileOut: list[str],
|
|
||||||
rule: str,
|
|
||||||
args: list[str] = [],
|
|
||||||
deps: Optional[str] = None,
|
|
||||||
):
|
|
||||||
self.id = id
|
|
||||||
self.fileIn = fileIn
|
|
||||||
self.fileOut = fileOut
|
|
||||||
self.rule = rule
|
|
||||||
self.args = args
|
|
||||||
self.deps = deps
|
|
||||||
|
|
||||||
|
|
||||||
rules: dict[str, Rule] = {
|
rules: dict[str, Rule] = {
|
||||||
"cp": Rule("cp", ["*"], ["*"], "$in $out"),
|
"cp": Rule("cp", ["*"], "*", "$in $out"),
|
||||||
"cc": Rule(
|
"cc": Rule(
|
||||||
"cc",
|
"cc",
|
||||||
["*.c"],
|
["*.c"],
|
||||||
["*.o"],
|
"*.o",
|
||||||
"-c -o $out $in -MD -MF $out.d $flags $cincs $cdefs",
|
"-c -o $out $in -MD -MF $out.d $flags $cincs $cdefs",
|
||||||
["-std=gnu2x", "-Wall", "-Wextra", "-Werror"],
|
[
|
||||||
"$out.d",
|
"-std=gnu2x",
|
||||||
|
"-Wall",
|
||||||
|
"-Wextra",
|
||||||
|
"-Werror",
|
||||||
|
"-fcolor-diagnostics",
|
||||||
|
],
|
||||||
|
["$out.d"],
|
||||||
),
|
),
|
||||||
"cxx": Rule(
|
"cxx": Rule(
|
||||||
"cxx",
|
"cxx",
|
||||||
["*.cpp", "*.cc", "*.cxx"],
|
["*.cpp", "*.cc", "*.cxx"],
|
||||||
["*.o"],
|
"*.o",
|
||||||
"-c -o $out $in -MD -MF $out.d $flags $cincs $cdefs",
|
"-c -o $out $in -MD -MF $out.d $flags $cincs $cdefs",
|
||||||
["-std=gnu++2b", "-Wall", "-Wextra", "-Werror", "-fno-exceptions", "-fno-rtti"],
|
[
|
||||||
"$out.d",
|
"-std=gnu++2b",
|
||||||
|
"-Wall",
|
||||||
|
"-Wextra",
|
||||||
|
"-Werror",
|
||||||
|
"-fcolor-diagnostics",
|
||||||
|
"-fno-exceptions",
|
||||||
|
"-fno-rtti",
|
||||||
|
],
|
||||||
|
["$out.d"],
|
||||||
|
),
|
||||||
|
"as": Rule("as", ["*.s", "*.asm", "*.S"], "*.o", "-o $out $in $flags"),
|
||||||
|
"ar": Rule("ar", ["*.o"], "*.a", "$flags $out $in"),
|
||||||
|
"ld": Rule(
|
||||||
|
"ld",
|
||||||
|
["*.o", "*.a"],
|
||||||
|
"*.out",
|
||||||
|
"-o $out $objs --whole-archive $wholeLibs --no-whole-archive $libs $flags",
|
||||||
),
|
),
|
||||||
"as": Rule("as", ["*.s", "*.asm", "*.S"], ["*.o"], "-o $out $in $flags"),
|
|
||||||
"ar": Rule("ar", ["*.o"], ["*.a"], "$flags $out $in"),
|
|
||||||
"ld": Rule("ld", ["*.o", "*.a"], ["*.out"], "-o $out $in $flags"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
254
cutekit/shell.py
254
cutekit/shell.py
|
@ -10,28 +10,36 @@ import fnmatch
|
||||||
import platform
|
import platform
|
||||||
import logging
|
import logging
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import dataclasses as dt
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Literal, Optional
|
||||||
from . import const
|
from . import const, cli
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dt.dataclass
|
||||||
class Uname:
|
class Uname:
|
||||||
def __init__(
|
sysname: str
|
||||||
self, sysname: str, nodename: str, release: str, version: str, machine: str
|
distrib: str
|
||||||
):
|
nodename: str
|
||||||
self.sysname = sysname
|
release: str
|
||||||
self.nodename = nodename
|
version: str
|
||||||
self.release = release
|
machine: str
|
||||||
self.version = version
|
|
||||||
self.machine = machine
|
|
||||||
|
|
||||||
|
|
||||||
def uname() -> Uname:
|
def uname() -> Uname:
|
||||||
un = platform.uname()
|
un = platform.uname()
|
||||||
result = Uname(un.system, un.node, un.release, un.version, un.machine)
|
|
||||||
|
if hasattr(platform, "freedesktop_os_release"):
|
||||||
|
distrib = platform.freedesktop_os_release()
|
||||||
|
else:
|
||||||
|
distrib = {"NAME": "Unknown"}
|
||||||
|
|
||||||
|
result = Uname(
|
||||||
|
un.system, distrib["NAME"], un.node, un.release, un.version, un.machine
|
||||||
|
)
|
||||||
|
|
||||||
match result.machine:
|
match result.machine:
|
||||||
case "aarch64":
|
case "aarch64":
|
||||||
|
@ -52,14 +60,14 @@ def sha256sum(path: str) -> str:
|
||||||
def find(
|
def find(
|
||||||
path: str | list[str], wildcards: list[str] = [], recusive: bool = True
|
path: str | list[str], wildcards: list[str] = [], recusive: bool = True
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
_logger.info(f"Looking for files in {path} matching {wildcards}")
|
_logger.debug(f"Looking for files in {path} matching {wildcards}")
|
||||||
|
|
||||||
result: list[str] = []
|
result: list[str] = []
|
||||||
|
|
||||||
if isinstance(path, list):
|
if isinstance(path, list):
|
||||||
for p in path:
|
for p in path:
|
||||||
result += find(p, wildcards, recusive)
|
result += find(p, wildcards, recusive)
|
||||||
return result
|
return sorted(result)
|
||||||
|
|
||||||
if not os.path.isdir(path):
|
if not os.path.isdir(path):
|
||||||
return []
|
return []
|
||||||
|
@ -84,11 +92,12 @@ def find(
|
||||||
result.append(os.path.join(path, f))
|
result.append(os.path.join(path, f))
|
||||||
break
|
break
|
||||||
|
|
||||||
return result
|
# sort for reproducibility
|
||||||
|
return sorted(result)
|
||||||
|
|
||||||
|
|
||||||
def mkdir(path: str) -> str:
|
def mkdir(path: str) -> str:
|
||||||
_logger.info(f"Creating directory {path}")
|
_logger.debug(f"Creating directory {path}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.makedirs(path)
|
os.makedirs(path)
|
||||||
|
@ -99,11 +108,14 @@ def mkdir(path: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
def rmrf(path: str) -> bool:
|
def rmrf(path: str) -> bool:
|
||||||
_logger.info(f"Removing directory {path}")
|
_logger.debug(f"Removing directory {path}")
|
||||||
|
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
return False
|
return False
|
||||||
shutil.rmtree(path, ignore_errors=True)
|
if os.path.isfile(path):
|
||||||
|
os.remove(path)
|
||||||
|
else:
|
||||||
|
shutil.rmtree(path, ignore_errors=True)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -118,7 +130,7 @@ def wget(url: str, path: Optional[str] = None) -> str:
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
return path
|
return path
|
||||||
|
|
||||||
_logger.info(f"Downloading {url} to {path}")
|
_logger.debug(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()
|
||||||
|
@ -131,56 +143,129 @@ def wget(url: str, path: Optional[str] = None) -> str:
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
def exec(*args: str, quiet: bool = False) -> bool:
|
def exec(*args: str, quiet: bool = False, cwd: Optional[str] = None) -> bool:
|
||||||
_logger.info(f"Executing {args}")
|
_logger.debug(f"Executing {args}")
|
||||||
|
cmdName = Path(args[0]).name
|
||||||
|
|
||||||
try:
|
try:
|
||||||
proc = subprocess.run(
|
proc = subprocess.run(
|
||||||
args,
|
args,
|
||||||
|
cwd=cwd,
|
||||||
stdout=sys.stdout if not quiet else subprocess.PIPE,
|
stdout=sys.stdout if not quiet else subprocess.PIPE,
|
||||||
stderr=sys.stderr if not quiet else subprocess.PIPE,
|
stderr=sys.stderr if not quiet else subprocess.PIPE,
|
||||||
)
|
)
|
||||||
|
|
||||||
if proc.stdout:
|
if proc.stdout:
|
||||||
_logger.info(proc.stdout.decode("utf-8"))
|
_logger.debug(proc.stdout.decode("utf-8"))
|
||||||
|
|
||||||
if proc.stderr:
|
if proc.stderr:
|
||||||
_logger.error(proc.stderr.decode("utf-8"))
|
_logger.debug(proc.stderr.decode("utf-8"))
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise RuntimeError(f"{args[0]}: Command not found")
|
if cwd and not os.path.exists(cwd):
|
||||||
|
raise RuntimeError(f"{cwd}: No such file or directory")
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"{args[0]}: Command not found")
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
raise RuntimeError(f"{args[0]}: Interrupted")
|
raise RuntimeError(f"{cmdName}: Interrupted")
|
||||||
|
|
||||||
if proc.returncode == -signal.SIGSEGV:
|
if proc.returncode == -signal.SIGSEGV:
|
||||||
raise RuntimeError(f"{args[0]}: Segmentation fault")
|
raise RuntimeError(f"{cmdName}: Segmentation fault")
|
||||||
|
|
||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
raise RuntimeError(f"{args[0]}: Process exited with code {proc.returncode}")
|
raise RuntimeError(f"{cmdName}: Process exited with code {proc.returncode}")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def popen(*args: str) -> str:
|
def popen(*args: str) -> str:
|
||||||
_logger.info(f"Executing {args}")
|
_logger.debug(f"Executing {args}...")
|
||||||
|
|
||||||
|
cmdName = Path(args[0]).name
|
||||||
|
|
||||||
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 RuntimeError(f"{args[0]}: Command not found")
|
raise RuntimeError(f"{cmdName}: Command not found")
|
||||||
|
|
||||||
if proc.returncode == -signal.SIGSEGV:
|
if proc.returncode == -signal.SIGSEGV:
|
||||||
raise RuntimeError(f"{args[0]}: Segmentation fault")
|
raise RuntimeError(f"{cmdName}: Segmentation fault")
|
||||||
|
|
||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
raise RuntimeError(f"{args[0]}: Process exited with code {proc.returncode}")
|
raise RuntimeError(f"{cmdName}: Process exited with code {proc.returncode}")
|
||||||
|
|
||||||
return proc.stdout.decode("utf-8")
|
return proc.stdout.decode("utf-8").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def debug(cmd: list[str], debugger: str = "lldb", wait: bool = False):
|
||||||
|
if debugger == "lldb":
|
||||||
|
exec(
|
||||||
|
"lldb",
|
||||||
|
*(("-o", "b main") if wait else ()),
|
||||||
|
*("-o", "run"),
|
||||||
|
*cmd,
|
||||||
|
)
|
||||||
|
elif debugger == "gdb":
|
||||||
|
exec(
|
||||||
|
"gdb",
|
||||||
|
*(("-ex", "b main") if wait else ()),
|
||||||
|
*("-ex", "run"),
|
||||||
|
*cmd,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Unknown debugger {debugger}")
|
||||||
|
|
||||||
|
|
||||||
|
def _profileCpu(cmd: list[str], rate=1000):
|
||||||
|
mkdir(const.TMP_DIR)
|
||||||
|
perfFile = f"{const.TMP_DIR}/cpu-profile.data"
|
||||||
|
try:
|
||||||
|
exec(
|
||||||
|
"perf",
|
||||||
|
"record",
|
||||||
|
"-F",
|
||||||
|
str(rate),
|
||||||
|
"-g",
|
||||||
|
"-o",
|
||||||
|
perfFile,
|
||||||
|
"--call-graph",
|
||||||
|
"dwarf",
|
||||||
|
*cmd,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
if not os.path.exists(perfFile):
|
||||||
|
raise e
|
||||||
|
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
["perf", "script", "-i", perfFile], stdout=subprocess.PIPE
|
||||||
|
)
|
||||||
|
subprocess.run(["speedscope", "-"], stdin=proc.stdout)
|
||||||
|
proc.wait()
|
||||||
|
except Exception as e:
|
||||||
|
rmrf(perfFile)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
rmrf(perfFile)
|
||||||
|
|
||||||
|
|
||||||
|
def _profileMem(cmd: list[str]):
|
||||||
|
perfFile = f"{const.TMP_DIR}/mem-profile.data"
|
||||||
|
exec("heaptrack", "-o", perfFile, *cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def profile(cmd: list[str], rate=1000, what: Literal["cpu", "mem"] = "cpu"):
|
||||||
|
if what == "cpu":
|
||||||
|
_profileCpu(cmd, rate)
|
||||||
|
elif what == "mem":
|
||||||
|
_profileMem(cmd)
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Unknown profile type {what}")
|
||||||
|
|
||||||
|
|
||||||
def readdir(path: str) -> list[str]:
|
def readdir(path: str) -> list[str]:
|
||||||
_logger.info(f"Reading directory {path}")
|
_logger.debug(f"Reading directory {path}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return os.listdir(path)
|
return os.listdir(path)
|
||||||
|
@ -189,19 +274,19 @@ def readdir(path: str) -> list[str]:
|
||||||
|
|
||||||
|
|
||||||
def cp(src: str, dst: str):
|
def cp(src: str, dst: str):
|
||||||
_logger.info(f"Copying {src} to {dst}")
|
_logger.debug(f"Copying {src} to {dst}")
|
||||||
|
|
||||||
shutil.copy(src, dst)
|
shutil.copy(src, dst)
|
||||||
|
|
||||||
|
|
||||||
def mv(src: str, dst: str):
|
def mv(src: str, dst: str):
|
||||||
_logger.info(f"Moving {src} to {dst}")
|
_logger.debug(f"Moving {src} to {dst}")
|
||||||
|
|
||||||
shutil.move(src, dst)
|
shutil.move(src, dst)
|
||||||
|
|
||||||
|
|
||||||
def cpTree(src: str, dst: str):
|
def cpTree(src: str, dst: str):
|
||||||
_logger.info(f"Copying {src} to {dst}")
|
_logger.debug(f"Copying {src} to {dst}")
|
||||||
|
|
||||||
shutil.copytree(src, dst, dirs_exist_ok=True)
|
shutil.copytree(src, dst, dirs_exist_ok=True)
|
||||||
|
|
||||||
|
@ -241,10 +326,15 @@ def latest(cmd: str) -> str:
|
||||||
if cmd in LATEST_CACHE:
|
if cmd in LATEST_CACHE:
|
||||||
return LATEST_CACHE[cmd]
|
return LATEST_CACHE[cmd]
|
||||||
|
|
||||||
_logger.info(f"Finding latest version of {cmd}")
|
if "IN_NIX_SHELL" in os.environ:
|
||||||
|
# By default, NixOS symlinks tools automatically
|
||||||
|
# to their latest version. Also, if the user uses
|
||||||
|
# clang-xx, the std libraries will not be accessible.
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
_logger.debug(f"Finding latest version of {cmd}")
|
||||||
|
|
||||||
regex: re.Pattern[str]
|
regex: re.Pattern[str]
|
||||||
|
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
regex = re.compile(r"^" + re.escape(cmd) + r"(-.[0-9]+)?(\.exe)?$")
|
regex = re.compile(r"^" + re.escape(cmd) + r"(-.[0-9]+)?(\.exe)?$")
|
||||||
else:
|
else:
|
||||||
|
@ -263,7 +353,7 @@ def latest(cmd: str) -> str:
|
||||||
versions.sort()
|
versions.sort()
|
||||||
chosen = versions[-1]
|
chosen = versions[-1]
|
||||||
|
|
||||||
_logger.info(f"Chosen {chosen} as latest version of {cmd}")
|
_logger.debug(f"Chosen {chosen} as latest version of {cmd}")
|
||||||
|
|
||||||
LATEST_CACHE[cmd] = chosen
|
LATEST_CACHE[cmd] = chosen
|
||||||
|
|
||||||
|
@ -275,3 +365,89 @@ def which(cmd: str) -> Optional[str]:
|
||||||
Find the path of a command
|
Find the path of a command
|
||||||
"""
|
"""
|
||||||
return shutil.which(cmd)
|
return shutil.which(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def nproc() -> int:
|
||||||
|
"""
|
||||||
|
Return the number of processors
|
||||||
|
"""
|
||||||
|
return os.cpu_count() or 1
|
||||||
|
|
||||||
|
|
||||||
|
def gzip(path: str, dest: Optional[str] = None) -> str:
|
||||||
|
"""
|
||||||
|
Compress a file or directory
|
||||||
|
"""
|
||||||
|
|
||||||
|
if dest is None:
|
||||||
|
dest = path + ".gz"
|
||||||
|
|
||||||
|
with open(dest, "wb") as f:
|
||||||
|
proc = subprocess.run(
|
||||||
|
["gzip", "-c", path],
|
||||||
|
stdout=f,
|
||||||
|
stderr=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise RuntimeError(f"gzip: Process exited with code {proc.returncode}")
|
||||||
|
|
||||||
|
return dest
|
||||||
|
|
||||||
|
|
||||||
|
def compress(path: str, dest: Optional[str] = None, format: str = "zstd") -> str:
|
||||||
|
"""
|
||||||
|
Compress a file or directory
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXTS = {
|
||||||
|
"zip": "zip",
|
||||||
|
"zstd": "zst",
|
||||||
|
"gzip": "gz",
|
||||||
|
}
|
||||||
|
|
||||||
|
if dest is None:
|
||||||
|
dest = path + "." + EXTS[format]
|
||||||
|
|
||||||
|
_logger.debug(f"Compressing {path} to {dest}")
|
||||||
|
|
||||||
|
if format == "zip":
|
||||||
|
exec("zip", "-r", dest, path)
|
||||||
|
elif format == "zstd":
|
||||||
|
exec("zstd", "-q", "-o", dest, path)
|
||||||
|
elif format == "gzip":
|
||||||
|
gzip(path, dest)
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Unknown compression format {format}")
|
||||||
|
|
||||||
|
return dest
|
||||||
|
|
||||||
|
|
||||||
|
# --- Commands --------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("s", "scripts", "Manage scripts")
|
||||||
|
def _(args: cli.Args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("d", "debug", "Debug a program")
|
||||||
|
def _(args: cli.Args):
|
||||||
|
wait = args.consumeOpt("wait", False) is True
|
||||||
|
debugger = args.consumeOpt("debugger", "lldb")
|
||||||
|
command = [str(args.consumeArg()), *args.extra]
|
||||||
|
debug(command, debugger=str(debugger), wait=wait)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("p", "profile", "Profile a program")
|
||||||
|
def _(args: cli.Args):
|
||||||
|
command = [str(args.consumeArg()), *args.extra]
|
||||||
|
profile(command)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("c", "compress", "Compress a file or directory")
|
||||||
|
def _(args: cli.Args):
|
||||||
|
path = str(args.consumeArg())
|
||||||
|
dest = args.consumeOpt("dest", None)
|
||||||
|
format = args.consumeOpt("format", "zstd")
|
||||||
|
compress(path, dest, format)
|
||||||
|
|
|
@ -6,7 +6,7 @@ import hashlib
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
def uniq(lst: list[T]) -> list[T]:
|
def uniqPreserveOrder(lst: list[T]) -> list[T]:
|
||||||
result: list[T] = []
|
result: list[T] = []
|
||||||
for i in lst:
|
for i in lst:
|
||||||
if i in result:
|
if i in result:
|
||||||
|
@ -29,6 +29,10 @@ def hash(
|
||||||
return hashlib.sha256(data.encode("utf-8")).hexdigest()
|
return hashlib.sha256(data.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def randomHash() -> str:
|
||||||
|
return hashlib.sha256(os.urandom(32)).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def camelCase(s: str) -> str:
|
def camelCase(s: str) -> str:
|
||||||
s = "".join(x for x in s.title() if x != "_" and x != "-")
|
s = "".join(x for x in s.title() if x != "_" and x != "-")
|
||||||
s = s[0].lower() + s[1:]
|
s = s[0].lower() + s[1:]
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
# Cutekit
|
# Cutekit
|
||||||
|
|
||||||
Cutekit is a build system that aims to be simple, fast and easy to use.
|
Cutekit is a build system that aims to be simple, fast and easy to use.
|
||||||
A project is described using json files.
|
A project is described using json files.
|
||||||
|
|
||||||
## Project file
|
## Project file
|
||||||
|
|
||||||
The project file is used to describe the project and its dependencies.
|
The project file is used to describe the project and its dependencies.
|
||||||
|
|
||||||
See: [doc/spec/project.md](doc/spec/project.md) for the full specification.
|
See: [doc/spec/project.md](doc/spec/project.md) for the full specification.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
> project.json
|
> project.json
|
||||||
```json
|
```json
|
||||||
|
@ -35,7 +35,7 @@ Here we describe a project with the id `skift-org/skift` and a dependency to `cu
|
||||||
When you want to create an executable package, you need to create a `manifest.json` file in any directory under `src/`.
|
When you want to create an executable package, you need to create a `manifest.json` file in any directory under `src/`.
|
||||||
This is the file that describe an executable with its dependencies.
|
This is the file that describe an executable with its dependencies.
|
||||||
|
|
||||||
> src/nyan-cat-app/manifest.json
|
> src/nyan-cat-app/manifest.json
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"$schema": "https://schemas.cute.engineering/stable/cutekit.manifest.component.v1",
|
"$schema": "https://schemas.cute.engineering/stable/cutekit.manifest.component.v1",
|
||||||
|
@ -56,7 +56,7 @@ You can run the executable by running `$ ck run nyan-cat-app`.
|
||||||
|
|
||||||
When you want to create a library package, you need to create a `manifest.json` file in any directory under `src/`, like an executable package.
|
When you want to create a library package, you need to create a `manifest.json` file in any directory under `src/`, like an executable package.
|
||||||
|
|
||||||
> src/easy-lib/manifest.json
|
> src/easy-lib/manifest.json
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"$schema": "https://schemas.cute.engineering/stable/cutekit.manifest.component.v1",
|
"$schema": "https://schemas.cute.engineering/stable/cutekit.manifest.component.v1",
|
||||||
|
|
|
@ -11,8 +11,6 @@ For example you can add a new command to the CLI:
|
||||||
from cutekit import cli
|
from cutekit import cli
|
||||||
|
|
||||||
@cli.command("h", "hello", "Print hello world")
|
@cli.command("h", "hello", "Print hello world")
|
||||||
def bootCmd(args: cli.Args) -> None:
|
def _(args: cli.Args) -> None:
|
||||||
print("Hello world!")
|
print("Hello world!")
|
||||||
```
|
```
|
||||||
|
|
||||||
This feature is used - for example - by [SkiftOS](https://github.com/skift-org/skift/blob/main/meta/plugins/start-cmd.py) to add the `start` command, that build packages and run a virtual machine.
|
|
||||||
|
|
60
meta/nix/flake.lock
Normal file
60
meta/nix/flake.lock
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1705309234,
|
||||||
|
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1706098335,
|
||||||
|
"narHash": "sha256-r3dWjT8P9/Ah5m5ul4WqIWD8muj5F+/gbCdjiNVBKmU=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "a77ab169a83a4175169d78684ddd2e54486ac651",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"id": "nixpkgs",
|
||||||
|
"ref": "nixos-23.11",
|
||||||
|
"type": "indirect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
36
meta/nix/flake.nix
Normal file
36
meta/nix/flake.nix
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
description = "The *magical* build system and package manager";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "nixpkgs/nixos-23.11";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, flake-utils }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
ck = pkgs.python311Packages.buildPythonApplication {
|
||||||
|
pyproject = true;
|
||||||
|
pname = "cutekit";
|
||||||
|
version = "0.7-dev";
|
||||||
|
|
||||||
|
src = ./../..;
|
||||||
|
|
||||||
|
nativeBuildInputs = with pkgs.python311Packages; [ setuptools ];
|
||||||
|
propagatedBuildInputs = with pkgs.python311Packages; [
|
||||||
|
dataclasses-json
|
||||||
|
docker
|
||||||
|
requests
|
||||||
|
graphviz
|
||||||
|
];
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
package.ck = ck;
|
||||||
|
defaultPackage = self.package.${system}.ck;
|
||||||
|
devShell = pkgs.mkShell {
|
||||||
|
buildInputs = [ ck ];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ authors = [
|
||||||
{ name = "Cute Engineering", email = "contact@cute.engineering" },
|
{ name = "Cute Engineering", email = "contact@cute.engineering" },
|
||||||
]
|
]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.11"
|
||||||
license = { text = "MIT" }
|
license = { text = "MIT" }
|
||||||
dynamic = ["version", "dependencies"]
|
dynamic = ["version", "dependencies"]
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ packages = ["cutekit"]
|
||||||
|
|
||||||
[tool.setuptools.dynamic]
|
[tool.setuptools.dynamic]
|
||||||
version = { attr = "cutekit.const.VERSION" }
|
version = { attr = "cutekit.const.VERSION" }
|
||||||
dependencies = { file = ["requirements.txt"] }
|
dependencies = { file = ["cutekit/requirements.txt"] }
|
||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
"cutekit" = ["py.typed"]
|
"cutekit" = ["py.typed", "requirements.txt", "entrypoint.sh", "bootstrap.sh"]
|
||||||
|
|
106
readme.md
Normal file
106
readme.md
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<p align="center">
|
||||||
|
<img src="logo.png" width="200" height="200">
|
||||||
|
</p>
|
||||||
|
<h1 align="center">CuteKit</h1>
|
||||||
|
<p align="center">
|
||||||
|
The *magical* build system and package manager
|
||||||
|
</p>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
**CuteKit** is a suite of tools and utilities for compiling, cross-compiling, linking, and packaging project written in low-level languages such as C, C++ or, Rust. Anything from a simple library to an operating system can be built using CuteKit.
|
||||||
|
|
||||||
|
- ✨ It uses **JSON**: Cutekit uses JSON instead of introducing a whole new programming language for describing the project. And also has macros for more advanced use cases (see [Jexpr](doc/spec/jexpr.md)).
|
||||||
|
- ✨ It's a **package manager**: Cutekit package manager is based on **Git**, nothing is centralized.
|
||||||
|
- ✨ It's **extendible**: Cutekit can be [extended](./doc/extends.md) by writing custom Python plugins.
|
||||||
|
- ✨ It's **easy**: the [**templates**](./doc/templates.md) help the user quick-start a project.
|
||||||
|
- ✨ It's **portable**: Cutekit works on Linux, Windows, and MacOS.
|
||||||
|
|
||||||
|
## CuteKit in the wild
|
||||||
|
|
||||||
|
- [SkiftOS](https://github.com/skift-org/skift) : A hobbyist operating system written in C++.
|
||||||
|
- [WKHtmlToPdf](https://github.com/odoo/wkhtmltopdf) : [Odoo](https://github.com/odoo/odoo)'s fork of wkhtmltopdf which is a command line tools to render HTML into PDF and various image formats using the Qt WebKit rendering engine.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
To install Cutekit, you may use your favourite package manager if it is available. Or you can install it manually by following the instructions below.
|
||||||
|
|
||||||
|
|
||||||
|
### By using pip
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ git clone https://github.com/cute-engineering/cutekit
|
||||||
|
|
||||||
|
$ cd cutekit
|
||||||
|
|
||||||
|
# If you want to use the latest version of Cutekit, you can switch to the dev branch.
|
||||||
|
# $ git switch dev
|
||||||
|
|
||||||
|
$ pip install --user -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
### By using Nix
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ git clone https://github.com/cute-engineering/cutekit
|
||||||
|
|
||||||
|
$ cd cutekit
|
||||||
|
|
||||||
|
# If you want to use the latest version of Cutekit, you can switch to the dev branch.
|
||||||
|
# $ git switch dev
|
||||||
|
|
||||||
|
$ nix shell ./meta/nix
|
||||||
|
```
|
||||||
|
|
||||||
|
Or you can also use Cutekit in your flakes. For example:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
description = "Hello cutekit";
|
||||||
|
inputs = {
|
||||||
|
# If you want to use the latest version of Cutekit, you can specify the dev branch.
|
||||||
|
# cutekit.url = "github:cute-engineering/cutekit/dev?dir=meta/nix";
|
||||||
|
cutekit.url = "github:cute-engineering/cutekit?dir=meta/nix";
|
||||||
|
nixpkgs.url = "nixpkgs/nixos-23.11";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = {self, nixpkgs, cutekit, ... }:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
||||||
|
ck = cutekit.defaultPackage.x86_64-linux;
|
||||||
|
in {
|
||||||
|
devShells.x86_64-linux.default = pkgs.mkShell {
|
||||||
|
packages = with pkgs; [
|
||||||
|
ck
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Quick-start
|
||||||
|
|
||||||
|
- If you directly want to start using Cutekit for a new project, you can just run `$ ck I host` and it will create a new project in the host directory (you can rename it later).
|
||||||
|
|
||||||
|
- If you want to use Cutekit for writing operating systems, you can create a new [limine](https://github.com/limine-bootloader/limine/)-based project by running `$ ck I limine-barebone`.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
If you want to see how it works you can read the [doc/cutekit.md](doc/cutekit.md) file.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
<a href="https://opensource.org/licenses/MIT">
|
||||||
|
<img align="right" height="96" alt="MIT License" src="doc/mit.svg" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
Cutekit is licensed under the **MIT License**.
|
||||||
|
|
||||||
|
The full text of the license can be accessed via [this link](https://opensource.org/licenses/MIT) and is also included in the [license.md](license.md) file of this software package.
|
216
tests/test_cli.py
Normal file
216
tests/test_cli.py
Normal file
|
@ -0,0 +1,216 @@
|
||||||
|
from cutekit import cli2, utils
|
||||||
|
from asserts import (
|
||||||
|
assert_is,
|
||||||
|
assert_equal,
|
||||||
|
assert_is_instance,
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Parse Values ----------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_int_val():
|
||||||
|
assert_equal(cli2.parseValue("1"), 1)
|
||||||
|
assert_equal(cli2.parseValue("2"), 2)
|
||||||
|
assert_equal(cli2.parseValue("+2"), +2)
|
||||||
|
assert_equal(cli2.parseValue("-2"), -2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_true_val():
|
||||||
|
assert_equal(cli2.parseValue("true"), True)
|
||||||
|
assert_equal(cli2.parseValue("True"), True)
|
||||||
|
assert_equal(cli2.parseValue("y"), True)
|
||||||
|
assert_equal(cli2.parseValue("yes"), True)
|
||||||
|
assert_equal(cli2.parseValue("Y"), True)
|
||||||
|
assert_equal(cli2.parseValue("Yes"), True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_false_val():
|
||||||
|
assert_equal(cli2.parseValue("false"), False)
|
||||||
|
assert_equal(cli2.parseValue("False"), False)
|
||||||
|
assert_equal(cli2.parseValue("n"), False)
|
||||||
|
assert_equal(cli2.parseValue("no"), False)
|
||||||
|
assert_equal(cli2.parseValue("N"), False)
|
||||||
|
assert_equal(cli2.parseValue("No"), False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_str_val():
|
||||||
|
assert_equal(cli2.parseValue("foo"), "foo")
|
||||||
|
assert_equal(cli2.parseValue("'foo'"), "foo")
|
||||||
|
assert_equal(cli2.parseValue('"foo"'), "foo")
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_list_val():
|
||||||
|
assert_equal(cli2.parseValue("foo,bar"), ["foo", "bar"])
|
||||||
|
assert_equal(cli2.parseValue("'foo','bar'"), ["foo", "bar"])
|
||||||
|
assert_equal(cli2.parseValue('"foo","bar"'), ["foo", "bar"])
|
||||||
|
|
||||||
|
|
||||||
|
# --- Parse Args ------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_short_arg():
|
||||||
|
args = cli2.parseArg("-a")
|
||||||
|
assert_equal(len(args), 1)
|
||||||
|
arg = args[0]
|
||||||
|
assert_is_instance(arg, cli2.ArgumentToken)
|
||||||
|
assert_equal(arg.key, "a")
|
||||||
|
assert_equal(arg.value, True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_short_args():
|
||||||
|
args = cli2.parseArg("-abc")
|
||||||
|
assert_equal(len(args), 3)
|
||||||
|
arg = args[0]
|
||||||
|
assert_is_instance(arg, cli2.ArgumentToken)
|
||||||
|
assert_equal(arg.key, "a")
|
||||||
|
assert_equal(arg.value, True)
|
||||||
|
|
||||||
|
arg = args[1]
|
||||||
|
assert_is_instance(arg, cli2.ArgumentToken)
|
||||||
|
assert_equal(arg.key, "b")
|
||||||
|
assert_equal(arg.value, True)
|
||||||
|
|
||||||
|
arg = args[2]
|
||||||
|
assert_is_instance(arg, cli2.ArgumentToken)
|
||||||
|
assert_equal(arg.key, "c")
|
||||||
|
assert_equal(arg.value, True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_long_arg():
|
||||||
|
args = cli2.parseArg("--foo")
|
||||||
|
assert_equal(len(args), 1)
|
||||||
|
arg = args[0]
|
||||||
|
assert_is_instance(arg, cli2.ArgumentToken)
|
||||||
|
assert_equal(arg.key, "foo")
|
||||||
|
assert_equal(arg.value, True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_long_arg_with_value():
|
||||||
|
args = cli2.parseArg("--foo=bar")
|
||||||
|
assert_equal(len(args), 1)
|
||||||
|
arg = args[0]
|
||||||
|
assert_is_instance(arg, cli2.ArgumentToken)
|
||||||
|
assert_equal(arg.key, "foo")
|
||||||
|
assert_equal(arg.value, "bar")
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_long_arg_with_value_list():
|
||||||
|
args = cli2.parseArg("--foo=bar,baz")
|
||||||
|
assert_equal(len(args), 1)
|
||||||
|
arg = args[0]
|
||||||
|
assert_is_instance(arg, cli2.ArgumentToken)
|
||||||
|
assert_equal(arg.key, "foo")
|
||||||
|
assert_equal(arg.value, ["bar", "baz"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_key_subkey_arg():
|
||||||
|
args = cli2.parseArg("--foo:bar")
|
||||||
|
assert_equal(len(args), 1)
|
||||||
|
arg = args[0]
|
||||||
|
assert_is_instance(arg, cli2.ArgumentToken)
|
||||||
|
assert_equal(arg.key, "foo")
|
||||||
|
assert_equal(arg.subkey, "bar")
|
||||||
|
assert_equal(arg.value, True)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Extract Args ----------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
def extractParse(type: type[utils.T], args: list[str]) -> utils.T:
|
||||||
|
schema = cli2.Schema.extract(type)
|
||||||
|
return schema.parse(args)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_arg_int():
|
||||||
|
class IntArg:
|
||||||
|
value: int = cli2.arg(None, "value")
|
||||||
|
|
||||||
|
assert_equal(extractParse(IntArg, ["--value=-1"]).value, -1)
|
||||||
|
assert_equal(extractParse(IntArg, ["--value=0"]).value, 0)
|
||||||
|
assert_equal(extractParse(IntArg, ["--value=1"]).value, 1)
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
class StrArg:
|
||||||
|
value: str = cli2.arg(None, "value")
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_arg_str1():
|
||||||
|
v = extractParse(StrArg, ["--value=foo"])
|
||||||
|
assert v.value == "foo"
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_arg_str2():
|
||||||
|
v = extractParse(StrArg, ["--value='foo, bar'"])
|
||||||
|
assert v.value == "foo, bar"
|
||||||
|
|
||||||
|
|
||||||
|
class BoolArg:
|
||||||
|
value: bool = cli2.arg(None, "value")
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_arg_bool():
|
||||||
|
assert_is(extractParse(BoolArg, ["--value"]).value, True)
|
||||||
|
assert_is(extractParse(BoolArg, ["--value=true"]).value, True)
|
||||||
|
assert_is(extractParse(BoolArg, ["--value=True"]).value, True)
|
||||||
|
assert_is(extractParse(BoolArg, ["--value=y"]).value, True)
|
||||||
|
assert_is(extractParse(BoolArg, ["--value=yes"]).value, True)
|
||||||
|
assert_is(extractParse(BoolArg, ["--value=Y"]).value, True)
|
||||||
|
assert_is(extractParse(BoolArg, ["--value=Yes"]).value, True)
|
||||||
|
assert_is(extractParse(BoolArg, ["--value=1"]).value, True)
|
||||||
|
|
||||||
|
assert extractParse(BoolArg, ["--value=false"]).value is False
|
||||||
|
assert extractParse(BoolArg, ["--value=False"]).value is False
|
||||||
|
assert extractParse(BoolArg, ["--value=n"]).value is False
|
||||||
|
assert extractParse(BoolArg, ["--value=no"]).value is False
|
||||||
|
assert extractParse(BoolArg, ["--value=N"]).value is False
|
||||||
|
assert extractParse(BoolArg, ["--value=No"]).value is False
|
||||||
|
print(extractParse(BoolArg, ["--value=0"]).value)
|
||||||
|
assert extractParse(BoolArg, ["--value=0"]).value is False
|
||||||
|
|
||||||
|
|
||||||
|
class IntListArg:
|
||||||
|
value: list[int] = cli2.arg(None, "value")
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_arg_list_int1():
|
||||||
|
assert_equal(extractParse(IntListArg, []).value, [])
|
||||||
|
assert_equal(extractParse(IntListArg, ["--value=1", "--value=2"]).value, [1, 2])
|
||||||
|
assert_equal(extractParse(IntListArg, ["--value=1,2"]).value, [1, 2])
|
||||||
|
|
||||||
|
|
||||||
|
class StrListArg:
|
||||||
|
value: list[str] = cli2.arg(None, "value")
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_arg_list_str():
|
||||||
|
assert_equal(
|
||||||
|
extractParse(StrListArg, ["--value=foo", "--value=bar"]).value,
|
||||||
|
[
|
||||||
|
"foo",
|
||||||
|
"bar",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_equal(extractParse(StrListArg, ["--value=foo,bar"]).value, ["foo", "bar"])
|
||||||
|
assert_equal(extractParse(StrListArg, ["--value=foo,bar"]).value, ["foo", "bar"])
|
||||||
|
assert_equal(extractParse(StrListArg, ["--value='foo,bar'"]).value, ["foo,bar"])
|
||||||
|
assert_equal(extractParse(StrListArg, ["--value='foo, bar'"]).value, ["foo, bar"])
|
||||||
|
assert_equal(extractParse(StrListArg, ['--value="foo, bar"']).value, ["foo, bar"])
|
||||||
|
|
||||||
|
|
||||||
|
class StrDictArg:
|
||||||
|
value: dict[str, str] = cli2.arg(None, "value")
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_arg_dict_str():
|
||||||
|
assert_equal(extractParse(StrDictArg, ["--value:foo=bar"]).value, {"foo": "bar"})
|
||||||
|
assert_equal(
|
||||||
|
extractParse(StrDictArg, ["--value:foo=bar", "--value:baz=qux"]).value,
|
||||||
|
{
|
||||||
|
"foo": "bar",
|
||||||
|
"baz": "qux",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
"""
|
|
@ -2,7 +2,7 @@ from cutekit import model
|
||||||
|
|
||||||
|
|
||||||
def test_direct_deps():
|
def test_direct_deps():
|
||||||
r = model.Registry("")
|
r = model.Registry(model.Project("myproj"))
|
||||||
r._append(model.Component("myapp", requires=["mylib"]))
|
r._append(model.Component("myapp", requires=["mylib"]))
|
||||||
r._append(model.Component("mylib"))
|
r._append(model.Component("mylib"))
|
||||||
t = model.Target("host")
|
t = model.Target("host")
|
||||||
|
@ -10,32 +10,32 @@ def test_direct_deps():
|
||||||
|
|
||||||
resolved = res.resolve("myapp")
|
resolved = res.resolve("myapp")
|
||||||
assert resolved.reason is None
|
assert resolved.reason is None
|
||||||
assert resolved.resolved == ["myapp", "mylib"]
|
assert resolved.required == ["myapp", "mylib"]
|
||||||
|
|
||||||
|
|
||||||
def test_indirect_deps():
|
def test_indirect_deps():
|
||||||
r = model.Registry("")
|
r = model.Registry(model.Project("myproj"))
|
||||||
r._append(model.Component("myapp", requires=["mylib"]))
|
r._append(model.Component("myapp", requires=["mylib"]))
|
||||||
r._append(model.Component("mylib", requires=["myembed"]))
|
r._append(model.Component("mylib", requires=["myembed"]))
|
||||||
r._append(model.Component("myimpl", provides=["myembed"]))
|
r._append(model.Component("myimpl", provides=["myembed"]))
|
||||||
t = model.Target("host")
|
t = model.Target("host")
|
||||||
res = model.Resolver(r, t)
|
res = model.Resolver(r, t)
|
||||||
assert res.resolve("myapp").resolved == ["myapp", "mylib", "myimpl"]
|
assert res.resolve("myapp").required == ["myapp", "mylib", "myimpl"]
|
||||||
|
|
||||||
|
|
||||||
def test_deps_routing():
|
def test_deps_routing():
|
||||||
r = model.Registry("")
|
r = model.Registry(model.Project("myproj"))
|
||||||
r._append(model.Component("myapp", requires=["mylib"]))
|
r._append(model.Component("myapp", requires=["mylib"]))
|
||||||
r._append(model.Component("mylib", requires=["myembed"]))
|
r._append(model.Component("mylib", requires=["myembed"]))
|
||||||
r._append(model.Component("myimplA", provides=["myembed"]))
|
r._append(model.Component("myimplA", provides=["myembed"]))
|
||||||
r._append(model.Component("myimplB", provides=["myembed"]))
|
r._append(model.Component("myimplB", provides=["myembed"]))
|
||||||
t = model.Target("host", routing={"myembed": "myimplB"})
|
t = model.Target("host", routing={"myembed": "myimplB"})
|
||||||
res = model.Resolver(r, t)
|
res = model.Resolver(r, t)
|
||||||
assert res.resolve("myapp").resolved == ["myapp", "mylib", "myimplB"]
|
assert res.resolve("myapp").required == ["myapp", "mylib", "myimplB"]
|
||||||
|
|
||||||
t = model.Target("host", routing={"myembed": "myimplA"})
|
t = model.Target("host", routing={"myembed": "myimplA"})
|
||||||
res = model.Resolver(r, t)
|
res = model.Resolver(r, t)
|
||||||
assert res.resolve("myapp").resolved == ["myapp", "mylib", "myimplA"]
|
assert res.resolve("myapp").required == ["myapp", "mylib", "myimplA"]
|
||||||
|
|
||||||
t = model.Target("host", routing={"myembed": "myimplC"})
|
t = model.Target("host", routing={"myembed": "myimplC"})
|
||||||
res = model.Resolver(r, t)
|
res = model.Resolver(r, t)
|
||||||
|
@ -43,7 +43,7 @@ def test_deps_routing():
|
||||||
|
|
||||||
|
|
||||||
def test_deps_routing_with_props():
|
def test_deps_routing_with_props():
|
||||||
r = model.Registry("")
|
r = model.Registry(model.Project("myproj"))
|
||||||
r._append(model.Component("myapp", requires=["mylib"]))
|
r._append(model.Component("myapp", requires=["mylib"]))
|
||||||
r._append(model.Component("mylib", requires=["myembed"]))
|
r._append(model.Component("mylib", requires=["myembed"]))
|
||||||
r._append(
|
r._append(
|
||||||
|
@ -54,11 +54,11 @@ def test_deps_routing_with_props():
|
||||||
)
|
)
|
||||||
t = model.Target("host", routing={"myembed": "myimplB"}, props={"myprop": "b"})
|
t = model.Target("host", routing={"myembed": "myimplB"}, props={"myprop": "b"})
|
||||||
res = model.Resolver(r, t)
|
res = model.Resolver(r, t)
|
||||||
assert res.resolve("myapp").resolved == ["myapp", "mylib", "myimplB"]
|
assert res.resolve("myapp").required == ["myapp", "mylib", "myimplB"]
|
||||||
|
|
||||||
t = model.Target("host", routing={"myembed": "myimplA"}, props={"myprop": "a"})
|
t = model.Target("host", routing={"myembed": "myimplA"}, props={"myprop": "a"})
|
||||||
res = model.Resolver(r, t)
|
res = model.Resolver(r, t)
|
||||||
assert res.resolve("myapp").resolved == ["myapp", "mylib", "myimplA"]
|
assert res.resolve("myapp").required == ["myapp", "mylib", "myimplA"]
|
||||||
|
|
||||||
t = model.Target("host", routing={"myembed": "myimplC"}, props={"myprop": "c"})
|
t = model.Target("host", routing={"myembed": "myimplC"}, props={"myprop": "c"})
|
||||||
res = model.Resolver(r, t)
|
res = model.Resolver(r, t)
|
||||||
|
@ -68,7 +68,7 @@ def test_deps_routing_with_props():
|
||||||
|
|
||||||
|
|
||||||
def test_deps_routing_with_props_and_requires():
|
def test_deps_routing_with_props_and_requires():
|
||||||
r = model.Registry("")
|
r = model.Registry(model.Project("myproj"))
|
||||||
r._append(model.Component("myapp", requires=["mylib"]))
|
r._append(model.Component("myapp", requires=["mylib"]))
|
||||||
r._append(model.Component("mylib", requires=["myembed"]))
|
r._append(model.Component("mylib", requires=["myembed"]))
|
||||||
r._append(
|
r._append(
|
||||||
|
@ -79,11 +79,11 @@ def test_deps_routing_with_props_and_requires():
|
||||||
)
|
)
|
||||||
t = model.Target("host", routing={"myembed": "myimplB"}, props={"myprop": "b"})
|
t = model.Target("host", routing={"myembed": "myimplB"}, props={"myprop": "b"})
|
||||||
res = model.Resolver(r, t)
|
res = model.Resolver(r, t)
|
||||||
assert res.resolve("myapp").resolved == ["myapp", "mylib", "myimplB"]
|
assert res.resolve("myapp").required == ["myapp", "mylib", "myimplB"]
|
||||||
|
|
||||||
t = model.Target("host", routing={"myembed": "myimplA"}, props={"myprop": "a"})
|
t = model.Target("host", routing={"myembed": "myimplA"}, props={"myprop": "a"})
|
||||||
res = model.Resolver(r, t)
|
res = model.Resolver(r, t)
|
||||||
assert res.resolve("myapp").resolved == ["myapp", "mylib", "myimplA"]
|
assert res.resolve("myapp").required == ["myapp", "mylib", "myimplA"]
|
||||||
|
|
||||||
t = model.Target("host", routing={"myembed": "myimplC"}, props={"myprop": "c"})
|
t = model.Target("host", routing={"myembed": "myimplC"}, props={"myprop": "c"})
|
||||||
res = model.Resolver(r, t)
|
res = model.Resolver(r, t)
|
||||||
|
|
Loading…
Reference in a new issue