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:
deploy:
runs-on: ubuntu-latest
steps:

View file

@ -1,6 +1,7 @@
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
of this software and associated documentation files (the "Software"), to deal

View file

@ -1,30 +1,39 @@
# osdk
The operating system development kit
<br/>
<br/>
<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
- [osdk](#osdk)
- [Table of contents](#table-of-contents)
- [Macros](#macros)
- [`@latest`](#latest)
- [`@uname`](#uname)
- [`@include`](#include)
- [`@join`](#join)
- [`@concat`](#concat)
- [`@exec`](#exec)
- [Manifest file format](#manifest-file-format)
- [`id`](#id)
- [`type`](#type)
- [`description`](#description)
- [`enabledIf`](#enabledif)
- [`requires`](#requires)
- [`provides`](#provides)
- [Target file format](#target-file-format)
- [`id`](#id-1)
- [`type`](#type-1)
- [`props`](#props)
- [`tools`](#tools)
- [Table of contents](#table-of-contents)
- [Macros](#macros)
- [`@latest`](#latest)
- [`@uname`](#uname)
- [`@include`](#include)
- [`@join`](#join)
- [`@concat`](#concat)
- [`@exec`](#exec)
- [Manifest file format](#manifest-file-format)
- [`id`](#id)
- [`type`](#type)
- [`description`](#description)
- [`enabledIf`](#enabledif)
- [`requires`](#requires)
- [`provides`](#provides)
- [Target file format](#target-file-format)
- [`id`](#id-1)
- [`type`](#type-1)
- [`props`](#props)
- [`tools`](#tools)
## 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`

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:
@ -17,23 +20,23 @@ class Args:
del self.opts[key]
return result
def consumeOpt(self, key: str, default: Value) -> Value:
def consumeOpt(self, key: str, default: Value = False) -> Value:
if key in self.opts:
result = self.opts[key]
del self.opts[key]
return result
return default
def tryConsumeOpt(self, key: str) -> Value | None:
def tryConsumeOpt(self, key: str) -> Optional[Value]:
if key in self.opts:
result = self.opts[key]
del self.opts[key]
return result
return None
def consumeArg(self) -> str | None:
def consumeArg(self, default: Optional[str] = None) -> Optional[str]:
if len(self.args) == 0:
return None
return default
first = 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 logging
from typing import TextIO
from osdk.model import ComponentManifest, TargetManifest, Props
from osdk.ninja import Writer
from osdk.logger import Logger
from osdk.context import Context, contextFor
from osdk import shell, rules
from cutekit.model import Props
from cutekit.ninja import Writer
from cutekit.context import ComponentInstance, Context, contextFor
from cutekit import shell, rules
logger = Logger("builder")
logger = logging.getLogger(__name__)
def gen(out: TextIO, context: Context):
@ -28,6 +28,9 @@ def gen(out: TextIO, context: Context):
writer.newline()
writer.rule("cp", "cp $in $out")
writer.newline()
for i in target.tools:
tool = target.tools[i]
rule = rules.rules[i]
@ -43,22 +46,26 @@ def gen(out: TextIO, context: Context):
all: list[str] = []
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"Resolved: {', '.join(instance.resolved)}")
for obj in objects:
r = rules.byFileIn(obj[0])
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]
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()
if instance.isLib():
writer.build(instance.libfile(context), "ar",
list(map(lambda o: o[1], objects)))
writer.build(instance.outfile(), "ar",
list(map(lambda o: o[1], objects)), implicit=list(map(lambda o: o[1], assets)))
else:
libraries: list[str] = []
@ -66,17 +73,17 @@ def gen(out: TextIO, context: Context):
reqInstance = context.componentByName(req)
if reqInstance is None:
raise Exception(f"Component {req} not found")
raise RuntimeError(f"Component {req} not found")
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",
list(map(lambda o: o[1], objects)) + libraries)
writer.build(instance.outfile(), "ld", list(
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()
@ -86,7 +93,7 @@ def gen(out: TextIO, context: Context):
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)
shell.mkdir(context.builddir())
@ -95,18 +102,21 @@ def build(componentSpec: str, targetSpec: str, props: Props = {}) -> str:
with open(ninjaPath, "w") as f:
gen(f, context)
instance = context.componentByName(componentSpec)
instances = map(lambda i: context.componentByName(i), componentSpec)
if instance is None:
raise Exception(f"Component {componentSpec} not found")
for instance in instances:
if not instance.enabled:
raise RuntimeError(
f"Component {componentSpec} is disabled: {instance.disableReason}")
if not instance.enabled:
raise Exception(
f"Component {componentSpec} is disabled: {instance.disableReason}")
shell.exec(f"ninja", "-v", "-f", ninjaPath,
*map(lambda i: i.outfile(), instances))
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:
@ -120,9 +130,8 @@ class Paths:
self.obj = obj
def buildAll(targetSpec: str) -> Paths:
def buildAll(targetSpec: str) -> Context:
context = contextFor(targetSpec)
target = context.target
shell.mkdir(context.builddir())
ninjaPath = os.path.join(context.builddir(), "build.ninja")
@ -132,8 +141,24 @@ def buildAll(targetSpec: str) -> Paths:
shell.exec(f"ninja", "-v", "-f", ninjaPath)
return Paths(
os.path.join(context.builddir(), "bin"),
os.path.join(context.builddir(), "lib"),
os.path.join(context.builddir(), "obj")
)
return context
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 pathlib import Path
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
from osdk.logger import Logger
from osdk import const, shell, jexpr, utils, rules, mixins
logger = Logger("context")
logger = logging.getLogger(__name__)
class IContext(Protocol):
target: TargetManifest
def builddir(self) -> str:
...
@ -21,7 +22,9 @@ class ComponentInstance:
disableReason = ""
manifest: ComponentManifest
sources: list[str] = []
res: list[str] = []
resolved: list[str] = []
context: IContext
def __init__(
self,
@ -29,43 +32,54 @@ class ComponentInstance:
disableReason: str,
manifest: ComponentManifest,
sources: list[str],
res: list[str],
resolved: list[str]):
self.enabled = enabled
self.disableReason = disableReason
self.manifest = manifest
self.sources = sources
self.res = res
self.resolved = resolved
def id(self) -> str:
return self.manifest.id
def isLib(self):
return self.manifest.type == Type.LIB
def binfile(self, context: IContext) -> str:
return os.path.join(context.builddir(), "bin", f"{self.manifest.id}.out")
def objdir(self) -> str:
return os.path.join(self.context.builddir(), f"{self.manifest.id}/obj")
def objdir(self, context: IContext) -> str:
return os.path.join(context.builddir(), "obj", self.manifest.id)
def resdir(self) -> str:
return os.path.join(self.context.builddir(), f"{self.manifest.id}/res")
def objsfiles(self, context: IContext) -> list[tuple[str, str]]:
return list(
map(
lambda s: (
s, os.path.join(self.objdir(context), s.replace(os.path.join(self.manifest.dirname(), ''), '') + ".o")),
self.sources))
def objsfiles(self) -> list[tuple[str, str]]:
def toOFile(s: str) -> str:
return os.path.join(self.objdir(), s.replace(os.path.join(self.manifest.dirname(), ''), '') + ".o")
return list(map(lambda s: (s, toOFile(s)), self.sources))
def libfile(self, context: IContext) -> str:
return os.path.join(context.builddir(), "lib", f"{self.manifest.id}.a")
def resfiles(self) -> list[tuple[str, str, str]]:
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():
return self.libfile(context)
return os.path.join(self.context.builddir(), self.manifest.id, f"lib/{self.manifest.id}.a")
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:
if "cpp-root-include" in self.manifest.props:
return self.manifest.dirname()
else:
elif self.manifest.type == Type.LIB:
return str(Path(self.manifest.dirname()).parent)
else:
return ""
class Context(IContext):
@ -81,31 +95,44 @@ class Context(IContext):
self.instances = instances
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))
if len(result) == 0:
return None
raise RuntimeError(f"Component '{name}' not found")
return result[0]
def cincls(self) -> list[str]:
includes = list(
map(lambda x: x.cinclude(), self.enabledInstances()))
includes = list(filter(lambda x: x != "", map(
lambda x: x.cinclude(), self.enabledInstances())))
return utils.uniq(includes)
def cdefs(self) -> list[str]:
return self.target.cdefs()
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:
return os.path.join(const.BUILD_DIR, f"{self.target.id}-{self.hashid()[:8]}")
def loadAllTargets() -> list[TargetManifest]:
files = shell.find(const.TARGETS_DIR, ["*.json"])
return list(
map(lambda path: TargetManifest(jexpr.evalRead(path), path), files))
projectRoot = project.root()
if projectRoot is None:
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:
@ -117,7 +144,7 @@ def loadTarget(id: str) -> TargetManifest:
try:
return next(filter(lambda t: t.id == id, loadAllTargets()))
except StopIteration:
raise Exception(f"Target '{id}' not found")
raise RuntimeError(f"Target '{id}' not found")
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))
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(
filter(lambda c: c.id == what, components))
@ -167,7 +194,7 @@ def resolveDeps(componentSpec: str, components: list[ComponentManifest], target:
return False, unresolvedReason, []
if resolved in stack:
raise Exception(f"Dependency loop: {stack} -> {resolved}")
raise RuntimeError(f"Dependency loop: {stack} -> {resolved}")
stack.append(resolved)
@ -191,30 +218,39 @@ def resolveDeps(componentSpec: str, components: list[ComponentManifest], target:
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))
wildcards = set(
chain(*map(lambda rule: rule.fileIn, rules.rules.values())))
sources = shell.find(
manifest.subdirs, list(wildcards), recusive=False)
res = shell.find(os.path.join(manifest.dirname(), "res"))
enabled, unresolvedReason, resolved = resolveDeps(
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:
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:
if targetSpec in context:
return context[targetSpec]
logger.log(f"Loading context for '{targetSpec}'")
logger.info(f"Loading context for '{targetSpec}'")
targetEls = targetSpec.split(":")
@ -261,4 +297,7 @@ def contextFor(targetSpec: str, props: Props = {}) -> Context:
tools,
)
for instance in instances:
instance.context = context[targetSpec]
return context[targetSpec]

View file

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

View file

@ -1,14 +1,12 @@
import os
from enum import Enum
from typing import Any
from json import JSONEncoder
import logging
from osdk.jexpr import Json
from osdk.logger import Logger
from osdk import const, utils
from cutekit.jexpr import Json
logger = Logger("model")
logger = logging.getLogger(__name__)
Props = dict[str, Any]
@ -26,25 +24,32 @@ class Manifest:
type: Type = Type.UNKNOWN
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 not "id" in json:
raise ValueError("Missing id")
raise RuntimeError("Missing id")
self.id = json["id"]
if not "type" in json and strict:
raise ValueError("Missing type")
raise RuntimeError("Missing type")
self.type = Type(json["type"])
self.path = path
elif strict:
raise ValueError("Missing json")
raise RuntimeError("Missing json")
for key in kwargs:
setattr(self, key, kwargs[key])
def toJson(self) -> Json:
return {
"id": self.id,
"type": self.type.value,
"path": self.path
}
def __str__(self):
return f"Manifest(id={self.id}, type={self.type}, path={self.path})"
@ -59,23 +64,29 @@ class Extern:
git: 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 not "git" in json and strict:
raise ValueError("Missing git")
raise RuntimeError("Missing git")
self.git = json["git"]
if not "tag" in json and strict:
raise ValueError("Missing tag")
raise RuntimeError("Missing tag")
self.tag = json["tag"]
elif strict:
raise ValueError("Missing json")
raise RuntimeError("Missing json")
for key in kwargs:
setattr(self, key, kwargs[key])
def toJson(self) -> Json:
return {
"git": self.git,
"tag": self.tag
}
def __str__(self):
return f"Extern(git={self.git}, tag={self.tag})"
@ -87,20 +98,27 @@ class ProjectManifest(Manifest):
description: str = ""
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 not "description" in json and strict:
raise ValueError("Missing description")
raise RuntimeError("Missing description")
self.description = json["description"]
self.extern = {k: Extern(v)
for k, v in json.get("extern", {}).items()}
elif strict:
raise ValueError("Missing json")
raise RuntimeError("Missing json")
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):
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] = []
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 not "cmd" in json and strict:
raise ValueError("Missing cmd")
raise RuntimeError("Missing cmd")
self.cmd = json.get("cmd", self.cmd)
if not "args" in json and strict:
raise ValueError("Missing args")
raise RuntimeError("Missing args")
self.args = json.get("args", [])
self.files = json.get("files", [])
elif strict:
raise ValueError("Missing json")
raise RuntimeError("Missing json")
for key in kwargs:
setattr(self, key, kwargs[key])
def toJson(self) -> Json:
return {
"cmd": self.cmd,
"args": self.args,
"files": self.files
}
def __str__(self):
return f"Tool(cmd={self.cmd}, args={self.args}, files={self.files})"
@ -147,15 +172,15 @@ class TargetManifest(Manifest):
tools: Tools
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 not "props" in json and strict:
raise ValueError("Missing props")
raise RuntimeError("Missing props")
self.props = json["props"]
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()}
@ -163,6 +188,14 @@ class TargetManifest(Manifest):
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):
return f"TargetManifest({self.id})"
@ -178,9 +211,9 @@ class TargetManifest(Manifest):
macrovalue = str(prop).lower().replace(" ", "_").replace("-", "_")
if isinstance(prop, bool):
if prop:
defines += [f"-D__osdk_{macroname}__"]
defines += [f"-D__ck_{macroname}__"]
else:
defines += [f"-D__osdk_{macroname}_{macrovalue}__"]
defines += [f"-D__ck_{macroname}_{macrovalue}__"]
return defines
@ -194,7 +227,7 @@ class ComponentManifest(Manifest):
provides: 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:
self.decription = json.get("description", self.decription)
self.props = json.get("props", self.props)
@ -208,19 +241,31 @@ class ComponentManifest(Manifest):
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):
return f"ComponentManifest({self.id})"
def isEnabled(self, target: TargetManifest) -> tuple[bool, str]:
for k, v in self.enableIf.items():
if not k in target.props:
logger.log(
logger.info(
f"Component {self.id} disabled by missing {k} in target")
return False, f"Missing props '{k}' in target"
if not target.props[k] 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}")
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
from typing import TextIO, Union
from osdk.utils import asList
from cutekit.utils import asList
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:
id: str
fileIn: list[str]
fileOut: list[str]
rule: 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.fileIn = fileIn
self.fileOut = fileOut
@ -14,17 +17,18 @@ class Rule:
self.args = args
self.deps = deps
rules: dict[str, Rule] = {
"cc": Rule("cc", ["*.c"], ["*.o"], "-c -o $out $in -MD -MF $out.d $flags $cincs $cdefs", ["-std=gnu2x",
"-Wall",
"-Wextra",
"-Werror"], "$out.d"),
"-Wall",
"-Wextra",
"-Werror"], "$out.d"),
"cxx": Rule("cxx", ["*.cpp", "*.cc", "*.cxx"], ["*.o"], "-c -o $out $in -MD -MF $out.d $flags $cincs $cdefs", ["-std=gnu++2b",
"-Wall",
"-Wextra",
"-Werror",
"-fno-exceptions",
"-fno-rtti"], "$out.d"),
"-Wall",
"-Wextra",
"-Werror",
"-fno-exceptions",
"-fno-rtti"], "$out.d"),
"as": Rule("as", ["*.s", "*.asm", "*.S"], ["*.o"], "-o $out $in $flags"),
"ar": Rule("ar", ["*.o"], ["*.a"], "$flags $out $in"),
"ld": Rule("ld", ["*.o", "*.a"], ["*.out"], "-o $out $in $flags"),
@ -35,7 +39,7 @@ def append(rule: Rule):
rules[rule.id] = rule
def byFileIn(fileIn: str) -> Rule | None:
def byFileIn(fileIn: str) -> Optional[Rule]:
for key in rules:
rule = rules[key]
for ext in rule.fileIn:
@ -44,5 +48,5 @@ def byFileIn(fileIn: str) -> Rule | None:
return None
def byId(id: str) -> Rule | None:
def byId(id: str) -> Optional[Rule]:
return rules.get(id, None)

View file

@ -8,11 +8,14 @@ import re
import shutil
import fnmatch
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:
@ -33,6 +36,8 @@ def uname() -> Uname:
result.machine = "arm64"
case "AMD64":
result.machine = "x86_64"
case _:
pass
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]:
logger.log(f"Looking for files in {path} matching {wildcards}")
logger.info(f"Looking for files in {path} matching {wildcards}")
result: list[str] = []
@ -79,7 +84,7 @@ def find(path: str | list[str], wildcards: list[str] = [], recusive: bool = True
def mkdir(path: str) -> str:
logger.log(f"Creating directory {path}")
logger.info(f"Creating directory {path}")
try:
os.makedirs(path)
@ -90,7 +95,7 @@ def mkdir(path: str) -> str:
def rmrf(path: str) -> bool:
logger.log(f"Removing directory {path}")
logger.info(f"Removing directory {path}")
if not os.path.exists(path):
return False
@ -98,17 +103,18 @@ def rmrf(path: str) -> bool:
return True
def wget(url: str, path: str | None = None) -> str:
def wget(url: str, path: Optional[str] = None) -> str:
import requests
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())
if os.path.exists(path):
return path
logger.log(f"Downloading {url} to {path}")
logger.info(f"Downloading {url} to {path}")
r = requests.get(url, stream=True)
r.raise_for_status()
@ -121,48 +127,55 @@ def wget(url: str, path: str | None = None) -> str:
return path
def exec(*args: str):
logger.log(f"Executing {args}")
def exec(*args: str, quiet: bool = False) -> bool:
logger.info(f"Executing {args}")
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:
raise Exception(f"{args[0]}: Command not found")
raise RuntimeError(f"{args[0]}: Command not found")
except KeyboardInterrupt:
raise Exception(f"{args[0]}: Interrupted")
raise RuntimeError(f"{args[0]}: Interrupted")
if proc.returncode == -signal.SIGSEGV:
raise Exception(f"{args[0]}: Segmentation fault")
raise RuntimeError(f"{args[0]}: Segmentation fault")
if proc.returncode != 0:
raise Exception(
raise RuntimeError(
f"{args[0]}: Process exited with code {proc.returncode}")
return True
def popen(*args: str) -> str:
logger.log(f"Executing {args}")
logger.info(f"Executing {args}")
try:
proc = subprocess.run(args, stdout=subprocess.PIPE, stderr=sys.stderr)
except FileNotFoundError:
raise Exception(f"{args[0]}: Command not found")
raise RuntimeError(f"{args[0]}: Command not found")
if proc.returncode == -signal.SIGSEGV:
raise Exception(f"{args[0]}: Segmentation fault")
raise RuntimeError(f"{args[0]}: Segmentation fault")
if proc.returncode != 0:
raise Exception(
raise RuntimeError(
f"{args[0]}: Process exited with code {proc.returncode}")
return proc.stdout.decode('utf-8')
def readdir(path: str) -> list[str]:
logger.log(f"Reading directory {path}")
logger.info(f"Reading directory {path}")
try:
return os.listdir(path)
@ -171,11 +184,36 @@ def readdir(path: str) -> list[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)
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] = {}
@ -194,7 +232,7 @@ def latest(cmd: str) -> str:
if cmd in LATEST_CACHE:
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)?$")
@ -206,12 +244,12 @@ def latest(cmd: str) -> str:
versions.append(f)
if len(versions) == 0:
raise Exception(f"{cmd} not found")
raise RuntimeError(f"{cmd} not found")
versions.sort()
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

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 hashlib
from osdk import shell, const
T = TypeVar('T')
@ -16,7 +15,7 @@ def uniq(l: list[str]) -> list[str]:
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 = {}
if len(keys) == 0:
toHash = obj
@ -52,9 +51,13 @@ def key(obj: Any, keys: list[str] = []) -> str:
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:
return []
if isinstance(i, list):
return cast(list[T], 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"
RED = "\033[0;31m"
GREEN = "\033[0;32m"
BROWN = "\033[0;33m"
BLUE = "\033[0;34m"
PURPLE = "\033[0;35m"
CYAN = "\033[0;36m"
LIGHT_GRAY = "\033[0;37m"
DARK_GRAY = "\033[1;30m"
LIGHT_RED = "\033[1;31m"
LIGHT_GREEN = "\033[1;32m"
YELLOW = "\033[1;33m"
LIGHT_BLUE = "\033[1;34m"
LIGHT_PURPLE = "\033[1;35m"
LIGHT_CYAN = "\033[1;36m"
LIGHT_WHITE = "\033[1;37m"
BLACK = "\033[30m"
RED = "\033[31m"
GREEN = "\033[32m"
BROWN = "\033[33m"
BLUE = "\033[34m"
PURPLE = "\033[35m"
CYAN = "\033[36m"
WHITE = "\033[37m"
BRIGHT_BLACK = "\033[90m"
BRIGHT_RED = "\033[91m"
BRIGHT_GREEN = "\033[92m"
BRIGHT_BROWN = "\033[93m"
BRIGHT_BLUE = "\033[94m"
BRIGHT_PURPLE = "\033[95m"
BRIGHT_CYAN = "\033[96m"
BRIGHT_WHITE = "\033[97m"
BOLD = "\033[1m"
FAINT = "\033[2m"
ITALIC = "\033[3m"
@ -25,10 +28,10 @@ RESET = "\033[0m"
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 = ""
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
graphviz ~= 0.20.1
graphviz ~= 0.20.1

View file

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