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, plugins,
pods, # noqa: F401 this is imported for side effects pods, # noqa: F401 this is imported for side effects
vt100, vt100,
shell,
) )
@ -40,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,
@ -56,21 +54,21 @@ def setupLogger(verbose: bool):
def main() -> int: def main() -> int:
try: try:
shell.mkdir(const.GLOBAL_CK_DIR)
args = cli.parse(sys.argv[1:]) args = cli.parse(sys.argv[1:])
setupLogger(args.consumeOpt("verbose", False) is True) setupLogger(args.consumeOpt("verbose", False) is True)
safemode = args.consumeOpt("safemode", False) is True
if not safemode: const.setup()
plugins.loadAll() plugins.setup(args)
pods.reincarnate(args) pods.setup(args)
cli.exec(args) cli.exec(args)
print()
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

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 @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 registry: model.Registry
target: model.Target target: model.Target
@ -21,6 +37,9 @@ class TargetScope:
target = model.Target.use(args) target = model.Target.use(args)
return TargetScope(registry, target) return TargetScope(registry, target)
def key(self) -> str:
return super().key() + "/" + self.target.id + "/" + self.target.hashid
def openComponentScope(self, c: model.Component): def openComponentScope(self, c: model.Component):
return ComponentScope(self.registry, self.target, c) return ComponentScope(self.registry, self.target, c)
@ -29,6 +48,9 @@ class TargetScope:
class ComponentScope(TargetScope): class ComponentScope(TargetScope):
component: model.Component component: model.Component
def key(self) -> str:
return super().key() + "/" + self.component.id
def openComponentScope(self, c: model.Component): def openComponentScope(self, c: model.Component):
return ComponentScope(self.registry, self.target, c) return ComponentScope(self.registry, self.target, c)
@ -76,6 +98,8 @@ def _computeCinc(scope: TargetScope) -> str:
for c in scope.registry.iterEnabled(scope.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))
@ -108,18 +132,16 @@ def buildpath(scope: ComponentScope, path) -> Path:
def subdirs(scope: ComponentScope) -> list[str]: def subdirs(scope: ComponentScope) -> list[str]:
registry = scope.registry
target = scope.target
component = scope.component component = scope.component
result = [component.dirname()] result = [component.dirname()]
for subs in component.subdirs: for subs in component.subdirs:
result.append(os.path.join(component.dirname(), subs)) result.append(os.path.join(component.dirname(), subs))
for inj in component.resolved[target.id].injected: # for inj in component.resolved[target.id].injected:
injected = registry.lookup(inj, model.Component) # injected = registry.lookup(inj, model.Component)
assert injected is not None # model.Resolver has already checked this # assert injected is not None # model.Resolver has already checked this
result.extend(subdirs(scope)) # result.extend(subdirs(scope.openComponentScope(injected)))
return result return result
@ -134,13 +156,31 @@ def compile(
res: list[str] = [] res: list[str] = []
for src in srcs: for src in srcs:
rel = Path(src).relative_to(scope.component.dirname()) 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] 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)
res.append(str(dest)) res.append(str(dest))
return res 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 ------------------------------------------------------------- # # --- Ressources ------------------------------------------------------------- #
@ -195,26 +235,12 @@ def link(
w.newline() w.newline()
out = outfile(scope) 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) res = compileRes(w, scope)
libs = collectLibs(scope) objs = compileObjs(w, scope)
if scope.component.type == model.Kind.LIB: if scope.component.type == model.Kind.LIB:
w.build(out, "ar", objs, implicit=res) w.build(out, "ar", objs, implicit=res)
else: else:
libs = collectLibs(scope)
w.build(out, "ld", objs + libs, implicit=res) w.build(out, "ld", objs + libs, implicit=res)
return out return out
@ -300,12 +326,12 @@ def build(
@cli.command("b", "builder", "Build/Run/Clean 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):
pass pass
@cli.command("b", "builder/build", "Build a component or all components") @cli.command("b", "builder/build", "Build a component or all components")
def buildBuildCmd(args: cli.Args): def _(args: cli.Args):
scope = TargetScope.use(args) scope = TargetScope.use(args)
componentSpec = args.consumeArg() componentSpec = args.consumeArg()
component = None component = None
@ -315,7 +341,7 @@ def buildBuildCmd(args: cli.Args):
@cli.command("r", "builder/run", "Run a component") @cli.command("r", "builder/run", "Run a component")
def buildRunCmd(args: cli.Args): def runCmd(args: cli.Args):
scope = TargetScope.use(args) scope = TargetScope.use(args)
debug = args.consumeOpt("debug", False) is True 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") @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 # 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__")
buildRunCmd(args) runCmd(args)
@cli.command("d", "builder/debug", "Debug a component") @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 # 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
buildRunCmd(args) runCmd(args)
@cli.command("c", "builder/clean", "Clean build files") @cli.command("c", "builder/clean", "Clean build files")
def buildCleanCmd(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", "builder/nuke", "Clean all build files and caches") @cli.command("n", "builder/nuke", "Clean all build files and caches")
def buildNukeCmd(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)

View file

@ -1,6 +1,20 @@
import os import os
import sys 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 = (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__))
@ -16,4 +30,17 @@ 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)

View file

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

View file

@ -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)

View file

@ -174,18 +174,18 @@ class Project(Manifest):
@cli.command("m", "model", "Manage the model") @cli.command("m", "model", "Manage the model")
def modelCmd(args: cli.Args): def _(args: cli.Args):
pass pass
@cli.command("i", "model/install", "Install required external packages") @cli.command("i", "model/install", "Install required external packages")
def modelInstallCmd(args: cli.Args): def _(args: cli.Args):
project = Project.use(args) project = Project.use(args)
Project.fetchs(project.extern) Project.fetchs(project.extern)
@cli.command("I", "model/init", "Initialize a new project") @cli.command("I", "model/init", "Initialize a new project")
def modelInitCmd(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)
@ -275,7 +275,10 @@ class Target(Manifest):
@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) -> "Target":
@ -310,7 +313,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)
@ -593,7 +596,7 @@ class Registry(DataClassJsonMixin):
for c in r.iter(Component): for c in r.iter(Component):
if c.isEnabled(target)[0]: if c.isEnabled(target)[0]:
for inject in c.injects: for inject in c.injects:
victim = r.lookup(inject, Component) victim = r.lookup(inject, Component, includeProvides=True)
if not victim: if not victim:
raise RuntimeError(f"Cannot find component '{inject}'") raise RuntimeError(f"Cannot find component '{inject}'")
victim.resolved[target.id].injected.append(c.id) 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") @cli.command("l", "model/list", "List all components and targets")
def modelListCmd(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))

View file

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

View file

@ -10,7 +10,7 @@ import fnmatch
import platform import platform
import logging import logging
import tempfile import tempfile
import dataclasses as dt
from typing import Optional from typing import Optional
from . import const from . import const
@ -18,15 +18,13 @@ from . import const
_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 nodename: str
): release: str
self.sysname = sysname version: str
self.nodename = nodename machine: str
self.release = release
self.version = version
self.machine = machine
def uname() -> Uname: def uname() -> Uname:
@ -131,12 +129,13 @@ 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: str | None = None) -> bool:
_logger.debug(f"Executing {args}") _logger.debug(f"Executing {args}")
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,
) )
@ -274,3 +273,10 @@ 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

View file

@ -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:]

View file

@ -11,7 +11,7 @@ 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!")
``` ```