This commit is contained in:
Sleepy Monax 2023-12-09 13:18:48 +01:00
parent cc88242cef
commit ea0e5613e8
12 changed files with 221 additions and 79 deletions

View file

@ -11,6 +11,7 @@ from . import (
plugins,
pods, # noqa: F401 this is imported for side effects
vt100,
shell,
)
@ -40,10 +41,7 @@ def setupLogger(verbose: bool):
if projectRoot is not None:
logFile = os.path.join(projectRoot.dirname(), const.PROJECT_LOG_FILE)
# create the directory if it doesn't exist
logDir = os.path.dirname(logFile)
if not os.path.isdir(logDir):
os.makedirs(logDir)
shell.mkdir(os.path.dirname(logFile))
logging.basicConfig(
level=logging.INFO,
@ -56,21 +54,21 @@ def setupLogger(verbose: bool):
def main() -> int:
try:
shell.mkdir(const.GLOBAL_CK_DIR)
args = cli.parse(sys.argv[1:])
setupLogger(args.consumeOpt("verbose", False) is True)
safemode = args.consumeOpt("safemode", False) is True
if not safemode:
plugins.loadAll()
pods.reincarnate(args)
const.setup()
plugins.setup(args)
pods.setup(args)
cli.exec(args)
print()
return 0
except RuntimeError as e:
logging.exception(e)
cli.error(str(e))
cli.usage()
print()
return 1
except KeyboardInterrupt:
print()
return 1
return 1

62
cutekit/bootstrap.sh Normal file
View file

@ -0,0 +1,62 @@
#!/bin/env bash
# version: {version}
set -e
if [ "$CUTEKIT_NOVENV" == "1" ]; then
echo "CUTEKIT_NOVENV is set, skipping virtual environment setup."
exec cutekit $@
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..."
python3 -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 "0.7-dev"
else
echo "CuteKit already downloaded."
fi
echo "Installing Tools..."
pip3 install -e .cutekit/tools/cutekit
pip3 install -r meta/plugins/requirements.txt
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 $@

View file

@ -11,7 +11,23 @@ _logger = logging.getLogger(__name__)
@dt.dataclass
class TargetScope:
class Scope:
registry: model.Registry
@staticmethod
def use(args: cli.Args) -> "Scope":
registry = model.Registry.use(args)
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
@ -21,6 +37,9 @@ class TargetScope:
target = model.Target.use(args)
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)
@ -29,6 +48,9 @@ class TargetScope:
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)
@ -76,6 +98,8 @@ def _computeCinc(scope: TargetScope) -> str:
for c in scope.registry.iterEnabled(scope.target):
if "cpp-root-include" in c.props:
res.add(c.dirname())
elif "cpp-excluded" in c.props:
pass
elif c.type == model.Kind.LIB:
res.add(str(Path(c.dirname()).parent))
@ -108,18 +132,16 @@ def buildpath(scope: ComponentScope, path) -> Path:
def subdirs(scope: ComponentScope) -> list[str]:
registry = scope.registry
target = scope.target
component = scope.component
result = [component.dirname()]
for subs in component.subdirs:
result.append(os.path.join(component.dirname(), subs))
for inj in component.resolved[target.id].injected:
injected = registry.lookup(inj, model.Component)
assert injected is not None # model.Resolver has already checked this
result.extend(subdirs(scope))
# for inj in component.resolved[target.id].injected:
# injected = registry.lookup(inj, model.Component)
# assert injected is not None # model.Resolver has already checked this
# result.extend(subdirs(scope.openComponentScope(injected)))
return result
@ -134,13 +156,31 @@ def compile(
res: list[str] = []
for src in srcs:
rel = Path(src).relative_to(scope.component.dirname())
dest = buildpath(scope, "obj") / rel.with_suffix(".o")
dest = buildpath(scope, path="obj") / rel.with_suffix(".o")
t = scope.target.tools[rule]
w.build(str(dest), rule, inputs=src, order_only=t.files)
res.append(str(dest))
return res
def compileObjs(w: ninja.Writer, scope: ComponentScope) -> list[str]:
objs = []
objs += compile(w, scope, "cc", wilcard(scope, ["*.c"]))
objs += compile(
w,
scope,
"cxx",
wilcard(scope, ["*.cpp", "*.cc", "*.cxx"]),
)
objs += compile(
w,
scope,
"as",
wilcard(scope, ["*.s", "*.asm", "*.S"]),
)
return objs
# --- Ressources ------------------------------------------------------------- #
@ -195,26 +235,12 @@ def link(
w.newline()
out = outfile(scope)
objs = []
objs += compile(w, scope, "cc", wilcard(scope, ["*.c"]))
objs += compile(
w,
scope,
"cxx",
wilcard(scope, ["*.cpp", "*.cc", "*.cxx"]),
)
objs += compile(
w,
scope,
"as",
wilcard(scope, ["*.s", "*.asm", "*.S"]),
)
res = compileRes(w, scope)
libs = collectLibs(scope)
objs = compileObjs(w, scope)
if scope.component.type == model.Kind.LIB:
w.build(out, "ar", objs, implicit=res)
else:
libs = collectLibs(scope)
w.build(out, "ld", objs + libs, implicit=res)
return out
@ -300,12 +326,12 @@ def build(
@cli.command("b", "builder", "Build/Run/Clean a component or all components")
def buildCmd(args: cli.Args):
def _(args: cli.Args):
pass
@cli.command("b", "builder/build", "Build a component or all components")
def buildBuildCmd(args: cli.Args):
def _(args: cli.Args):
scope = TargetScope.use(args)
componentSpec = args.consumeArg()
component = None
@ -315,7 +341,7 @@ def buildBuildCmd(args: cli.Args):
@cli.command("r", "builder/run", "Run a component")
def buildRunCmd(args: cli.Args):
def runCmd(args: cli.Args):
scope = TargetScope.use(args)
debug = args.consumeOpt("debug", False) is True
@ -337,28 +363,28 @@ def buildRunCmd(args: cli.Args):
@cli.command("t", "builder/test", "Run all test targets")
def buildTestCmd(args: cli.Args):
def _(args: cli.Args):
# This is just a wrapper around the `run` command that try
# to run a special hook component named __tests__.
args.args.insert(0, "__tests__")
buildRunCmd(args)
runCmd(args)
@cli.command("d", "builder/debug", "Debug a component")
def buildDebugCmd(args: cli.Args):
def _(args: cli.Args):
# This is just a wrapper around the `run` command that
# always enable debug mode.
args.opts["debug"] = True
buildRunCmd(args)
runCmd(args)
@cli.command("c", "builder/clean", "Clean build files")
def buildCleanCmd(args: cli.Args):
def _(args: cli.Args):
model.Project.use(args)
shell.rmrf(const.BUILD_DIR)
@cli.command("n", "builder/nuke", "Clean all build files and caches")
def buildNukeCmd(args: cli.Args):
def _(args: cli.Args):
model.Project.use(args)
shell.rmrf(const.PROJECT_CK_DIR)

View file

@ -1,6 +1,20 @@
import os
import sys
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 ''}"
MODULE_DIR = os.path.dirname(os.path.realpath(__file__))
@ -16,4 +30,17 @@ TARGETS_DIR = os.path.join(META_DIR, "targets")
DEFAULT_REPO_TEMPLATES = "cute-engineering/cutekit-templates"
DESCRIPTION = "A build system and package manager for low-level software development"
PROJECT_LOG_FILE = os.path.join(PROJECT_CK_DIR, "cutekit.log")
GLOBAL_LOG_FILE = os.path.join(os.path.expanduser("~"), ".cutekit", "cutekit.log")
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)

View file

@ -1,5 +1,8 @@
#!/bin/env bash
# This script makes sure that the virtual environment is
# set up and that the plugins requirements are installed.
set -e
export PY=python3.11
@ -30,5 +33,6 @@ else
source /tools/venv/bin/activate
fi
cd /project
export PYTHONPATH=/tools
$PY -m cutekit "$@"
$PY -m cutekit $@

View file

@ -46,7 +46,7 @@ def view(
g.node(
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,
style="filled",
fillcolor=fillcolor,
@ -67,7 +67,7 @@ def view(
elif showDisabled:
g.node(
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",
style="filled",
fontcolor="#999999",
@ -84,7 +84,7 @@ def view(
@cli.command("g", "graph", "Show the dependency graph")
def graphCmd(args: cli.Args):
def _(args: cli.Args):
registry = model.Registry.use(args)
target = model.Target.use(args)

View file

@ -174,18 +174,18 @@ class Project(Manifest):
@cli.command("m", "model", "Manage the model")
def modelCmd(args: cli.Args):
def _(args: cli.Args):
pass
@cli.command("i", "model/install", "Install required external packages")
def modelInstallCmd(args: cli.Args):
def _(args: cli.Args):
project = Project.use(args)
Project.fetchs(project.extern)
@cli.command("I", "model/init", "Initialize a new project")
def modelInitCmd(args: cli.Args):
def _(args: cli.Args):
import requests
repo = args.consumeOpt("repo", const.DEFAULT_REPO_TEMPLATES)
@ -275,7 +275,10 @@ class Target(Manifest):
@property
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
def use(args: cli.Args) -> "Target":
@ -310,7 +313,7 @@ class Resolved:
@dt.dataclass
class Component(Manifest):
decription: str = dt.field(default="(No description)")
description: str = dt.field(default="(No description)")
props: Props = dt.field(default_factory=dict)
tools: Tools = dt.field(default_factory=dict)
enableIf: dict[str, list[Any]] = dt.field(default_factory=dict)
@ -593,7 +596,7 @@ class Registry(DataClassJsonMixin):
for c in r.iter(Component):
if c.isEnabled(target)[0]:
for inject in c.injects:
victim = r.lookup(inject, Component)
victim = r.lookup(inject, Component, includeProvides=True)
if not victim:
raise RuntimeError(f"Cannot find component '{inject}'")
victim.resolved[target.id].injected.append(c.id)
@ -622,7 +625,7 @@ class Registry(DataClassJsonMixin):
@cli.command("l", "model/list", "List all components and targets")
def modelListCmd(args: cli.Args):
def _(args: cli.Args):
registry = Registry.use(args)
components = list(registry.iter(Component))

View file

@ -2,7 +2,7 @@ import logging
import os
import sys
from . import shell, model, const
from . import shell, model, const, cli
import importlib.util as importlib
@ -43,3 +43,8 @@ def loadAll():
for files in shell.readdir(pluginDir):
if files.endswith(".py"):
load(os.path.join(pluginDir, files))
def setup(args: cli.Args):
if not bool(args.consumeOpt("safemode", False)):
loadAll()

View file

@ -7,7 +7,7 @@ from . import cli, model, shell, vt100
podPrefix = "CK__"
projectRoot = "/self"
projectRoot = "/project"
toolingRoot = "/tools"
defaultPodName = f"{podPrefix}default"
defaultPodImage = "ubuntu"
@ -16,8 +16,9 @@ defaultPodImage = "ubuntu"
@dt.dataclass
class Image:
id: str
like: str
image: str
init: list[str]
setup: list[str]
@dt.dataclass
@ -28,6 +29,7 @@ class Pod:
IMAGES: dict[str, Image] = {
"ubuntu": Image(
"ubuntu",
"ubuntu",
"ubuntu:jammy",
[
@ -36,6 +38,7 @@ IMAGES: dict[str, Image] = {
],
),
"debian": Image(
"debian",
"debian",
"debian:bookworm",
[
@ -44,6 +47,7 @@ IMAGES: dict[str, Image] = {
],
),
"alpine": Image(
"alpine",
"alpine",
"alpine:3.18",
[
@ -52,6 +56,7 @@ IMAGES: dict[str, Image] = {
],
),
"arch": Image(
"arch",
"arch",
"archlinux:latest",
[
@ -60,6 +65,7 @@ IMAGES: dict[str, Image] = {
],
),
"fedora": Image(
"fedora",
"fedora",
"fedora:39",
[
@ -70,7 +76,7 @@ IMAGES: dict[str, Image] = {
}
def reincarnate(args: cli.Args):
def setup(args: cli.Args):
"""
Reincarnate cutekit within a docker container, this is
useful for cross-compiling
@ -95,6 +101,7 @@ def reincarnate(args: cli.Args):
"-it",
pod,
"/tools/cutekit/entrypoint.sh",
"--reincarnated",
*args.args,
)
sys.exit(0)
@ -103,15 +110,15 @@ def reincarnate(args: cli.Args):
@cli.command("p", "pod", "Manage pods")
def podCmd(args: cli.Args):
def _(args: cli.Args):
pass
@cli.command("c", "pod/create", "Create a new pod")
def podCreateCmd(args: cli.Args):
def _(args: cli.Args):
"""
Create a new development pod with cutekit installed and the current
project mounted at /self
project mounted at /project
"""
project = model.Project.ensure()
@ -144,7 +151,7 @@ def podCreateCmd(args: cli.Args):
)
print(f"Initializing pod '{name[len(podPrefix) :]}'...")
for cmd in image.init:
for cmd in image.setup:
print(vt100.p(cmd))
exitCode, ouput = container.exec_run(f"/bin/bash -c '{cmd}'", demux=True)
if exitCode != 0:
@ -154,7 +161,7 @@ def podCreateCmd(args: cli.Args):
@cli.command("k", "pod/kill", "Stop and remove a pod")
def podKillCmd(args: cli.Args):
def _(args: cli.Args):
client = docker.from_env()
name = str(args.consumeOpt("name", defaultPodName))
if not name.startswith(podPrefix):
@ -170,13 +177,13 @@ def podKillCmd(args: cli.Args):
@cli.command("s", "pod/shell", "Open a shell in a pod")
def podShellCmd(args: cli.Args):
def _(args: cli.Args):
args.args.insert(0, "/bin/bash")
podExecCmd(args)
@cli.command("l", "pod/list", "List all pods")
def podListCmd(args: cli.Args):
def _(args: cli.Args):
client = docker.from_env()
hasPods = False
for container in client.containers.list(all=True):

View file

@ -10,7 +10,7 @@ import fnmatch
import platform
import logging
import tempfile
import dataclasses as dt
from typing import Optional
from . import const
@ -18,15 +18,13 @@ from . import const
_logger = logging.getLogger(__name__)
@dt.dataclass
class Uname:
def __init__(
self, sysname: str, nodename: str, release: str, version: str, machine: str
):
self.sysname = sysname
self.nodename = nodename
self.release = release
self.version = version
self.machine = machine
sysname: str
nodename: str
release: str
version: str
machine: str
def uname() -> Uname:
@ -131,12 +129,13 @@ def wget(url: str, path: Optional[str] = None) -> str:
return path
def exec(*args: str, quiet: bool = False) -> bool:
def exec(*args: str, quiet: bool = False, cwd: str | None = None) -> bool:
_logger.debug(f"Executing {args}")
try:
proc = subprocess.run(
args,
cwd=cwd,
stdout=sys.stdout if not quiet else subprocess.PIPE,
stderr=sys.stderr if not quiet else subprocess.PIPE,
)
@ -274,3 +273,10 @@ def which(cmd: str) -> Optional[str]:
Find the path of a command
"""
return shutil.which(cmd)
def nproc() -> int:
"""
Return the number of processors
"""
return os.cpu_count() or 1

View file

@ -29,6 +29,10 @@ def hash(
return hashlib.sha256(data.encode("utf-8")).hexdigest()
def randomHash() -> str:
return hashlib.sha256(os.urandom(32)).hexdigest()
def camelCase(s: str) -> str:
s = "".join(x for x in s.title() if x != "_" and x != "-")
s = s[0].lower() + s[1:]

View file

@ -11,7 +11,7 @@ For example you can add a new command to the CLI:
from cutekit import cli
@cli.command("h", "hello", "Print hello world")
def bootCmd(args: cli.Args) -> None:
def _(args: cli.Args) -> None:
print("Hello world!")
```