Compare commits

...

26 commits

Author SHA1 Message Date
Sleepy Monax aae3ccd06e wip 2023-06-12 19:09:17 +02:00
Sleepy Monax 515893de5f feat: Make the init command more friendly. 2023-06-09 19:32:20 +02:00
Sleepy Monax d46d268156 feat: Add utils.isNewer 2023-06-09 19:31:17 +02:00
Sleepy Monax d9e7be504d Update LICENSE 2023-06-09 19:30:57 +02:00
Jordan ⌨️ c3ead4092c chore: improve type hinting 2023-06-09 20:47:11 +02:00
Jordan ⌨️ cc8aafb300 feat: added abspath to jexpr 2023-06-02 15:57:56 +02:00
Sleepy Monax c33e5316bd fix: Make sure with have a projet before loading plugins. 2023-05-30 08:00:51 +02:00
Jordan ⌨️ f3dcfd8bf0 feat: project can loads targets from external deps 2023-05-29 21:59:24 +02:00
Jordan ⌨️ 0396c8165a feat: project can loads plugins from external deps 2023-05-29 21:47:05 +02:00
Jordan ⌨️ 0f7a5f6502 fix: check if templates is valid + exists 2023-05-29 18:13:00 +02:00
Jordan ⌨️ edc4119bf7 fix: error when no template is provided to initCmd 2023-05-29 18:03:08 +02:00
Jordan ⌨️ 2fd545f313 feat: added recursive package resolution 2023-05-29 17:38:03 +02:00
Keyboard Slayer 1c5e369926
chore: update license 2023-05-29 00:55:07 +02:00
Sleepy Monax 004a5f4518 meta: Renamed osdk -> cutekit + improved logging + better error handling. 2023-05-28 11:28:52 +02:00
Jordan ⌨️ f03051df7e fix: better init command 2023-05-27 23:42:57 +02:00
Jordan ⌨️ 5ef1a586f5 feat: added fetching of osdk templates 2023-05-27 23:08:06 +02:00
Sleepy Monax 10f4a29e89 Use default python logger 2023-05-26 21:41:41 +01:00
Sleepy Monax 5f6ca201fc Fix invalid computation of hask keys. 2023-04-20 08:18:37 +02:00
Sleepy Monax 49bdb4ebad Added tune mixin. 2023-04-18 08:24:41 +02:00
Sleepy Monax e6f245d2ad Added support for res directory. 2023-04-16 01:03:06 +02:00
Sleepy Monax 175f4e0c3f Added shell.cpTree() 2023-04-09 19:59:35 +02:00
Sleepy Monax 0534c2304a Don't add application to the include path. 2023-04-04 14:26:59 +02:00
Sleepy Monax 745918c003 Fixed missing types. 2023-03-25 15:26:34 +01:00
Sleepy Monax 0d4a17fb26 Added test command. 2023-03-11 15:29:31 +01:00
Sleepy Monax 782065ec10 Pass arguments to commands 2023-03-11 14:34:07 +01:00
Sleepy Monax 36108a0fc7 Fix broken osdk version command. 2023-02-26 20:26:33 +01:00
32 changed files with 1702 additions and 697 deletions

View file

@ -17,7 +17,6 @@ permissions:
jobs: jobs:
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

View file

@ -1,6 +1,7 @@
MIT License MIT License
Copyright (c) 2022 skift Copyright (c) 2022, the skiftOS Developers
Copyright (c) 2022-2023, Cute Engineering
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -1,30 +1,39 @@
# osdk <br/>
<br/>
The operating system development kit <br/>
<p align="center">
<img src="logo.png" width="200" height="200">
</p>
<h1 align="center">CuteKit</h1>
<p align="center">
The Cute build system and package manager
</p>
<br/>
<br/>
<br/>
## Table of contents ## Table of contents
- [osdk](#osdk) - [Table of contents](#table-of-contents)
- [Table of contents](#table-of-contents) - [Macros](#macros)
- [Macros](#macros) - [`@latest`](#latest)
- [`@latest`](#latest) - [`@uname`](#uname)
- [`@uname`](#uname) - [`@include`](#include)
- [`@include`](#include) - [`@join`](#join)
- [`@join`](#join) - [`@concat`](#concat)
- [`@concat`](#concat) - [`@exec`](#exec)
- [`@exec`](#exec) - [Manifest file format](#manifest-file-format)
- [Manifest file format](#manifest-file-format) - [`id`](#id)
- [`id`](#id) - [`type`](#type)
- [`type`](#type) - [`description`](#description)
- [`description`](#description) - [`enabledIf`](#enabledif)
- [`enabledIf`](#enabledif) - [`requires`](#requires)
- [`requires`](#requires) - [`provides`](#provides)
- [`provides`](#provides) - [Target file format](#target-file-format)
- [Target file format](#target-file-format) - [`id`](#id-1)
- [`id`](#id-1) - [`type`](#type-1)
- [`type`](#type-1) - [`props`](#props)
- [`props`](#props) - [`tools`](#tools)
- [`tools`](#tools)
## Macros ## Macros
@ -197,7 +206,7 @@ Exemple:
} }
``` ```
Theses values are exposed the translation unit as `__osdk_{prop}__`. Theses values are exposed the translation unit as `__ck_{prop}__`.
### `tools` ### `tools`

50
cutekit/__init__.py Normal file
View file

@ -0,0 +1,50 @@
import sys
import os
import logging
from cutekit import const, project, vt100, plugins, cmds
def setupLogger(verbose: bool):
if verbose:
logging.basicConfig(
level=logging.INFO,
format=f"{vt100.CYAN}%(asctime)s{vt100.RESET} {vt100.YELLOW}%(levelname)s{vt100.RESET} %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
else:
projectRoot = project.root()
logFile = const.GLOBAL_LOG_FILE
if projectRoot is not None:
logFile = os.path.join(projectRoot, const.PROJECT_LOG_FILE)
# create the directory if it doesn't exist
logDir = os.path.dirname(logFile)
if not os.path.isdir(logDir):
os.makedirs(logDir)
logging.basicConfig(
level=logging.INFO,
filename=logFile,
filemode="w",
format=f"%(asctime)s %(levelname)s %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
def main() -> int:
try:
setupLogger(False)
plugins.loadAll()
cmds.exec(sys.argv)
print()
return 0
except RuntimeError as e:
logging.exception(e)
cmds.error(str(e))
cmds.usage()
print()
return 1
except KeyboardInterrupt:
print()
return 1

View file

@ -1,4 +1,7 @@
Value = str | bool | int from typing import Optional, Union
Value = Union[str, bool, int]
class Args: class Args:
@ -17,23 +20,23 @@ class Args:
del self.opts[key] del self.opts[key]
return result return result
def consumeOpt(self, key: str, default: Value) -> Value: def consumeOpt(self, key: str, default: Value = False) -> Value:
if key in self.opts: if key in self.opts:
result = self.opts[key] result = self.opts[key]
del self.opts[key] del self.opts[key]
return result return result
return default return default
def tryConsumeOpt(self, key: str) -> Value | None: def tryConsumeOpt(self, key: str) -> Optional[Value]:
if key in self.opts: if key in self.opts:
result = self.opts[key] result = self.opts[key]
del self.opts[key] del self.opts[key]
return result return result
return None return None
def consumeArg(self) -> str | None: def consumeArg(self, default: Optional[str] = None) -> Optional[str]:
if len(self.args) == 0: if len(self.args) == 0:
return None return default
first = self.args[0] first = self.args[0]
del self.args[0] del self.args[0]

651
cutekit/args2.py Normal file
View file

@ -0,0 +1,651 @@
from typing import TypeVar, Generic, Optional, Callable, Any
import sys
from cutekit import vt100
T = TypeVar('T')
# --- Base ------------------------------------------------------------------- #
class Scan:
_argv: list[str]
_off: int = 0
def __init__(self, argv: list[str]):
self._argv = argv
def any(self) -> bool:
return self._off < len(self._argv)
def peek(self) -> str:
return self._argv[self._off]
def tryPeek(self, default: str = "") -> str:
if self.any():
return self.peek()
return default
def isHelp(self) -> bool:
return self.tryPeek() == "-h" or self.tryPeek() == "--help"
def next(self) -> str:
result = self._argv[self._off]
self._off += 1
return result
def consumed(self) -> list[str]:
return self._argv[:self._off]
class Emit:
_buf: str = ""
_ident: int = 0
_newline: int = 0
def __init__(self) -> None:
pass
def ident(self) -> None:
self._ident += 1
def unident(self) -> None:
self._ident -= 1
def emit(self, text: str) -> None:
for c in text:
if c == "\n":
self._newline += 1
continue
if c != "\n" and self._newline:
self.flushNewline()
self._buf += " " * self._ident
self._buf += c
def newline(self) -> None:
self._newline += 1
def flushNewline(self) -> None:
if self._newline:
self._buf += "\n" * min(2, self._newline)
self._newline = 0
def finish(self) -> str:
self._newline = min(1, self._newline)
self.flushNewline()
if self._ident:
raise RuntimeError("Unbalanced ident")
return self._buf
class Node:
def eval(self, s: Scan, args: Any) -> bool:
raise NotImplementedError()
def usage(self, e: Emit) -> None:
raise NotImplementedError()
def help(self, e: Emit) -> None:
raise NotImplementedError()
# --- Quatifiers ------------------------------------------------------------ #
class AllOf(Node):
parts: list[Node]
def __init__(self, *parts: Node):
self.parts = list(parts)
def eval(self, s: Scan, args: Any) -> bool:
if s.isHelp():
e = Emit()
self.help(e)
print(e.finish())
raise SystemExit(0)
for part in self.parts:
part.eval(s, args)
return True
def usage(self, e: Emit) -> None:
if len(self.parts) == 1:
self.parts[0].usage(e)
return
e.emit("(")
first = True
for part in self.parts:
if not first:
e.emit(" ")
first = False
part.usage(e)
e.emit(")")
def help(self, e: Emit) -> None:
for part in self.parts:
part.help(e)
class OneOf(Node):
parts: list[Node]
def __init__(self, *parts: Node, title: Optional[str] = None):
self.parts = list(parts)
def eval(self, s: Scan, args: Any) -> bool:
if s.isHelp():
e = Emit()
self.help(e)
print(e.finish())
raise SystemExit(0)
for part in self.parts:
if part.eval(s, args):
return True
e = Emit()
self.usage(e)
if not s.any():
raise RuntimeError(
f"Unexpected end of input, expected one of " + e.finish())
raise RuntimeError(
f"Unexpected {s.peek()}, expected one of " + e.finish())
def usage(self, e: Emit) -> None:
e.emit("(")
first = True
for part in self.parts:
if not first:
e.emit(" | ")
first = False
part.usage(e)
e.emit(")")
def help(self, e: Emit) -> None:
for part in self.parts:
part.help(e)
class ZeroOrMoreOf(Node):
operands: list[Node]
def __init__(self, *operands: Node):
self.operands = list(operands)
def eval(self, s: Scan, args: Any) -> bool:
if s.isHelp():
e = Emit()
self.help(e)
print(e.finish())
raise SystemExit(0)
any = True
while s.any() and any:
any = False
for part in self.operands:
if not part.eval(s, args):
break
return True
def usage(self, e: Emit) -> None:
if len(self.operands) == 1:
e.emit("[")
self.operands[0].usage(e)
e.emit("...]")
return
e.emit("[")
first = True
for part in self.operands:
if not first:
e.emit(" | ")
first = False
part.usage(e)
e.emit("]+")
def help(self, e: Emit) -> None:
for part in self.operands:
part.help(e)
class OneOrMoreOf(Node):
operands: list[Node]
def __init__(self, *operands: Node):
self.operands = list(operands)
def eval(self, s: Scan, args: Any) -> bool:
if s.isHelp():
e = Emit()
self.help(e)
print(e.finish())
raise SystemExit(0)
any = False
while s.any():
for operand in self.operands:
if operand.eval(s, args):
any = True
break
if not any:
break
if not any:
e = Emit()
self.usage(e)
if not s.any():
raise RuntimeError(
f"Unexpected end of input, expected one or more {vt100.BOLD + vt100.GREEN}{e.finish()}{vt100.RESET}")
raise RuntimeError(
f"Unexpected {vt100.BOLD + vt100.BROWN}{s.peek()}{vt100.RESET}, expected one or more {vt100.BOLD + vt100.GREEN}{e.finish()}{vt100.RESET}")
return True
def usage(self, e: Emit) -> None:
if len(self.operands) == 1:
e.emit("")
self.operands[0].usage(e)
e.emit("+")
return
e.emit("[")
first = True
for operand in self.operands:
if not first:
e.emit(" | ")
first = False
e.emit("[")
operand.usage(e)
e.emit("]")
e.emit("]+")
def help(self, e: Emit) -> None:
for operand in self.operands:
operand.help(e)
# --- Keywords, Options and Operands ---------------------------------------- #
class Keyword(Node):
name: str
shorthand: str
description: str
syntax: Optional[Node]
run: Optional[Callable[[Any], None]]
def __init__(self, name: str, shorthand: str = "", description: str = "", syntax: Optional[Node] = None, run: Optional[Callable[[Any], None]] = None):
self.name = name
self.shorthand = shorthand
self.description = description
self.syntax = syntax
self.run = run
def eval(self, s: Scan, args: Any) -> bool:
if s.any() and (s.peek() == self.name or s.peek() == self.shorthand):
s.next()
if self.syntax:
try:
self.syntax.eval(s, args)
except RuntimeError as e:
print(f"{vt100.RED}Error:{vt100.RESET} {e}")
print("Try: --help for more information")
raise SystemExit(1)
if self.run:
self.run(args)
return True
return False
def usage(self, e: Emit) -> None:
if self.shorthand:
e.emit(f'({self.shorthand} | ')
e.emit(self.name)
if self.shorthand:
e.emit(")")
if self.syntax:
e.emit(" ")
self.syntax.usage(e)
def help(self, e: Emit) -> None:
if self.shorthand:
e.emit(f" {vt100.BOLD + vt100.GREEN}{self.shorthand}{vt100.RESET} ")
else:
e.emit(" ")
e.emit(f"{self.name} -")
e.emit(f" {self.description}\n")
e.ident()
e.newline()
e.emit(f"{vt100.BOLD}Usage:{vt100.RESET}")
e.newline()
self.usage(e)
e.newline()
if self.syntax:
self.syntax.help(e)
e.unident()
e.newline()
class Option(Node, Generic[T]):
name: str
key: str
shorthand: str
description: str
default: Optional[T]
def __init__(self, name: str, key: Optional[str] = None, shorthand: str = "", description: str = "", default: Optional[T] = None):
self.name = name
self.key = key or name
self.shorthand = shorthand
self.description = description
self.default = default
def eval(self, s: Scan, args: Any) -> bool:
if not s.any():
return False
opt = s.peek()
if s.peek().startswith('--' + self.name) or s.peek().startswith('-' + self.shorthand):
s.next()
if '=' in opt:
value = opt.split('=')[1]
else:
if not s.any():
raise RuntimeError(
f"Missing value for {vt100.BOLD + vt100.BROWN}{opt}{vt100.RESET}")
value = s.next()
args[self.key] = value
return True
return False
def usage(self, e: Emit) -> None:
e.emit("(")
if self.shorthand:
e.emit(f'(-{self.shorthand} | ')
e.emit(f'--{self.name}')
if self.shorthand:
e.emit(")")
if self.default:
e.emit(f'=<{self.key}>')
e.emit(")")
def help(self, e: Emit) -> None:
if self.shorthand:
e.emit(f"-{self.shorthand}, ")
else:
e.emit(" ")
e.emit(f"--{self.name}")
e.emit(f" {self.description}")
if self.default:
e.emit(f" (default: {self.default})")
e.emit("\n")
class Operand(Node, Generic[T]):
name: str
key: str
description: str
default: Optional[T]
def __init__(self, name: str, key: Optional[str] = None, description: str = "", default: Optional[T] = None):
self.name = name
self.key = key or name
self.description = description
self.default = default
def eval(self, s: Scan, args: Any) -> bool:
if s.any() and not s.peek().startswith('-'):
args[self.key] = s.next()
return True
return False
def usage(self, e: Emit) -> None:
e.emit(f"<{self.key}>")
def help(self, e: Emit) -> None:
e.emit(f"{vt100.WHITE}<{self.name}>{vt100.RESET}")
e.emit(f" {self.description}\n")
if self.default:
e.emit(f" (default: {self.default})")
class Operands(Node, Generic[T]):
name: str
key: str
description: str
default: list[T]
def __init__(self, name: str, key: Optional[str] = None, description: str = "", default: list[T] = []):
self.name = name
self.key = key or name
self.description = description
self.default = default
def eval(self, s: Scan, args: Any) -> bool:
if self.key not in args:
args[self.key] = []
any = False
while s.any() and not s.peek().startswith('-'):
args[self.key] += [s.next()]
any = True
if not any and len(args[self.key]) == 0:
args[self.key] += self.default
return True
def usage(self, e: Emit) -> None:
e.emit(f"<{self.key}...>")
def help(self, e: Emit) -> None:
e.emit(f"{vt100.WHITE}<{self.name}>{vt100.RESET}")
e.emit(f" {self.description}\n")
if self.default:
e.emit(f" (default: {self.default})")
class Sink(Node):
name: str
key: str
description: str
def __init__(self, name: str, key: Optional[str] = None, description: str = ""):
self.name = name
self.key = key or name
self.description = description
def eval(self, s: Scan, args: Any) -> bool:
if self.key not in args:
args[self.key] = []
while s.any():
args[self.key].append(s.next())
return True
def usage(self, e: Emit) -> None:
e.emit(f"<{self.name}>...")
def help(self, e: Emit) -> None:
e.emit(f"{vt100.WHITE}<{self.name}...>{vt100.RESET}")
e.emit(f" {self.description}\n")
e.emit("\n")
# --- Help ------------------------------------------------------------------- #
class Title(Node):
title: str
def __init__(self, title: str):
self.title = title
def eval(self, s: Scan, args: Any) -> bool:
return False
def usage(self, e: Emit) -> None:
pass
def help(self, e: Emit) -> None:
e.emit(f"{vt100.BOLD}{self.title}{vt100.RESET}\n")
class Heading(Node):
title: str
def __init__(self, title: str):
self.title = title
def eval(self, s: Scan, args: Any) -> bool:
return False
def usage(self, e: Emit) -> None:
pass
def help(self, e: Emit) -> None:
e.emit(f"{vt100.BOLD}{self.title}:{vt100.RESET}\n")
class Paragraph(Node):
_text: str
def __init__(self, text: str):
self._text = text
def eval(self, s: Scan, args: Any) -> bool:
return False
def usage(self, e: Emit) -> None:
pass
def help(self, e: Emit) -> None:
e.emit(f"{self._text}\n")
class Section(Node):
_inner: Node
def __init__(self, inner: Node):
self._inner = inner
def eval(self, s: Scan, args: Any) -> bool:
return self._inner.eval(s, args)
def usage(self, e: Emit) -> None:
self._inner.usage(e)
def help(self, e: Emit) -> None:
e.newline()
self._inner.help(e)
e.newline()
def Options(*nodes: Node) -> Node:
return Section(ZeroOrMoreOf(
Heading("Options"),
Paragraph("Options can be specified in any order."),
ZeroOrMoreOf(
*nodes,
),
))
"""
cmds = None
def helpCmd(args):
global cmds
if cmds:
e = Emit()
cmds.help(e)
print(e.finish())
cmds = Section(OneOf(
Title("CuteKit"),
Paragraph(
"A build system and package manager for low-level software development"),
Section(
ZeroOrMoreOf(
Heading("Usage"),
Paragraph("ck <command> [args...]")
)
),
Section(
OneOf(
Heading("Commands"),
Keyword(
"help",
shorthand="h",
description="Show help",
syntax=AllOf(
Operand("command", description="Command to show help for"),
),
run=helpCmd,
),
Keyword(
"build",
shorthand="b",
description="Build the project",
syntax=AllOf(
Options(
Option[str](
"target",
shorthand="t",
description="Build target",
default="default",
),
),
Operands(
"components",
description="Components to build",
),
),
run=lambda args: print("build", args),
),
Keyword(
"run",
shorthand="r",
description="Run the project",
syntax=AllOf(
Options(
Option[str](
"target",
shorthand="t",
description="Build target",
default="default",
),
),
Operand("component", description="Component to run"),
Sink("args", description="Arguments to pass to the component")
),
run=lambda args: print("run", args),
),
)
)
))
try:
cmds.eval(Scan(sys.argv[1:]), {})
except RuntimeError as e:
print(f"{vt100.RED}Error:{vt100.RESET} {e}")
print("Try: --help for more information")
raise SystemExit(1)
"""

View file

@ -1,13 +1,13 @@
import os import os
import logging
from typing import TextIO from typing import TextIO
from osdk.model import ComponentManifest, TargetManifest, Props from cutekit.model import Props
from osdk.ninja import Writer from cutekit.ninja import Writer
from osdk.logger import Logger from cutekit.context import ComponentInstance, Context, contextFor
from osdk.context import Context, contextFor from cutekit import shell, rules
from osdk import shell, rules
logger = Logger("builder") logger = logging.getLogger(__name__)
def gen(out: TextIO, context: Context): def gen(out: TextIO, context: Context):
@ -28,6 +28,9 @@ def gen(out: TextIO, context: Context):
writer.newline() writer.newline()
writer.rule("cp", "cp $in $out")
writer.newline()
for i in target.tools: for i in target.tools:
tool = target.tools[i] tool = target.tools[i]
rule = rules.rules[i] rule = rules.rules[i]
@ -43,22 +46,26 @@ def gen(out: TextIO, context: Context):
all: list[str] = [] all: list[str] = []
for instance in context.enabledInstances(): for instance in context.enabledInstances():
objects = instance.objsfiles(context) objects = instance.objsfiles()
assets = instance.resfiles()
writer.comment(f"Component: {instance.manifest.id}") writer.comment(f"Component: {instance.manifest.id}")
writer.comment(f"Resolved: {', '.join(instance.resolved)}") writer.comment(f"Resolved: {', '.join(instance.resolved)}")
for obj in objects: for obj in objects:
r = rules.byFileIn(obj[0]) r = rules.byFileIn(obj[0])
if r is None: if r is None:
raise Exception(f"Unknown rule for file {obj[0]}") raise RuntimeError(f"Unknown rule for file {obj[0]}")
t = target.tools[r.id] t = target.tools[r.id]
writer.build(obj[1], r.id, obj[0], order_only=t.files) writer.build(obj[1], r.id, obj[0], order_only=t.files)
for asset in assets:
writer.build(asset[1], "cp", asset[0])
writer.newline() writer.newline()
if instance.isLib(): if instance.isLib():
writer.build(instance.libfile(context), "ar", writer.build(instance.outfile(), "ar",
list(map(lambda o: o[1], objects))) list(map(lambda o: o[1], objects)), implicit=list(map(lambda o: o[1], assets)))
else: else:
libraries: list[str] = [] libraries: list[str] = []
@ -66,17 +73,17 @@ def gen(out: TextIO, context: Context):
reqInstance = context.componentByName(req) reqInstance = context.componentByName(req)
if reqInstance is None: if reqInstance is None:
raise Exception(f"Component {req} not found") raise RuntimeError(f"Component {req} not found")
if not reqInstance.isLib(): if not reqInstance.isLib():
raise Exception(f"Component {req} is not a library") raise RuntimeError(f"Component {req} is not a library")
libraries.append(reqInstance.outfile(context)) libraries.append(reqInstance.outfile())
writer.build(instance.binfile(context), "ld", writer.build(instance.outfile(), "ld", list(
list(map(lambda o: o[1], objects)) + libraries) map(lambda o: o[1], objects)) + libraries, implicit=list(map(lambda o: o[1], assets)))
all.append(instance.binfile(context)) all.append(instance.outfile())
writer.newline() writer.newline()
@ -86,7 +93,7 @@ def gen(out: TextIO, context: Context):
writer.default("all") writer.default("all")
def build(componentSpec: str, targetSpec: str, props: Props = {}) -> str: def buildMany(componentSpec: list[str], targetSpec: str, props: Props = {}) -> list[ComponentInstance]:
context = contextFor(targetSpec, props) context = contextFor(targetSpec, props)
shell.mkdir(context.builddir()) shell.mkdir(context.builddir())
@ -95,18 +102,21 @@ def build(componentSpec: str, targetSpec: str, props: Props = {}) -> str:
with open(ninjaPath, "w") as f: with open(ninjaPath, "w") as f:
gen(f, context) gen(f, context)
instance = context.componentByName(componentSpec) instances = map(lambda i: context.componentByName(i), componentSpec)
if instance is None: for instance in instances:
raise Exception(f"Component {componentSpec} not found") if not instance.enabled:
raise RuntimeError(
f"Component {componentSpec} is disabled: {instance.disableReason}")
if not instance.enabled: shell.exec(f"ninja", "-v", "-f", ninjaPath,
raise Exception( *map(lambda i: i.outfile(), instances))
f"Component {componentSpec} is disabled: {instance.disableReason}")
shell.exec(f"ninja", "-v", "-f", ninjaPath, instance.outfile(context)) return [instance]
return instance.outfile(context)
def build(componentSpec: str, targetSpec: str, props: Props = {}) -> ComponentInstance:
return buildMany([componentSpec], targetSpec, props)[0]
class Paths: class Paths:
@ -120,9 +130,8 @@ class Paths:
self.obj = obj self.obj = obj
def buildAll(targetSpec: str) -> Paths: def buildAll(targetSpec: str) -> Context:
context = contextFor(targetSpec) context = contextFor(targetSpec)
target = context.target
shell.mkdir(context.builddir()) shell.mkdir(context.builddir())
ninjaPath = os.path.join(context.builddir(), "build.ninja") ninjaPath = os.path.join(context.builddir(), "build.ninja")
@ -132,8 +141,24 @@ def buildAll(targetSpec: str) -> Paths:
shell.exec(f"ninja", "-v", "-f", ninjaPath) shell.exec(f"ninja", "-v", "-f", ninjaPath)
return Paths( return context
os.path.join(context.builddir(), "bin"),
os.path.join(context.builddir(), "lib"),
os.path.join(context.builddir(), "obj") def testAll(targetSpec: str):
) context = contextFor(targetSpec)
shell.mkdir(context.builddir())
ninjaPath = os.path.join(context.builddir(), "build.ninja")
with open(ninjaPath, "w") as f:
gen(f, context)
shell.exec(f"ninja", "-v", "-f", ninjaPath, "all")
for instance in context.enabledInstances():
if instance.isLib():
continue
if instance.id().endswith("-tests"):
print(f"Running {instance.id()}")
shell.exec(instance.outfile())

442
cutekit/cmds.py Normal file
View file

@ -0,0 +1,442 @@
import os
import logging
import requests
import sys
import git
from typing import cast, Optional, Any
from cutekit import context, shell, const, vt100, builder, graph, project, args2
from cutekit.model import Extern
from cutekit.context import contextFor
# === Commons ================================================================ #
TARGET_OPTION = args2.Option[str](
"target",
shorthand="t",
description="Build target",
default="host-" + shell.uname().machine
)
logger = logging.getLogger(__name__)
root: Optional[args2.Node] = None
cmds: list[args2.Node] = []
def append(cmd: args2.Node):
global cmds
cmds.append(cmd)
def usage():
print(f"Usage: {const.ARGV0} <command> [args...]")
def error(msg: str) -> None:
print(f"{vt100.RED}Error:{vt100.RESET} {msg}\n", file=sys.stderr)
def exec(args: list[str]):
global root, cmds
if root is None:
root = args2.Section(args2.OneOf(
args2.Title("CuteKit"),
args2.Paragraph(
"A build system and package manager for low-level software development"),
args2.Section(
args2.ZeroOrMoreOf(
args2.Heading("Usage"),
args2.Paragraph("ck <command> [args...]")
)
),
args2.Section(
args2.OneOf(
args2.Heading("Commands"),
*cmds
)
)
))
try:
root.eval(args2.Scan(args[1:]), {})
except RuntimeError as e:
print(f"{vt100.RED}Error:{vt100.RESET} {e}")
print("Try: --help for more information")
raise SystemExit(1)
# === Commands =============================================================== #
# --- Help Command ----------------------------------------------------------- #
def helpCmd(args: Any):
global root
if root:
e = args.Emit()
root.help(e)
print(e.finish())
append(
args2.Keyword(
"help",
shorthand="h",
description="Show help",
run=helpCmd,
)
)
# --- Run Command ------------------------------------------------------------ #
def runCmd(args: Any):
project.chdir()
component = builder.build(args['component'], args['target'])
os.environ["CK_TARGET"] = component.context.target.id
os.environ["CK_COMPONENT"] = component.id()
os.environ["CK_BUILDDIR"] = component.context.builddir()
shell.exec(component.outfile(), *args["args"])
append(
args2.Keyword(
"run",
shorthand="r",
description="Run the target",
run=runCmd,
syntax=args2.AllOf(
args2.Options(
TARGET_OPTION
),
args2.Operand("component", description="Component to run"),
args2.Sink("args", description="Arguments to pass to the component")
),
),
)
# --- Test Command ----------------------------------------------------------- #
def testCmd(args: Any):
project.chdir()
builder.testAll(args['target'])
append(
args2.Keyword(
"test",
shorthand="t",
description="Run all test",
run=testCmd,
syntax=args2.AllOf(
args2.Options(
TARGET_OPTION
)
),
),
)
# --- Debug Command ---------------------------------------------------------- #
def debugCmd(args: Any):
project.chdir()
component = builder.build(args['component'], args['target'])
os.environ["CK_TARGET"] = component.context.target.id
os.environ["CK_COMPONENT"] = component.id()
os.environ["CK_BUILDDIR"] = component.context.builddir()
shell.exec("lldb", "-o", "run", component.outfile(), *args["args"])
append(
args2.Keyword(
"debug",
shorthand="d",
description="Run a component in the debugger",
run=debugCmd,
syntax=args2.AllOf(
args2.Options(
TARGET_OPTION
),
args2.Operand("component", description="Component to debug"),
args2.Sink("args", description="Arguments to pass to the component")
),
),
)
# --- Build Command ---------------------------------------------------------- #
def buildCmd(args: Any):
project.chdir()
if len(args['components']) == 0:
builder.buildAll(args['target'])
else:
builder.buildMany(args['components'], args['target'])
append(
args2.Keyword(
"build",
shorthand="b",
description="Build specified component or all components",
run=buildCmd,
syntax=args2.AllOf(
args2.Options(
TARGET_OPTION
),
args2.Operands(
"components", description="Components to build", default=[])
),
),
)
# --- List Command ----------------------------------------------------------- #
def listWithTitle(title: str, elements: list[str]):
vt100.title(title + " (" + str(len(elements)) + ")")
if len(elements) == 0:
print(f" (No {title.lower()} available)")
else:
print(vt100.indent(vt100.wordwrap(
", ".join(elements))))
print()
def listCmd(args: Any):
project.chdir()
components = context.loadAllComponents()
targets = context.loadAllTargets()
listWithTitle("Components", list(map(lambda m: m.id, components)))
listWithTitle("Targets", list(map(lambda m: m.id, targets)))
append(
args2.Keyword(
"list",
shorthand="l",
description="List all targets and components",
run=listCmd,
),
)
# --- Clean Command ---------------------------------------------------------- #
def cleanCmd(args: Any):
project.chdir()
shell.rmrf(const.BUILD_DIR)
append(
args2.Keyword(
"clean",
shorthand="c",
description="Clean the build directory",
run=cleanCmd,
),
)
# --- Nuke Command ---------------------------------------------------------- #
def nukeCmd(args: Any):
project.chdir()
shell.rmrf(const.PROJECT_CK_DIR)
append(
args2.Keyword(
"nuke",
shorthand="n",
description="Clean the build and cache directories",
run=nukeCmd,
),
)
# --- Version Command ---------------------------------------------------------- #
def versionCmd(args: Any):
print(f"CuteKit v{const.VERSION_STR}\n")
append(
args2.Keyword(
"version",
shorthand="v",
description="Show current version",
run=versionCmd,
),
)
# --- Version Command ---------------------------------------------------------- #
def graphCmd(args: Any):
project.chdir()
context = contextFor(args["target"])
graph.view(
context,
scope=args["scope"],
showExe=not args["only-libs"],
showDisabled=args["show-disabled"])
append(
args2.Keyword(
"graph",
shorthand="g",
description="Show dependency graph",
run=graphCmd,
syntax=args2.AllOf(
args2.Options(
TARGET_OPTION,
args2.Option[str](
"scope",
shorthand="s",
description="Scope to show (default: all)",
default=None,
),
args2.Option[bool](
"only-libs",
shorthand="l",
description="Only show libraries",
default=False,
),
args2.Option[bool](
"show-disabled",
shorthand="d",
description="Show disabled components",
default=False,
)
)
),
),
)
# --- Install Command ---------------------------------------------------------- #
def grabExtern(extern: dict[str, Extern]):
for extSpec, ext in extern.items():
extPath = os.path.join(const.EXTERN_DIR, extSpec)
if os.path.exists(extPath):
print(f"Skipping {extSpec}, already installed")
continue
print(f"Installing {extSpec}-{ext.tag} from {ext.git}...")
git.Repo.clone_from(ext.git, extPath, branch=ext.tag, depth=1)
if os.path.exists(os.path.join(extPath, "project.json")):
grabExtern(context.loadProject(extPath).extern)
def installCmd(args: Any):
project.chdir()
p = context.loadProject(".")
grabExtern(p.extern)
append(
args2.Keyword(
"install",
shorthand="i",
description="Install external dependencies",
run=installCmd,
),
)
# --- Init Command ---------------------------------------------------------- #
def initCmd(args: Any):
repo = args['repo']
list = args['list']
template = args['template']
name = args['name']
logger.info("Fetching registry...")
r = requests.get(
f'https://raw.githubusercontent.com/{repo}/main/registry.json')
if r.status_code != 200:
logger.error('Failed to fetch registry')
exit(1)
registry = r.json()
if list:
logger.info("Fetching registry...")
r = requests.get(
f'https://raw.githubusercontent.com/{repo}/main/registry.json')
if r.status_code != 200:
raise RuntimeError('Failed to fetch registry')
print('\n'.join(
f"* {entry['id']} - {entry['description']}" for entry in registry))
return
if not template:
raise RuntimeError('Template not specified')
if not name:
raise RuntimeError('Name not specified')
if os.path.exists(name):
raise RuntimeError(f"Directory {name} already exists")
print(f"Creating project {name} from template {template}...")
shell.cloneDir(f"https://github.com/{repo}", template, name)
print(f"Project {name} created\n")
print("We suggest that you begin by typing:")
print(f" {vt100.GREEN}cd {name}{vt100.RESET}")
print(f" {vt100.GREEN}cutekit install{vt100.BRIGHT_BLACK} # Install external packages{vt100.RESET}")
print(
f" {vt100.GREEN}cutekit build{vt100.BRIGHT_BLACK} # Build the project{vt100.RESET}")
append(
args2.Keyword(
"init",
shorthand="I",
description="Initialize a new project",
run=installCmd,
syntax=args2.AllOf(
args2.Options(
args2.Option[str](
"repo",
shorthand="r",
description="Repository to fetch templates from",
default=const.DEFAULT_REPO_TEMPLATES,
),
args2.Option[bool](
"list",
shorthand="l",
description="List available templates",
default=False,
),
),
args2.Operand[str](
"template",
description="Template to use",
),
args2.Operand[str](
"name",
description="Name of the project",
)
),
)
)

33
cutekit/compat.py Normal file
View file

@ -0,0 +1,33 @@
from typing import Any
SUPPORTED_MANIFEST = [
"https://schemas.cute.engineering/stable/cutekit.manifest.component.v1",
"https://schemas.cute.engineering/stable/cutekit.manifest.project.v1",
"https://schemas.cute.engineering/stable/cutekit.manifest.target.v1",
]
OSDK_MANIFEST_NOT_SUPPORTED = "OSDK manifests are not supported by CuteKit. Please use CuteKit manifest instead"
UNSUPORTED_MANIFEST = {
"https://schemas.cute.engineering/stable/osdk.manifest.component.v1": OSDK_MANIFEST_NOT_SUPPORTED,
"https://schemas.cute.engineering/stable/osdk.manifest.project.v1": OSDK_MANIFEST_NOT_SUPPORTED,
"https://schemas.cute.engineering/stable/osdk.manifest.target.v1": OSDK_MANIFEST_NOT_SUPPORTED,
"https://schemas.cute.engineering/latest/osdk.manifest.component": OSDK_MANIFEST_NOT_SUPPORTED,
"https://schemas.cute.engineering/latest/osdk.manifest.project": OSDK_MANIFEST_NOT_SUPPORTED,
"https://schemas.cute.engineering/latest/osdk.manifest.target": OSDK_MANIFEST_NOT_SUPPORTED,
}
def ensureSupportedManifest(manifest: Any, path: str):
if not "$schema" in manifest:
raise RuntimeError(f"Missing $schema in {path}")
if manifest["$schema"] in UNSUPORTED_MANIFEST:
raise RuntimeError(
f"Unsupported manifest schema {manifest['$schema']} in {path}: {UNSUPORTED_MANIFEST[manifest['$schema']]}")
if not manifest["$schema"] in SUPPORTED_MANIFEST:
raise RuntimeError(
f"Unsupported manifest schema {manifest['$schema']} in {path}")

19
cutekit/const.py Normal file
View file

@ -0,0 +1,19 @@
import os
import sys
VERSION = (0, 5, 0, "dev")
VERSION_STR = f"{VERSION[0]}.{VERSION[1]}.{VERSION[2]}{'-' + VERSION[3] if VERSION[3] else ''}"
MODULE_DIR = os.path.dirname(os.path.realpath(__file__))
ARGV0 = os.path.basename(sys.argv[0])
PROJECT_CK_DIR = ".cutekit"
GLOBAL_CK_DIR = os.path.join(os.path.expanduser("~"), ".cutekit")
BUILD_DIR = os.path.join(PROJECT_CK_DIR, "build")
CACHE_DIR = os.path.join(PROJECT_CK_DIR, "cache")
EXTERN_DIR = os.path.join(PROJECT_CK_DIR, "extern")
SRC_DIR = "src"
META_DIR = f"meta"
TARGETS_DIR = os.path.join(META_DIR, "targets")
DEFAULT_REPO_TEMPLATES = "cute-engineering/cutekit-templates"
DESCRIPTION = "A build system and package manager for low-level software development"
PROJECT_LOG_FILE = os.path.join(PROJECT_CK_DIR, "cutekit.log")
GLOBAL_LOG_FILE = os.path.join(os.path.expanduser("~"), ".cutekit", "cutekit.log")

View file

@ -1,17 +1,18 @@
from typing import cast, Protocol, Iterable from typing import cast, Optional, Protocol, Iterable
from itertools import chain from itertools import chain
from pathlib import Path from pathlib import Path
import os import os
import logging
from cutekit.model import ProjectManifest, TargetManifest, ComponentManifest, Props, Type, Tool, Tools
from cutekit import const, shell, jexpr, utils, rules, mixins, project
from osdk.model import ProjectManifest, TargetManifest, ComponentManifest, Props, Type, Tool, Tools logger = logging.getLogger(__name__)
from osdk.logger import Logger
from osdk import const, shell, jexpr, utils, rules, mixins
logger = Logger("context")
class IContext(Protocol): class IContext(Protocol):
target: TargetManifest
def builddir(self) -> str: def builddir(self) -> str:
... ...
@ -21,7 +22,9 @@ class ComponentInstance:
disableReason = "" disableReason = ""
manifest: ComponentManifest manifest: ComponentManifest
sources: list[str] = [] sources: list[str] = []
res: list[str] = []
resolved: list[str] = [] resolved: list[str] = []
context: IContext
def __init__( def __init__(
self, self,
@ -29,43 +32,54 @@ class ComponentInstance:
disableReason: str, disableReason: str,
manifest: ComponentManifest, manifest: ComponentManifest,
sources: list[str], sources: list[str],
res: list[str],
resolved: list[str]): resolved: list[str]):
self.enabled = enabled self.enabled = enabled
self.disableReason = disableReason self.disableReason = disableReason
self.manifest = manifest self.manifest = manifest
self.sources = sources self.sources = sources
self.res = res
self.resolved = resolved self.resolved = resolved
def id(self) -> str:
return self.manifest.id
def isLib(self): def isLib(self):
return self.manifest.type == Type.LIB return self.manifest.type == Type.LIB
def binfile(self, context: IContext) -> str: def objdir(self) -> str:
return os.path.join(context.builddir(), "bin", f"{self.manifest.id}.out") return os.path.join(self.context.builddir(), f"{self.manifest.id}/obj")
def objdir(self, context: IContext) -> str: def resdir(self) -> str:
return os.path.join(context.builddir(), "obj", self.manifest.id) return os.path.join(self.context.builddir(), f"{self.manifest.id}/res")
def objsfiles(self, context: IContext) -> list[tuple[str, str]]: def objsfiles(self) -> list[tuple[str, str]]:
return list( def toOFile(s: str) -> str:
map( return os.path.join(self.objdir(), s.replace(os.path.join(self.manifest.dirname(), ''), '') + ".o")
lambda s: ( return list(map(lambda s: (s, toOFile(s)), self.sources))
s, os.path.join(self.objdir(context), s.replace(os.path.join(self.manifest.dirname(), ''), '') + ".o")),
self.sources))
def libfile(self, context: IContext) -> str: def resfiles(self) -> list[tuple[str, str, str]]:
return os.path.join(context.builddir(), "lib", f"{self.manifest.id}.a") def toAssetFile(s: str) -> str:
return os.path.join(self.resdir(), s.replace(os.path.join(self.manifest.dirname(), 'res/'), ''))
def outfile(self, context: IContext) -> str: def toAssetId(s: str) -> str:
return s.replace(os.path.join(self.manifest.dirname(), 'res/'), '')
return list(map(lambda s: (s, toAssetFile(s), toAssetId(s)), self.res))
def outfile(self) -> str:
if self.isLib(): if self.isLib():
return self.libfile(context) return os.path.join(self.context.builddir(), self.manifest.id, f"lib/{self.manifest.id}.a")
else: else:
return self.binfile(context) return os.path.join(self.context.builddir(), self.manifest.id, f"bin/{self.manifest.id}.out")
def cinclude(self) -> str: def cinclude(self) -> str:
if "cpp-root-include" in self.manifest.props: if "cpp-root-include" in self.manifest.props:
return self.manifest.dirname() return self.manifest.dirname()
else: elif self.manifest.type == Type.LIB:
return str(Path(self.manifest.dirname()).parent) return str(Path(self.manifest.dirname()).parent)
else:
return ""
class Context(IContext): class Context(IContext):
@ -81,31 +95,44 @@ class Context(IContext):
self.instances = instances self.instances = instances
self.tools = tools self.tools = tools
def componentByName(self, name: str) -> ComponentInstance | None: def componentByName(self, name: str) -> ComponentInstance:
result = list(filter(lambda x: x.manifest.id == name, self.instances)) result = list(filter(lambda x: x.manifest.id == name, self.instances))
if len(result) == 0: if len(result) == 0:
return None raise RuntimeError(f"Component '{name}' not found")
return result[0] return result[0]
def cincls(self) -> list[str]: def cincls(self) -> list[str]:
includes = list( includes = list(filter(lambda x: x != "", map(
map(lambda x: x.cinclude(), self.enabledInstances())) lambda x: x.cinclude(), self.enabledInstances())))
return utils.uniq(includes) return utils.uniq(includes)
def cdefs(self) -> list[str]: def cdefs(self) -> list[str]:
return self.target.cdefs() return self.target.cdefs()
def hashid(self) -> str: def hashid(self) -> str:
return utils.hash((self.target.props, str(self.tools)))[0:8] return utils.hash((self.target.props, [self.tools[t].toJson() for t in self.tools]))[0:8]
def builddir(self) -> str: def builddir(self) -> str:
return os.path.join(const.BUILD_DIR, f"{self.target.id}-{self.hashid()[:8]}") return os.path.join(const.BUILD_DIR, f"{self.target.id}-{self.hashid()[:8]}")
def loadAllTargets() -> list[TargetManifest]: def loadAllTargets() -> list[TargetManifest]:
files = shell.find(const.TARGETS_DIR, ["*.json"]) projectRoot = project.root()
return list( if projectRoot is None:
map(lambda path: TargetManifest(jexpr.evalRead(path), path), files)) return []
pj = loadProject(projectRoot)
paths = list(
map(lambda e: os.path.join(const.EXTERN_DIR,
e, const.TARGETS_DIR), pj.extern.keys())
) + [const.TARGETS_DIR]
ret = []
for entry in paths:
files = shell.find(entry, ["*.json"])
ret += list(map(lambda path: TargetManifest(jexpr.evalRead(path), path), files))
return ret
def loadProject(path: str) -> ProjectManifest: def loadProject(path: str) -> ProjectManifest:
@ -117,7 +144,7 @@ def loadTarget(id: str) -> TargetManifest:
try: try:
return next(filter(lambda t: t.id == id, loadAllTargets())) return next(filter(lambda t: t.id == id, loadAllTargets()))
except StopIteration: except StopIteration:
raise Exception(f"Target '{id}' not found") raise RuntimeError(f"Target '{id}' not found")
def loadAllComponents() -> list[ComponentManifest]: def loadAllComponents() -> list[ComponentManifest]:
@ -135,7 +162,7 @@ def filterDisabled(components: list[ComponentManifest], target: TargetManifest)
list(filter(lambda c: not c.isEnabled(target)[0], components)) list(filter(lambda c: not c.isEnabled(target)[0], components))
def providerFor(what: str, components: list[ComponentManifest]) -> tuple[str | None, str]: def providerFor(what: str, components: list[ComponentManifest]) -> tuple[Optional[str], str]:
result: list[ComponentManifest] = list( result: list[ComponentManifest] = list(
filter(lambda c: c.id == what, components)) filter(lambda c: c.id == what, components))
@ -167,7 +194,7 @@ def resolveDeps(componentSpec: str, components: list[ComponentManifest], target:
return False, unresolvedReason, [] return False, unresolvedReason, []
if resolved in stack: if resolved in stack:
raise Exception(f"Dependency loop: {stack} -> {resolved}") raise RuntimeError(f"Dependency loop: {stack} -> {resolved}")
stack.append(resolved) stack.append(resolved)
@ -191,30 +218,39 @@ def resolveDeps(componentSpec: str, components: list[ComponentManifest], target:
return enabled, unresolvedReason, resolved return enabled, unresolvedReason, resolved
def instanciate(componentSpec: str, components: list[ComponentManifest], target: TargetManifest) -> ComponentInstance | None: def instanciate(componentSpec: str, components: list[ComponentManifest], target: TargetManifest) -> Optional[ComponentInstance]:
manifest = next(filter(lambda c: c.id == componentSpec, components)) manifest = next(filter(lambda c: c.id == componentSpec, components))
wildcards = set( wildcards = set(
chain(*map(lambda rule: rule.fileIn, rules.rules.values()))) chain(*map(lambda rule: rule.fileIn, rules.rules.values())))
sources = shell.find( sources = shell.find(
manifest.subdirs, list(wildcards), recusive=False) manifest.subdirs, list(wildcards), recusive=False)
res = shell.find(os.path.join(manifest.dirname(), "res"))
enabled, unresolvedReason, resolved = resolveDeps( enabled, unresolvedReason, resolved = resolveDeps(
componentSpec, components, target) componentSpec, components, target)
return ComponentInstance(enabled, unresolvedReason, manifest, sources, resolved[1:]) return ComponentInstance(enabled, unresolvedReason, manifest, sources, res, resolved[1:])
def instanciateDisabled(component: ComponentManifest, target: TargetManifest) -> ComponentInstance: def instanciateDisabled(component: ComponentManifest, target: TargetManifest) -> ComponentInstance:
return ComponentInstance(False, component.isEnabled(target)[1], component, [], []) return ComponentInstance(
enabled=False,
disableReason=component.isEnabled(target)[1],
manifest=component,
sources=[],
res=[],
resolved=[])
context: dict = {} context: dict[str, Context] = {}
def contextFor(targetSpec: str, props: Props = {}) -> Context: def contextFor(targetSpec: str, props: Props = {}) -> Context:
if targetSpec in context: if targetSpec in context:
return context[targetSpec] return context[targetSpec]
logger.log(f"Loading context for '{targetSpec}'") logger.info(f"Loading context for '{targetSpec}'")
targetEls = targetSpec.split(":") targetEls = targetSpec.split(":")
@ -261,4 +297,7 @@ def contextFor(targetSpec: str, props: Props = {}) -> Context:
tools, tools,
) )
for instance in instances:
instance.context = context[targetSpec]
return context[targetSpec] return context[targetSpec]

View file

@ -1,10 +1,11 @@
import os import os
from osdk.context import Context from typing import Optional
from osdk import vt100 from cutekit.context import Context
from cutekit import vt100
def view(context: Context, scope: str | None = None, showExe: bool = True, showDisabled: bool = False): def view(context: Context, scope: Optional[str] = None, showExe: bool = True, showDisabled: bool = False):
from graphviz import Digraph from graphviz import Digraph
g = Digraph(context.target.id, filename='graph.gv') g = Digraph(context.target.id, filename='graph.gv')

56
cutekit/jexpr.py Normal file
View file

@ -0,0 +1,56 @@
import os
from typing import Any, cast, Callable, Final
import json
import cutekit.shell as shell
from cutekit.compat import ensureSupportedManifest
Json = Any
Builtin = Callable[..., Json]
BUILTINS: Final[dict[str, Builtin]] = {
"uname": lambda arg, ctx: getattr(shell.uname(), arg).lower(),
"include": lambda arg, ctx: evalRead(arg),
"evalRead": lambda arg, ctx: evalRead(arg),
"join": lambda lhs, rhs, ctx: cast(Json, {**lhs, **rhs} if isinstance(lhs, dict) else lhs + rhs),
"concat": lambda *args, ctx: "".join(args),
"eval": lambda arg, ctx: eval(arg, ctx["filepath"]),
"read": lambda arg, ctx: read(arg),
"exec": lambda *args, ctx: shell.popen(*args).splitlines(),
"latest": lambda arg, ctx: shell.latest(arg),
"abspath": lambda *args, ctx: os.path.normpath(os.path.join(os.path.dirname(ctx["filepath"]), *args))
}
def eval(jexpr: Json, filePath: str) -> Json:
if isinstance(jexpr, dict):
result = {}
for k in cast(dict[str, Json], jexpr):
result[k] = eval(jexpr[k], filePath)
return cast(Json, result)
elif isinstance(jexpr, list):
jexpr = cast(list[Json], jexpr)
if len(jexpr) > 0 and isinstance(jexpr[0], str) and jexpr[0].startswith("@"):
funcName = jexpr[0][1:]
if funcName in BUILTINS:
return BUILTINS[funcName](*eval(jexpr[1:], filePath), ctx={"filepath": filePath})
raise RuntimeError(f"Unknown macro {funcName}")
else:
return list(map(lambda j: eval(j, filePath), jexpr))
else:
return jexpr
def read(path: str) -> Json:
try:
with open(path, "r") as f:
return json.load(f)
except:
raise RuntimeError(f"Failed to read {path}")
def evalRead(path: str) -> Json:
data = read(path)
ensureSupportedManifest(data, path)
return eval(data, path)

View file

@ -1,5 +1,5 @@
from typing import Callable from typing import Callable
from osdk.model import TargetManifest, Tools from cutekit.model import TargetManifest, Tools
Mixin = Callable[[TargetManifest, Tools], Tools] Mixin = Callable[[TargetManifest, Tools], Tools]
@ -43,12 +43,22 @@ def makeMixinOptimize(level: str) -> Mixin:
def mixinDebug(target: TargetManifest, tools: Tools) -> Tools: def mixinDebug(target: TargetManifest, tools: Tools) -> Tools:
patchToolArgs(tools, "cc", ["-g"]) patchToolArgs(tools, "cc", ["-g", "-gdwarf-4"])
patchToolArgs(tools, "cxx", ["-g"]) patchToolArgs(tools, "cxx", ["-g", "-gdwarf-4"])
return tools return tools
def makeMixinTune(tune: str) -> Mixin:
def mixinTune(target: TargetManifest, tools: Tools) -> Tools:
patchToolArgs(tools, "cc", [f"-mtune={tune}"])
patchToolArgs(tools, "cxx", [f"-mtune={tune}"])
return tools
return mixinTune
mixins: dict[str, Mixin] = { mixins: dict[str, Mixin] = {
"cache": mixinCache, "cache": mixinCache,
"debug": mixinDebug, "debug": mixinDebug,
@ -56,6 +66,7 @@ mixins: dict[str, Mixin] = {
"msan": makeMixinSan("memory"), "msan": makeMixinSan("memory"),
"tsan": makeMixinSan("thread"), "tsan": makeMixinSan("thread"),
"ubsan": makeMixinSan("undefined"), "ubsan": makeMixinSan("undefined"),
"tune": makeMixinTune("native"),
"o3": makeMixinOptimize("3"), "o3": makeMixinOptimize("3"),
"o2": makeMixinOptimize("2"), "o2": makeMixinOptimize("2"),
"o1": makeMixinOptimize("1"), "o1": makeMixinOptimize("1"),
@ -66,5 +77,6 @@ mixins: dict[str, Mixin] = {
def append(mixinSpec: str, mixin: Mixin): def append(mixinSpec: str, mixin: Mixin):
mixins[mixinSpec] = mixin mixins[mixinSpec] = mixin
def byId(id: str) -> Mixin: def byId(id: str) -> Mixin:
return mixins[id] return mixins[id]

View file

@ -1,14 +1,12 @@
import os import os
from enum import Enum from enum import Enum
from typing import Any from typing import Any
from json import JSONEncoder import logging
from osdk.jexpr import Json from cutekit.jexpr import Json
from osdk.logger import Logger
from osdk import const, utils
logger = Logger("model") logger = logging.getLogger(__name__)
Props = dict[str, Any] Props = dict[str, Any]
@ -26,25 +24,32 @@ class Manifest:
type: Type = Type.UNKNOWN type: Type = Type.UNKNOWN
path: str = "" path: str = ""
def __init__(self, json: Json = None, path: str = "", strict=True, **kwargs): def __init__(self, json: Json = None, path: str = "", strict: bool = True, **kwargs: Any):
if json is not None: if json is not None:
if not "id" in json: if not "id" in json:
raise ValueError("Missing id") raise RuntimeError("Missing id")
self.id = json["id"] self.id = json["id"]
if not "type" in json and strict: if not "type" in json and strict:
raise ValueError("Missing type") raise RuntimeError("Missing type")
self.type = Type(json["type"]) self.type = Type(json["type"])
self.path = path self.path = path
elif strict: elif strict:
raise ValueError("Missing json") raise RuntimeError("Missing json")
for key in kwargs: for key in kwargs:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])
def toJson(self) -> Json:
return {
"id": self.id,
"type": self.type.value,
"path": self.path
}
def __str__(self): def __str__(self):
return f"Manifest(id={self.id}, type={self.type}, path={self.path})" return f"Manifest(id={self.id}, type={self.type}, path={self.path})"
@ -59,23 +64,29 @@ class Extern:
git: str = "" git: str = ""
tag: str = "" tag: str = ""
def __init__(self, json: Json = None, strict=True, **kwargs): def __init__(self, json: Json = None, strict: bool = True, **kwargs: Any):
if json is not None: if json is not None:
if not "git" in json and strict: if not "git" in json and strict:
raise ValueError("Missing git") raise RuntimeError("Missing git")
self.git = json["git"] self.git = json["git"]
if not "tag" in json and strict: if not "tag" in json and strict:
raise ValueError("Missing tag") raise RuntimeError("Missing tag")
self.tag = json["tag"] self.tag = json["tag"]
elif strict: elif strict:
raise ValueError("Missing json") raise RuntimeError("Missing json")
for key in kwargs: for key in kwargs:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])
def toJson(self) -> Json:
return {
"git": self.git,
"tag": self.tag
}
def __str__(self): def __str__(self):
return f"Extern(git={self.git}, tag={self.tag})" return f"Extern(git={self.git}, tag={self.tag})"
@ -87,20 +98,27 @@ class ProjectManifest(Manifest):
description: str = "" description: str = ""
extern: dict[str, Extern] = {} extern: dict[str, Extern] = {}
def __init__(self, json: Json = None, path: str = "", strict=True, **kwargs): def __init__(self, json: Json = None, path: str = "", strict: bool = True, **kwargs: Any):
if json is not None: if json is not None:
if not "description" in json and strict: if not "description" in json and strict:
raise ValueError("Missing description") raise RuntimeError("Missing description")
self.description = json["description"] self.description = json["description"]
self.extern = {k: Extern(v) self.extern = {k: Extern(v)
for k, v in json.get("extern", {}).items()} for k, v in json.get("extern", {}).items()}
elif strict: elif strict:
raise ValueError("Missing json") raise RuntimeError("Missing json")
super().__init__(json, path, strict, **kwargs) super().__init__(json, path, strict, **kwargs)
def toJson(self) -> Json:
return {
**super().toJson(),
"description": self.description,
"extern": {k: v.toJson() for k, v in self.extern.items()}
}
def __str__(self): def __str__(self):
return f"ProjectManifest(id={self.id}, type={self.type}, path={self.path}, description={self.description}, extern={self.extern})" return f"ProjectManifest(id={self.id}, type={self.type}, path={self.path}, description={self.description}, extern={self.extern})"
@ -113,25 +131,32 @@ class Tool:
args: list[str] = [] args: list[str] = []
files: list[str] = [] files: list[str] = []
def __init__(self, json: Json = None, strict=True, **kwargs): def __init__(self, json: Json = None, strict: bool = True, **kwargs: Any):
if json is not None: if json is not None:
if not "cmd" in json and strict: if not "cmd" in json and strict:
raise ValueError("Missing cmd") raise RuntimeError("Missing cmd")
self.cmd = json.get("cmd", self.cmd) self.cmd = json.get("cmd", self.cmd)
if not "args" in json and strict: if not "args" in json and strict:
raise ValueError("Missing args") raise RuntimeError("Missing args")
self.args = json.get("args", []) self.args = json.get("args", [])
self.files = json.get("files", []) self.files = json.get("files", [])
elif strict: elif strict:
raise ValueError("Missing json") raise RuntimeError("Missing json")
for key in kwargs: for key in kwargs:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])
def toJson(self) -> Json:
return {
"cmd": self.cmd,
"args": self.args,
"files": self.files
}
def __str__(self): def __str__(self):
return f"Tool(cmd={self.cmd}, args={self.args}, files={self.files})" return f"Tool(cmd={self.cmd}, args={self.args}, files={self.files})"
@ -147,15 +172,15 @@ class TargetManifest(Manifest):
tools: Tools tools: Tools
routing: dict[str, str] routing: dict[str, str]
def __init__(self, json: Json = None, path: str = "", strict=True, **kwargs): def __init__(self, json: Json = None, path: str = "", strict: bool = True, **kwargs: Any):
if json is not None: if json is not None:
if not "props" in json and strict: if not "props" in json and strict:
raise ValueError("Missing props") raise RuntimeError("Missing props")
self.props = json["props"] self.props = json["props"]
if not "tools" in json and strict: if not "tools" in json and strict:
raise ValueError("Missing tools") raise RuntimeError("Missing tools")
self.tools = {k: Tool(v) for k, v in json["tools"].items()} self.tools = {k: Tool(v) for k, v in json["tools"].items()}
@ -163,6 +188,14 @@ class TargetManifest(Manifest):
super().__init__(json, path, strict, **kwargs) super().__init__(json, path, strict, **kwargs)
def toJson(self) -> Json:
return {
**super().toJson(),
"props": self.props,
"tools": {k: v.toJson() for k, v in self.tools.items()},
"routing": self.routing
}
def __repr__(self): def __repr__(self):
return f"TargetManifest({self.id})" return f"TargetManifest({self.id})"
@ -178,9 +211,9 @@ class TargetManifest(Manifest):
macrovalue = str(prop).lower().replace(" ", "_").replace("-", "_") macrovalue = str(prop).lower().replace(" ", "_").replace("-", "_")
if isinstance(prop, bool): if isinstance(prop, bool):
if prop: if prop:
defines += [f"-D__osdk_{macroname}__"] defines += [f"-D__ck_{macroname}__"]
else: else:
defines += [f"-D__osdk_{macroname}_{macrovalue}__"] defines += [f"-D__ck_{macroname}_{macrovalue}__"]
return defines return defines
@ -194,7 +227,7 @@ class ComponentManifest(Manifest):
provides: list[str] = [] provides: list[str] = []
subdirs: list[str] = [] subdirs: list[str] = []
def __init__(self, json: Json = None, path: str = "", strict=True, **kwargs): def __init__(self, json: Json = None, path: str = "", strict: bool = True, **kwargs: Any):
if json is not None: if json is not None:
self.decription = json.get("description", self.decription) self.decription = json.get("description", self.decription)
self.props = json.get("props", self.props) self.props = json.get("props", self.props)
@ -208,19 +241,31 @@ class ComponentManifest(Manifest):
super().__init__(json, path, strict, **kwargs) super().__init__(json, path, strict, **kwargs)
def toJson(self) -> Json:
return {
**super().toJson(),
"description": self.decription,
"props": self.props,
"tools": {k: v.toJson() for k, v in self.tools.items()},
"enableIf": self.enableIf,
"requires": self.requires,
"provides": self.provides,
"subdirs": self.subdirs
}
def __repr__(self): def __repr__(self):
return f"ComponentManifest({self.id})" return f"ComponentManifest({self.id})"
def isEnabled(self, target: TargetManifest) -> tuple[bool, str]: def isEnabled(self, target: TargetManifest) -> tuple[bool, str]:
for k, v in self.enableIf.items(): for k, v in self.enableIf.items():
if not k in target.props: if not k in target.props:
logger.log( logger.info(
f"Component {self.id} disabled by missing {k} in target") f"Component {self.id} disabled by missing {k} in target")
return False, f"Missing props '{k}' in target" return False, f"Missing props '{k}' in target"
if not target.props[k] in v: if not target.props[k] in v:
vStrs = [f"'{str(x)}'" for x in v] vStrs = [f"'{str(x)}'" for x in v]
logger.log( logger.info(
f"Component {self.id} disabled by {k}={target.props[k]} not in {v}") f"Component {self.id} disabled by {k}={target.props[k]} not in {v}")
return False, f"Props missmatch for '{k}': Got '{target.props[k]}' but expected {', '.join(vStrs)}" return False, f"Props missmatch for '{k}': Got '{target.props[k]}' but expected {', '.join(vStrs)}"

View file

@ -25,7 +25,7 @@ use Python.
import textwrap import textwrap
from typing import TextIO, Union from typing import TextIO, Union
from osdk.utils import asList from cutekit.utils import asList
def escapePath(word: str) -> str: def escapePath(word: str) -> str:

44
cutekit/plugins.py Normal file
View file

@ -0,0 +1,44 @@
import os
import logging
from cutekit import shell, project, const, context
import importlib.util as importlib
logger = logging.getLogger(__name__)
def load(path: str):
logger.info(f"Loading plugin {path}")
spec = importlib.spec_from_file_location("plugin", path)
if not spec or not spec.loader:
logger.error(f"Failed to load plugin {path}")
return None
module = importlib.module_from_spec(spec)
spec.loader.exec_module(module)
def loadAll():
logger.info("Loading plugins...")
projectRoot = project.root()
if projectRoot is None:
logger.info("Not in project, skipping plugin loading")
return
pj = context.loadProject(projectRoot)
paths = list(map(lambda e: os.path.join(const.EXTERN_DIR, e), pj.extern.keys())) + ["."]
for dirname in paths:
pluginDir = os.path.join(projectRoot, dirname, const.META_DIR, "plugins")
for files in shell.readdir(pluginDir):
if files.endswith(".py"):
plugin = load(os.path.join(pluginDir, files))
if plugin:
logger.info(f"Loaded plugin {plugin.name}")
plugin.init()

20
cutekit/project.py Normal file
View file

@ -0,0 +1,20 @@
import os
from typing import Optional
def root() -> Optional[str]:
cwd = os.getcwd()
while cwd != "/":
if os.path.isfile(os.path.join(cwd, "project.json")):
return cwd
cwd = os.path.dirname(cwd)
return None
def chdir() -> None:
projectRoot = root()
if projectRoot is None:
raise RuntimeError(
"No project.json found in this directory or any parent directory")
os.chdir(projectRoot)

View file

@ -1,12 +1,15 @@
from typing import Optional
class Rule: class Rule:
id: str id: str
fileIn: list[str] fileIn: list[str]
fileOut: list[str] fileOut: list[str]
rule: str rule: str
args: list[str] args: list[str]
deps: str | None = None deps: Optional[str] = None
def __init__(self, id: str, fileIn: list[str], fileOut: list[str], rule: str, args: list[str] = [], deps: str | None = None): def __init__(self, id: str, fileIn: list[str], fileOut: list[str], rule: str, args: list[str] = [], deps: Optional[str] = None):
self.id = id self.id = id
self.fileIn = fileIn self.fileIn = fileIn
self.fileOut = fileOut self.fileOut = fileOut
@ -14,17 +17,18 @@ class Rule:
self.args = args self.args = args
self.deps = deps self.deps = deps
rules: dict[str, Rule] = { rules: dict[str, Rule] = {
"cc": Rule("cc", ["*.c"], ["*.o"], "-c -o $out $in -MD -MF $out.d $flags $cincs $cdefs", ["-std=gnu2x", "cc": Rule("cc", ["*.c"], ["*.o"], "-c -o $out $in -MD -MF $out.d $flags $cincs $cdefs", ["-std=gnu2x",
"-Wall", "-Wall",
"-Wextra", "-Wextra",
"-Werror"], "$out.d"), "-Werror"], "$out.d"),
"cxx": Rule("cxx", ["*.cpp", "*.cc", "*.cxx"], ["*.o"], "-c -o $out $in -MD -MF $out.d $flags $cincs $cdefs", ["-std=gnu++2b", "cxx": Rule("cxx", ["*.cpp", "*.cc", "*.cxx"], ["*.o"], "-c -o $out $in -MD -MF $out.d $flags $cincs $cdefs", ["-std=gnu++2b",
"-Wall", "-Wall",
"-Wextra", "-Wextra",
"-Werror", "-Werror",
"-fno-exceptions", "-fno-exceptions",
"-fno-rtti"], "$out.d"), "-fno-rtti"], "$out.d"),
"as": Rule("as", ["*.s", "*.asm", "*.S"], ["*.o"], "-o $out $in $flags"), "as": Rule("as", ["*.s", "*.asm", "*.S"], ["*.o"], "-o $out $in $flags"),
"ar": Rule("ar", ["*.o"], ["*.a"], "$flags $out $in"), "ar": Rule("ar", ["*.o"], ["*.a"], "$flags $out $in"),
"ld": Rule("ld", ["*.o", "*.a"], ["*.out"], "-o $out $in $flags"), "ld": Rule("ld", ["*.o", "*.a"], ["*.out"], "-o $out $in $flags"),
@ -35,7 +39,7 @@ def append(rule: Rule):
rules[rule.id] = rule rules[rule.id] = rule
def byFileIn(fileIn: str) -> Rule | None: def byFileIn(fileIn: str) -> Optional[Rule]:
for key in rules: for key in rules:
rule = rules[key] rule = rules[key]
for ext in rule.fileIn: for ext in rule.fileIn:
@ -44,5 +48,5 @@ def byFileIn(fileIn: str) -> Rule | None:
return None return None
def byId(id: str) -> Rule | None: def byId(id: str) -> Optional[Rule]:
return rules.get(id, None) return rules.get(id, None)

View file

@ -8,11 +8,14 @@ import re
import shutil import shutil
import fnmatch import fnmatch
import platform import platform
import logging
import tempfile
from osdk.logger import Logger
from osdk import const
logger = Logger("shell") from typing import Optional
from cutekit import const
logger = logging.getLogger(__name__)
class Uname: class Uname:
@ -33,6 +36,8 @@ def uname() -> Uname:
result.machine = "arm64" result.machine = "arm64"
case "AMD64": case "AMD64":
result.machine = "x86_64" result.machine = "x86_64"
case _:
pass
return result return result
@ -43,7 +48,7 @@ def sha256sum(path: str) -> str:
def find(path: str | list[str], wildcards: list[str] = [], recusive: bool = True) -> list[str]: def find(path: str | list[str], wildcards: list[str] = [], recusive: bool = True) -> list[str]:
logger.log(f"Looking for files in {path} matching {wildcards}") logger.info(f"Looking for files in {path} matching {wildcards}")
result: list[str] = [] result: list[str] = []
@ -79,7 +84,7 @@ def find(path: str | list[str], wildcards: list[str] = [], recusive: bool = True
def mkdir(path: str) -> str: def mkdir(path: str) -> str:
logger.log(f"Creating directory {path}") logger.info(f"Creating directory {path}")
try: try:
os.makedirs(path) os.makedirs(path)
@ -90,7 +95,7 @@ def mkdir(path: str) -> str:
def rmrf(path: str) -> bool: def rmrf(path: str) -> bool:
logger.log(f"Removing directory {path}") logger.info(f"Removing directory {path}")
if not os.path.exists(path): if not os.path.exists(path):
return False return False
@ -98,17 +103,18 @@ def rmrf(path: str) -> bool:
return True return True
def wget(url: str, path: str | None = None) -> str: def wget(url: str, path: Optional[str] = None) -> str:
import requests import requests
if path is None: if path is None:
path = os.path.join(const.CACHE_DIR, path = os.path.join(
const.CACHE_DIR,
hashlib.sha256(url.encode('utf-8')).hexdigest()) hashlib.sha256(url.encode('utf-8')).hexdigest())
if os.path.exists(path): if os.path.exists(path):
return path return path
logger.log(f"Downloading {url} to {path}") logger.info(f"Downloading {url} to {path}")
r = requests.get(url, stream=True) r = requests.get(url, stream=True)
r.raise_for_status() r.raise_for_status()
@ -121,48 +127,55 @@ def wget(url: str, path: str | None = None) -> str:
return path return path
def exec(*args: str): def exec(*args: str, quiet: bool = False) -> bool:
logger.log(f"Executing {args}") logger.info(f"Executing {args}")
try: try:
proc = subprocess.run(args) proc = subprocess.run(
args, stdout=sys.stdout if not quiet else subprocess.PIPE, stderr=sys.stderr if not quiet else subprocess.PIPE)
if proc.stdout:
logger.info(proc.stdout.decode('utf-8'))
if proc.stderr:
logger.error(proc.stderr.decode('utf-8'))
except FileNotFoundError: except FileNotFoundError:
raise Exception(f"{args[0]}: Command not found") raise RuntimeError(f"{args[0]}: Command not found")
except KeyboardInterrupt: except KeyboardInterrupt:
raise Exception(f"{args[0]}: Interrupted") raise RuntimeError(f"{args[0]}: Interrupted")
if proc.returncode == -signal.SIGSEGV: if proc.returncode == -signal.SIGSEGV:
raise Exception(f"{args[0]}: Segmentation fault") raise RuntimeError(f"{args[0]}: Segmentation fault")
if proc.returncode != 0: if proc.returncode != 0:
raise Exception( raise RuntimeError(
f"{args[0]}: Process exited with code {proc.returncode}") f"{args[0]}: Process exited with code {proc.returncode}")
return True return True
def popen(*args: str) -> str: def popen(*args: str) -> str:
logger.log(f"Executing {args}") logger.info(f"Executing {args}")
try: try:
proc = subprocess.run(args, stdout=subprocess.PIPE, stderr=sys.stderr) proc = subprocess.run(args, stdout=subprocess.PIPE, stderr=sys.stderr)
except FileNotFoundError: except FileNotFoundError:
raise Exception(f"{args[0]}: Command not found") raise RuntimeError(f"{args[0]}: Command not found")
if proc.returncode == -signal.SIGSEGV: if proc.returncode == -signal.SIGSEGV:
raise Exception(f"{args[0]}: Segmentation fault") raise RuntimeError(f"{args[0]}: Segmentation fault")
if proc.returncode != 0: if proc.returncode != 0:
raise Exception( raise RuntimeError(
f"{args[0]}: Process exited with code {proc.returncode}") f"{args[0]}: Process exited with code {proc.returncode}")
return proc.stdout.decode('utf-8') return proc.stdout.decode('utf-8')
def readdir(path: str) -> list[str]: def readdir(path: str) -> list[str]:
logger.log(f"Reading directory {path}") logger.info(f"Reading directory {path}")
try: try:
return os.listdir(path) return os.listdir(path)
@ -171,11 +184,36 @@ def readdir(path: str) -> list[str]:
def cp(src: str, dst: str): def cp(src: str, dst: str):
logger.log(f"Copying {src} to {dst}") logger.info(f"Copying {src} to {dst}")
shutil.copy(src, dst) shutil.copy(src, dst)
def mv(src: str, dst: str):
logger.info(f"Moving {src} to {dst}")
shutil.move(src, dst)
def cpTree(src: str, dst: str):
logger.info(f"Copying {src} to {dst}")
shutil.copytree(src, dst, dirs_exist_ok=True)
def cloneDir(url: str, path: str, dest: str) -> str:
with tempfile.TemporaryDirectory() as tmp:
mkdir(tmp)
exec(*["git", "clone", "-n", "--depth=1",
"--filter=tree:0", url, tmp, "-q"], quiet=True)
exec(*["git", "-C", tmp, "sparse-checkout",
"set", "--no-cone", path, "-q"], quiet=True)
exec(*["git", "-C", tmp, "checkout", "-q", "--no-progress"], quiet=True)
mv(os.path.join(tmp, path), dest)
return dest
LATEST_CACHE: dict[str, str] = {} LATEST_CACHE: dict[str, str] = {}
@ -194,7 +232,7 @@ def latest(cmd: str) -> str:
if cmd in LATEST_CACHE: if cmd in LATEST_CACHE:
return LATEST_CACHE[cmd] return LATEST_CACHE[cmd]
logger.log(f"Finding latest version of {cmd}") logger.info(f"Finding latest version of {cmd}")
regex = re.compile(r"^" + re.escape(cmd) + r"(-.[0-9]+)?(\.exe)?$") regex = re.compile(r"^" + re.escape(cmd) + r"(-.[0-9]+)?(\.exe)?$")
@ -206,12 +244,12 @@ def latest(cmd: str) -> str:
versions.append(f) versions.append(f)
if len(versions) == 0: if len(versions) == 0:
raise Exception(f"{cmd} not found") raise RuntimeError(f"{cmd} not found")
versions.sort() versions.sort()
chosen = versions[-1] chosen = versions[-1]
logger.log(f"Chosen {chosen} as latest version of {cmd}") logger.info(f"Chosen {chosen} as latest version of {cmd}")
LATEST_CACHE[cmd] = chosen LATEST_CACHE[cmd] = chosen

View file

@ -1,9 +1,8 @@
from typing import Any, TypeVar, cast import os
from typing import Any, TypeVar, cast, Optional, Union
import json import json
import hashlib import hashlib
from osdk import shell, const
T = TypeVar('T') T = TypeVar('T')
@ -16,7 +15,7 @@ def uniq(l: list[str]) -> list[str]:
return result return result
def hash(obj: Any, keys: list[str] = [], cls: type[json.JSONEncoder] | None = None) -> str: def hash(obj: Any, keys: list[str] = [], cls: Optional[type[json.JSONEncoder]] = None) -> str:
toHash = {} toHash = {}
if len(keys) == 0: if len(keys) == 0:
toHash = obj toHash = obj
@ -52,9 +51,13 @@ def key(obj: Any, keys: list[str] = []) -> str:
return "-".join(k) return "-".join(k)
def asList(i: T | list[T] | None) -> list[T]: def asList(i: Optional[Union[T, list[T]]]) -> list[T]:
if i is None: if i is None:
return [] return []
if isinstance(i, list): if isinstance(i, list):
return cast(list[T], i) return cast(list[T], i)
return [i] return [i]
def isNewer(path1: str, path2: str) -> bool:
return not os.path.exists(path2) or os.path.getmtime(path1) > os.path.getmtime(path2)

View file

@ -1,19 +1,22 @@
BLACK = "\033[0;30m" BLACK = "\033[30m"
RED = "\033[0;31m" RED = "\033[31m"
GREEN = "\033[0;32m" GREEN = "\033[32m"
BROWN = "\033[0;33m" BROWN = "\033[33m"
BLUE = "\033[0;34m" BLUE = "\033[34m"
PURPLE = "\033[0;35m" PURPLE = "\033[35m"
CYAN = "\033[0;36m" CYAN = "\033[36m"
LIGHT_GRAY = "\033[0;37m" WHITE = "\033[37m"
DARK_GRAY = "\033[1;30m"
LIGHT_RED = "\033[1;31m"
LIGHT_GREEN = "\033[1;32m" BRIGHT_BLACK = "\033[90m"
YELLOW = "\033[1;33m" BRIGHT_RED = "\033[91m"
LIGHT_BLUE = "\033[1;34m" BRIGHT_GREEN = "\033[92m"
LIGHT_PURPLE = "\033[1;35m" BRIGHT_BROWN = "\033[93m"
LIGHT_CYAN = "\033[1;36m" BRIGHT_BLUE = "\033[94m"
LIGHT_WHITE = "\033[1;37m" BRIGHT_PURPLE = "\033[95m"
BRIGHT_CYAN = "\033[96m"
BRIGHT_WHITE = "\033[97m"
BOLD = "\033[1m" BOLD = "\033[1m"
FAINT = "\033[2m" FAINT = "\033[2m"
ITALIC = "\033[3m" ITALIC = "\033[3m"
@ -25,10 +28,10 @@ RESET = "\033[0m"
def title(text: str): def title(text: str):
print(f"{LIGHT_WHITE}{text}{RESET}:") print(f"{BOLD}{text}{RESET}:")
def wordwrap(text: str, width: int = 60, newline="\n") -> str: def wordwrap(text: str, width: int = 60, newline: str = "\n") -> str:
result = "" result = ""
curr = 0 curr = 0

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -1,30 +0,0 @@
import sys
from os.path import isdir
from osdk import const, shell
from osdk.args import parse
from osdk.cmds import exec, usage
from osdk.plugins import loadAll
import osdk.vt100 as vt100
def main() -> int:
a = parse(sys.argv[1:])
if not a.consumeOpt("verbose", False):
if not isdir(const.OSDK_DIR):
shell.mkdir(const.OSDK_DIR)
sys.stderr = open(f"{const.OSDK_DIR}/osdk.log", "w")
try:
loadAll()
exec(a)
return 0
except Exception as e:
print(f"{vt100.RED}{e}{vt100.RESET}")
print()
usage()
print()
raise e

View file

@ -1,347 +0,0 @@
from typing import Callable, cast
import os
import json
from osdk.args import Args
from osdk.context import contextFor
from osdk import context, shell, const, vt100, builder, graph
Callback = Callable[[Args], None]
class Cmd:
shortName: str | None
longName: str
helpText: str
callback: Callable[[Args], None]
isPlugin: bool = False
def __init__(self, shortName: str | None, longName: str, helpText: str, callback: Callable[[Args], None]):
self.shortName = shortName
self.longName = longName
self.helpText = helpText
self.callback = callback
cmds: list[Cmd] = []
def append(cmd: Cmd):
cmd.isPlugin = True
cmds.append(cmd)
cmds.sort(key=lambda c: c.shortName or c.longName)
def runCmd(args: Args):
targetSpec = cast(str, args.consumeOpt(
"target", "host-" + shell.uname().machine))
componentSpec = args.consumeArg()
if componentSpec is None:
raise Exception("Component not specified")
exe = builder.build(componentSpec, targetSpec)
shell.exec(exe)
cmds += [Cmd("r", "run", "Run the target", runCmd)]
def debugCmd(args: Args):
targetSpec = cast(str, args.consumeOpt(
"target", "host-" + shell.uname().machine))
componentSpec = args.consumeArg()
if componentSpec is None:
raise Exception("Component not specified")
exe = builder.build(componentSpec, targetSpec)
shell.exec("lldb", "-o", "run", exe)
cmds += [Cmd("d", "debug", "Debug the target", debugCmd)]
def buildCmd(args: Args):
targetSpec = cast(str, args.consumeOpt(
"target", "host-" + shell.uname().machine))
componentSpec = args.consumeArg()
if componentSpec is None:
builder.buildAll(targetSpec)
else:
builder.build(componentSpec, targetSpec)
cmds += [Cmd("b", "build", "Build the target", buildCmd)]
def listCmd(args: Args):
components = context.loadAllComponents()
targets = context.loadAllTargets()
vt100.title("Components")
if len(components) == 0:
print(f" (No components available)")
else:
print(vt100.indent(vt100.wordwrap(
", ".join(map(lambda m: m.id, components)))))
print()
vt100.title("Targets")
if len(targets) == 0:
print(f" (No targets available)")
else:
print(vt100.indent(vt100.wordwrap(", ".join(map(lambda m: m.id, targets)))))
print()
cmds += [Cmd("l", "list", "List the targets", listCmd)]
def cleanCmd(args: Args):
shell.rmrf(const.BUILD_DIR)
cmds += [Cmd("c", "clean", "Clean the build directory", cleanCmd)]
def nukeCmd(args: Args):
shell.rmrf(const.OSDK_DIR)
cmds += [Cmd("n", "nuke", "Clean the build directory and cache", nukeCmd)]
def helpCmd(args: Args):
usage()
print()
vt100.title("Description")
print(" Operating System Development Kit.")
print()
vt100.title("Commands")
for cmd in cmds:
pluginText = ""
if cmd.isPlugin:
pluginText = f"{vt100.CYAN}(plugin){vt100.RESET}"
print(
f" {vt100.GREEN}{cmd.shortName or ' '}{vt100.RESET} {cmd.longName} - {cmd.helpText} {pluginText}")
print()
cmds += [Cmd("h", "help", "Show this help message", helpCmd)]
def versionCmd(args: Args):
print(f"OSDK v{const.get_version()}\n")
cmds += [Cmd("v", "version", "Show current version", versionCmd)]
def graphCmd(args: Args):
targetSpec = cast(str, args.consumeOpt(
"target", "host-" + shell.uname().machine))
scope: str | None = cast(str | None, args.tryConsumeOpt("scope"))
onlyLibs: bool = args.consumeOpt("only-libs", False) == True
showDisabled: bool = args.consumeOpt("show-disabled", False) == True
context = contextFor(targetSpec)
graph.view(context, scope=scope, showExe=not onlyLibs,
showDisabled=showDisabled)
cmds += [Cmd("g", "graph", "Show dependency graph", graphCmd)]
def installCmd(args: Args):
project = context.loadProject(".")
for extSpec in project.extern:
ext = project.extern[extSpec]
extPath = os.path.join(const.EXTERN_DIR, extSpec)
if os.path.exists(extPath):
print(f"Skipping {extSpec}, already installed")
continue
print(f"Installing {extSpec}-{ext.tag} from {ext.git}...")
shell.popen("git", "clone", "--depth", "1", "--branch",
ext.tag, ext.git, extPath)
cmds += [Cmd("i", "install", "Install all the external packages", installCmd)]
def initCmd(args: Args):
"""
|
| - project.json
| - src/
| | - project_name/
| | - main.c
| | - manifest.json
| - meta/
| | - targets/
| | | - host-*.json
| | - plugins/
| | | - run.py
| - .gitignore
| - README.md
|
"""
print("This utility will walk you through creating a new project.")
print("This only covers the most common items, and tries to give sensible defaults.")
print()
print("First, let's create a project.json file.")
project_name = input("Project name: ")
description = input("Description: ")
to_create = ["src", "meta", os.path.join("meta", "targets"), os.path.join("meta", "plugins")]
os.mkdir(project_name.lower())
for directory in to_create:
os.mkdir(os.path.join(project_name.lower(), directory))
with open(os.path.join(project_name.lower(), "project.json"), "w") as f:
f.write(json.dumps({
"$schema": "https://schemas.cute.engineering/latest/osdk.manifest.component",
"name": project_name,
"type": "project",
"description": description,
"extern": {},
}, indent=4))
with open(os.path.join(project_name.lower(), ".gitignore"), "w") as f:
f.write(".osdk\n.ninja_log\n__pycache__\n")
with open(os.path.join(project_name.lower(), "README.md"), "w") as f:
f.write(f"# {project_name}\n")
f.write("I was created using the OSDK!\n")
f.write(
"You can find more information about the OSDK in its [Repo](https://github.com/cute-engineering/osdk)."
)
with open(os.path.join(project_name.lower(), "src", "main.c"), "w") as f:
f.write("#include <stdio.h>\n\n")
f.write("int main(void)\n{\n")
f.write(" printf(\"Hello, World!\\n\");\n")
f.write(" return 0;\n")
f.write("}")
with open(os.path.join(project_name.lower(), "src", "manifest.json"), "w") as f:
f.write(json.dumps({
"$schema": "https://schemas.cute.engineering/latest/osdk.manifest.component",
"id": project_name.lower(),
"type": "exe",
"description": description,
}, indent=4))
with open(os.path.join(project_name.lower(), "meta", "plugins", "run.py"), "w") as f:
f.write("from osdk import builder, shell\n")
f.write("from osdk.args import Args\n")
f.write("from osdk.cmds import Cmd, append\n\n")
f.write("def runCmd(args: Args) -> None:\n")
f.write(
f" {project_name.lower()} = builder.build(\"{project_name.lower()}\", \"host-{shell.uname().machine}\")\n"
)
f.write(f" shell.exec(*[{project_name.lower()}])")
f.write("\n\nappend(Cmd(\"s\", \"start\", \"Run the project\", runCmd))")
with open(os.path.join(project_name.lower(), "meta", "targets", f"host-{shell.uname().machine}.json"), "w") as f:
f.write(json.dumps({
"$schema": "https://schemas.cute.engineering/latest/osdk.manifest.component",
"id": f"host-{shell.uname().machine}",
"type": "target",
"props": {
"arch": shell.uname().machine,
"toolchain": "clang",
"sys": [
"@uname",
"sysname"
],
"abi": "unknown",
"freestanding": False,
"host": True,
},
"tools": {
"cc": {
"cmd": [
"@latest",
"clang"
],
"args": []
},
"cxx": {
"cmd": [
"@latest",
"clang++"
],
"args": []
},
"ld": {
"cmd": [
"@latest",
"clang++"
],
"args": [
]
},
"ar": {
"cmd": [
"@latest",
"llvm-ar"
],
"args": [
"rcs"
]
},
"as": {
"cmd": "clang",
"args": [
"-c"
]
}
}
}, indent=4))
shell.exec(*["git", "init", project_name.lower()])
print("Done! Don't forget to add a LICENSE ;)")
cmds += [Cmd("I", "init", "Start a new project", initCmd)]
def usage():
print(f"Usage: {const.ARGV0} <command> [args...]")
def exec(args: Args):
cmd = args.consumeArg()
if cmd is None:
raise Exception("No command specified")
for c in cmds:
if c.shortName == cmd or c.longName == cmd:
c.callback(args)
return
raise Exception(f"Unknown command {cmd}")

View file

@ -1,14 +0,0 @@
import os
import sys
import subprocess
VERSION = "0.4.1"
MODULE_DIR = os.path.dirname(os.path.realpath(__file__))
ARGV0 = os.path.basename(sys.argv[0])
OSDK_DIR = ".osdk"
BUILD_DIR = os.path.join(OSDK_DIR, "build")
CACHE_DIR = os.path.join(OSDK_DIR, "cache")
EXTERN_DIR = os.path.join(OSDK_DIR, "extern")
SRC_DIR = "src"
META_DIR = f"meta"
TARGETS_DIR = os.path.join(META_DIR, "targets")

View file

@ -1,51 +0,0 @@
from typing import Any, cast, Callable, Final
import json
import osdk.shell as shell
Json = Any
Builtin = Callable[..., Json]
BUILTINS: Final[dict[str, Builtin]] = {
"uname": lambda arg: getattr(shell.uname(), arg).lower(),
"include": lambda arg: evalRead(arg),
"evalRead": lambda arg: evalRead(arg),
"join": lambda lhs, rhs: cast(Json, {**lhs, **rhs} if isinstance(lhs, dict) else lhs + rhs),
"concat": lambda *args: "".join(args),
"eval": lambda arg: eval(arg),
"read": lambda arg: read(arg),
"exec": lambda *args: shell.popen(*args).splitlines(),
"latest": lambda arg: shell.latest(arg),
}
def eval(jexpr: Json) -> Json:
if isinstance(jexpr, dict):
result = {}
for k in cast(dict[str, Json], jexpr):
result[k] = eval(jexpr[k])
return cast(Json, result)
elif isinstance(jexpr, list):
jexpr = cast(list[Json], jexpr)
if len(jexpr) > 0 and isinstance(jexpr[0], str) and jexpr[0].startswith("@"):
funcName = jexpr[0][1:]
if funcName in BUILTINS:
return BUILTINS[funcName](*eval(jexpr[1:]))
raise Exception(f"Unknown macro {funcName}")
else:
return list(map(eval, jexpr))
else:
return jexpr
def read(path: str) -> Json:
try:
with open(path, "r") as f:
return json.load(f)
except:
raise Exception(f"Failed to read {path}")
def evalRead(path: str) -> Json:
return eval(read(path))

View file

@ -1,21 +0,0 @@
import sys
import osdk.vt100 as vt100
class Logger:
name: str
def __init__(self, name: str):
self.name = name
def log(self, message: str):
print(
f"{vt100.CYAN}[{self.name}]{vt100.RESET} {message}", file=sys.stderr)
def warn(self, message: str):
print(
f"{vt100.YELLOW}[{self.name}]{vt100.RESET} {message}", file=sys.stderr)
def error(self, message: str):
print(
f"{vt100.RED}[{self.name}]{vt100.RESET} {message}", file=sys.stderr)

View file

@ -1,30 +0,0 @@
import os
import importlib.util as importlib
from osdk.logger import Logger
from osdk.shell import readdir
logger = Logger("plugins")
def load(path: str):
logger.log(f"Loading plugin {path}")
spec = importlib.spec_from_file_location("plugin", path)
if not spec or not spec.loader:
logger.error(f"Failed to load plugin {path}")
return None
module = importlib.module_from_spec(spec)
spec.loader.exec_module(module)
def loadAll():
logger.log("Loading plugins...")
for files in readdir(os.path.join("meta", "plugins")):
if files.endswith(".py"):
plugin = load(os.path.join("meta", "plugins", files))
if plugin:
print(f"Loaded plugin {plugin.name}")
plugin.init()

View file

@ -1,2 +1,2 @@
requests ~= 2.28.0 requests ~= 2.28.0
graphviz ~= 0.20.1 graphviz ~= 0.20.1

View file

@ -1,22 +1,23 @@
from setuptools import setup from setuptools import setup
from osdk.const import VERSION from cutekit.const import VERSION_STR, DESCRIPTION
setup( setup(
name="osdk", name="cutekit",
version=VERSION, version=VERSION_STR,
python_requires='>=3.10', python_requires='>=3.10',
description="Operating System Development Kit", description=DESCRIPTION,
author="Cute Engineering", author="Cute Engineering",
author_email="contact@cute.engineering", author_email="contact@cute.engineering",
url="https://cute.engineering/", url="https://cute.engineering/",
packages=["osdk"], packages=["cutekit"],
install_requires=[ install_requires=[
"requests", "requests",
"graphviz" "graphviz"
], ],
entry_points={ entry_points={
"console_scripts": [ "console_scripts": [
"osdk = osdk:main", "ck = cutekit:main",
"cutekit = cutekit:main",
], ],
}, },
license="MIT", license="MIT",