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
37 changed files with 2312 additions and 2127 deletions

View file

@ -1,40 +0,0 @@
name: Checks
on:
pull_request:
push:
branches:
- stable
- dev
jobs:
checks:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip install mypy pytest ruff
- name: Type Checking
run: |
python -m mypy --install-types --non-interactive .
- name: Linting
run: |
ruff check cutekit
- name: Unit Testing
run: |
python -m pytest

View file

@ -20,14 +20,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.11'
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip

268
README.md
View file

@ -6,7 +6,7 @@
</p>
<h1 align="center">CuteKit</h1>
<p align="center">
The *magical* build system and package manager
The Cute build system and package manager
</p>
<br/>
<br/>
@ -15,52 +15,252 @@
## Table of contents
- [Table of contents](#table-of-contents)
- [Introduction](#introduction)
- [Quick-start](#quick-start)
- [Example](#example)
- [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)
## Introduction
## Macros
**CuteKit** is a simple - yet - powerful build system and package manager for C and C++. It:
- ✨ It uses **JSON**: Cutekit uses JSON instead of introducing a whole new programming language for describing the project. And also has macros to help the user experience (see [Jexpr](doc/spec/jexpr.md)).
- ✨ It's a **package manager**: Cutekit package manager is based on **Git**, nothing is centralized.
- ✨ It's **extendible**: Cutekit can be [extended](./doc/extends.md) by writing custom Python plugins.
- ✨ It's **easy**: the [**templates**](./doc/templates.md) help the user quick-start a project.
- ✨ It's **portable**: Cutekit can run on MacOS Gnu/Linux and Windows.
### `@latest`
## Installation
Find the latest version of a command in the path.
To install Cutekit, you may use your favourite package manager if it is available. Or you can install it manually by following the instructions below.
```bash
$ git clone https://github.com/cute-engineering/cutekit
$ cd cutekit
# If you want to use the latest version of Cutekit, you can switch to the dev branch.
# $ git switch dev
$ pip install --user -e .
```json
"cc": {
"cmd": ["@latest", "clang"], // clang-14
/* ... */
```
## Quick-start
### `@uname`
-> If you directly want to start using Cutekit for a new project, you can just run `$ ck I host` and it will create a new project in the host directory (you can rename it later).
Query the system for information about the current operating system.
-> If you want to use Cutekit for writing operating systems, you can create a new [limine](https://github.com/limine-bootloader/limine/)-based project by running `$ ck I limine-barebone`.
## Example
```json
"cc": {
"cmd": ["@uname", "machine"], // "x86_64"
/* ... */
```
If you want to see how it works you can read the [doc/cutekit.md](doc/cutekit.md) file.
### `@include`
# License
Include a file
<a href="https://opensource.org/licenses/MIT">
<img align="right" height="96" alt="MIT License" src="doc/mit.svg" />
</a>
### `@join`
Cutekit is licensed under the **MIT License**.
Join two objects
The full text of the license can be accessed via [this link](https://opensource.org/licenses/MIT) and is also included in the [license.md](license.md) file of this software package.
Example:
```json
["@join", {"a": 1}, {"b": 2}] // {"a": 1, "b": 2}
```
### `@concat`
Concatenate strings
Example:
```json
["@concat", "a", "b", "c"] // "abc"
```
### `@exec`
Execute a command and return the output
Example:
```json
["@exec", "uname", "-m"] // "x86_64"
```
## Manifest file format
### `id`
The id of the package. This is used to identify the package in the manifest file.
Exemple:
```json
{
"id": "hello"
}
```
### `type`
The type of the package. This is used to identify the package in the manifest file.
Exemple:
```json
{
"type": "exe"
}
```
### `description`
The description of the package for the user.
Exemple:
```json
{
"description": "Hello world"
}
```
### `enabledIf`
A list of requirements for the package check agaisnt the build props. If the requirement is not met, the package will be disabled.
```json
{
"enabledIf": {
"freestanding": [
false
]
}
}
```
### `requires`
Dependencies of the package. The name listed here must be the same as the id of the package or member of a provide list.
Exemple:
```json
{
"requires": [
"libc",
"libm"
]
}
```
### `provides`
Alias for the package.
Exemple:
```json
{
"provides": [
"hello"
]
}
```
## Target file format
### `id`
The id of the target. This is used to identify the target in the target file.
### `type`
Should be `target`.
### `props`
A list of properties for the target.
Exemple:
```json
{
"props": {
"arch": "x86_64",
"vendor": "pc",
"os": "linux",
"env": "gnu",
"abi": "elf",
"cpu": "x86_64",
"features": "fxsr,sse,sse2"
}
}
```
Theses values are exposed the translation unit as `__ck_{prop}__`.
### `tools`
A list of tools for the target.
```json
{
"tools": {
"cc": {
"cmd": ["@latest", "clang"],
"args": [
"-target",
"x86_64-unknown-windows",
"-ffreestanding",
"-fno-stack-protector",
"-fshort-wchar",
"-mno-red-zone"
]
},
"cxx": {
"cmd": ["@latest", "clang++"],
"args": [
"-target",
"x86_64-unknown-windows",
"-ffreestanding",
"-fno-stack-protector",
"-fshort-wchar",
"-mno-red-zone"
]
},
"ld": {
"cmd": ["@latest", "clang++"],
"args": [
"-target",
"x86_64-unknown-windows",
"-nostdlib",
"-Wl,-entry:efi_main",
"-Wl,-subsystem:efi_application",
"-fuse-ld=lld-link"
]
},
"ar": {
"cmd": ["@latest", "llvm-ar"],
"args": [
"rcs"
]
},
"as": {
"cmd": "nasm",
"args": [
"-f",
"win64"
]
}
}
}
```

View file

@ -2,42 +2,21 @@ import sys
import os
import logging
from . import (
builder, # noqa: F401 this is imported for side effects
cli,
const,
graph, # noqa: F401 this is imported for side effects
model,
plugins,
vt100,
)
def ensure(version: tuple[int, int, int]):
if (
const.VERSION[0] == version[0]
and const.VERSION[1] == version[1]
and const.VERSION[2] >= version[2]
):
return
raise RuntimeError(
f"Expected cutekit version {version[0]}.{version[1]}.{version[2]} but found {const.VERSION_STR}"
)
from cutekit import const, project, vt100, plugins, cmds
def setupLogger(verbose: bool):
if verbose:
logging.basicConfig(
level=logging.DEBUG,
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 = model.Project.topmost()
projectRoot = project.root()
logFile = const.GLOBAL_LOG_FILE
if projectRoot is not None:
logFile = os.path.join(projectRoot.dirname(), const.PROJECT_LOG_FILE)
logFile = os.path.join(projectRoot, const.PROJECT_LOG_FILE)
# create the directory if it doesn't exist
logDir = os.path.dirname(logFile)
@ -48,23 +27,22 @@ def setupLogger(verbose: bool):
level=logging.INFO,
filename=logFile,
filemode="w",
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
format=f"%(asctime)s %(levelname)s %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
def main() -> int:
try:
a = cli.parse(sys.argv[1:])
setupLogger(a.consumeOpt("verbose", False) is True)
setupLogger(False)
plugins.loadAll()
cli.exec(a)
cmds.exec(sys.argv)
print()
return 0
except RuntimeError as e:
logging.exception(e)
cli.error(str(e))
cli.usage()
cmds.error(str(e))
cmds.usage()
print()
return 1
except KeyboardInterrupt:

59
cutekit/args.py Normal file
View file

@ -0,0 +1,59 @@
from typing import Optional, Union
Value = Union[str, bool, int]
class Args:
opts: dict[str, Value]
args: list[str]
def __init__(self):
self.opts = {}
self.args = []
def consumePrefix(self, prefix: str) -> dict[str, Value]:
result: dict[str, Value] = {}
for key, value in self.opts.items():
if key.startswith(prefix):
result[key[len(prefix):]] = value
del self.opts[key]
return result
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) -> Optional[Value]:
if key in self.opts:
result = self.opts[key]
del self.opts[key]
return result
return None
def consumeArg(self, default: Optional[str] = None) -> Optional[str]:
if len(self.args) == 0:
return default
first = self.args[0]
del self.args[0]
return first
def parse(args: list[str]) -> Args:
result = Args()
for arg in args:
if arg.startswith("--"):
if "=" in arg:
key, value = arg[2:].split("=", 1)
result.opts[key] = value
else:
result.opts[arg[2:]] = True
else:
result.args.append(arg)
return result

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,298 +1,164 @@
import os
import logging
import dataclasses as dt
from typing import TextIO
from pathlib import Path
from typing import TextIO, Union
from cutekit.model import Props
from cutekit.ninja import Writer
from cutekit.context import ComponentInstance, Context, contextFor
from cutekit import shell, rules
from . import shell, rules, model, ninja, const, cli
_logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__)
def aggregateCincs(target: model.Target, registry: model.Registry) -> set[str]:
res = set()
def gen(out: TextIO, context: Context):
writer = Writer(out)
for c in registry.iterEnabled(target):
if "cpp-root-include" in c.props:
res.add(c.dirname())
elif c.type == model.Kind.LIB:
res.add(str(Path(c.dirname()).parent))
target = context.target
return set(map(lambda i: f"-I{i}", res))
writer.comment("File generated by the build system, do not edit")
writer.newline()
writer.variable("builddir", context.builddir())
writer.separator("Tools")
def aggregateCdefs(target: model.Target) -> set[str]:
res = set()
writer.variable("cincs", " ".join(
map(lambda i: f"-I{i}", context.cincls())))
def sanatize(s: str) -> str:
return s.lower().replace(" ", "_").replace("-", "_").replace(".", "_")
writer.variable("cdefs", " ".join(context.cdefs()))
for k, v in target.props.items():
if isinstance(v, bool):
if v:
res.add(f"-D__ck_{sanatize(k)}__")
else:
res.add(f"-D__ck_{sanatize(k)}_{sanatize(str(v))}__")
res.add(f"-D__ck_{sanatize(k)}_value={str(v)}")
writer.newline()
return res
def buildpath(target: model.Target, component: model.Component, path) -> Path:
return Path(target.builddir) / component.id / path
# --- Compilation ------------------------------------------------------------ #
def wilcard(component: model.Component, wildcards: list[str]) -> list[str]:
dirs = [component.dirname()] + list(
map(lambda d: os.path.join(component.dirname(), d), component.subdirs)
)
return shell.find(dirs, list(wildcards), recusive=False)
def compile(
w: ninja.Writer,
target: model.Target,
component: model.Component,
rule: str,
srcs: list[str],
) -> list[str]:
res: list[str] = []
for src in srcs:
rel = Path(src).relative_to(component.dirname())
dest = buildpath(target, component, "obj") / rel.with_suffix(".o")
t = target.tools[rule]
w.build(str(dest), rule, inputs=src, order_only=t.files)
res.append(str(dest))
return res
# --- Ressources ------------------------------------------------------------- #
def listRes(component: model.Component) -> list[str]:
return shell.find(str(component.subpath("res")))
def compileRes(
w: ninja.Writer,
target: model.Target,
component: model.Component,
) -> list[str]:
res: list[str] = []
for r in listRes(component):
rel = Path(r).relative_to(component.subpath("res"))
dest = buildpath(target, component, "res") / rel
w.build(str(dest), "cp", r)
res.append(str(dest))
return res
# --- Linking ---------------------------------------------------------------- #
def outfile(target: model.Target, component: model.Component) -> str:
if component.type == model.Kind.LIB:
return str(buildpath(target, component, f"lib/{component.id}.a"))
else:
return str(buildpath(target, component, f"bin/{component.id}.out"))
def collectLibs(
registry: model.Registry, target: model.Target, component: model.Component
) -> list[str]:
res: list[str] = []
for r in component.resolved[target.id].resolved:
req = registry.lookup(r, model.Component)
assert req is not None # model.Resolver has already checked this
if r == component.id:
continue
if not req.type == model.Kind.LIB:
raise RuntimeError(f"Component {r} is not a library")
res.append(outfile(target, req))
return res
def link(
w: ninja.Writer,
registry: model.Registry,
target: model.Target,
component: model.Component,
) -> str:
w.newline()
out = outfile(target, component)
objs = []
objs += compile(w, target, component, "cc", wilcard(component, ["*.c"]))
objs += compile(
w, target, component, "cxx", wilcard(component, ["*.cpp", "*.cc", "*.cxx"])
)
objs += compile(
w, target, component, "as", wilcard(component, ["*.s", "*.asm", "*.S"])
)
res = compileRes(w, target, component)
libs = collectLibs(registry, target, component)
if component.type == model.Kind.LIB:
w.build(out, "ar", objs, implicit=res)
else:
w.build(out, "ld", objs + libs, implicit=res)
return out
# --- Phony ------------------------------------------------------------------ #
def all(w: ninja.Writer, registry: model.Registry, target: model.Target) -> list[str]:
all: list[str] = []
for c in registry.iterEnabled(target):
all.append(link(w, registry, target, c))
w.build("all", "phony", all)
w.default("all")
return all
def gen(out: TextIO, target: model.Target, registry: model.Registry):
w = ninja.Writer(out)
w.comment("File generated by the build system, do not edit")
w.newline()
w.variable("builddir", target.builddir)
w.variable("hashid", target.hashid)
w.separator("Tools")
w.variable("cincs", " ".join(aggregateCincs(target, registry)))
w.variable("cdefs", " ".join(aggregateCdefs(target)))
w.newline()
writer.rule("cp", "cp $in $out")
writer.newline()
for i in target.tools:
tool = target.tools[i]
rule = rules.rules[i]
w.variable(i, tool.cmd)
w.variable(i + "flags", " ".join(rule.args + tool.args))
w.rule(
i,
f"{tool.cmd} {rule.rule.replace('$flags',f'${i}flags')}",
depfile=rule.deps,
)
w.newline()
writer.variable(i, tool.cmd)
writer.variable(
i + "flags", " ".join(rule.args + tool.args))
writer.rule(
i, f"{tool.cmd} {rule.rule.replace('$flags',f'${i}flags')}", depfile=rule.deps)
writer.newline()
w.separator("Build")
writer.separator("Components")
all(w, registry, target)
all: list[str] = []
for instance in context.enabledInstances():
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 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.outfile(), "ar",
list(map(lambda o: o[1], objects)), implicit=list(map(lambda o: o[1], assets)))
else:
libraries: list[str] = []
for req in instance.resolved:
reqInstance = context.componentByName(req)
if reqInstance is None:
raise RuntimeError(f"Component {req} not found")
if not reqInstance.isLib():
raise RuntimeError(f"Component {req} is not a library")
libraries.append(reqInstance.outfile())
writer.build(instance.outfile(), "ld", list(
map(lambda o: o[1], objects)) + libraries, implicit=list(map(lambda o: o[1], assets)))
all.append(instance.outfile())
writer.newline()
writer.separator("Phony targets")
writer.build("all", "phony", all)
writer.default("all")
@dt.dataclass
class Product:
path: Path
target: model.Target
component: model.Component
def buildMany(componentSpec: list[str], targetSpec: str, props: Props = {}) -> list[ComponentInstance]:
context = contextFor(targetSpec, props)
shell.mkdir(context.builddir())
ninjaPath = os.path.join(context.builddir(), "build.ninja")
with open(ninjaPath, "w") as f:
gen(f, context)
instances = map(lambda i: context.componentByName(i), componentSpec)
for instance in instances:
if not instance.enabled:
raise RuntimeError(
f"Component {componentSpec} is disabled: {instance.disableReason}")
shell.exec(f"ninja", "-v", "-f", ninjaPath,
*map(lambda i: i.outfile(), instances))
return [instance]
def build(
target: model.Target,
registry: model.Registry,
components: Union[list[model.Component], model.Component, None] = None,
) -> list[Product]:
all = False
shell.mkdir(target.builddir)
ninjaPath = os.path.join(target.builddir, "build.ninja")
if not os.path.exists(ninjaPath):
with open(ninjaPath, "w") as f:
gen(f, target, registry)
if components is None:
all = True
components = list(registry.iterEnabled(target))
if isinstance(components, model.Component):
components = [components]
products: list[Product] = []
for c in components:
r = c.resolved[target.id]
if not r.enabled:
raise RuntimeError(f"Component {c.id} is disabled: {r.reason}")
products.append(
Product(
path=Path(outfile(target, c)),
target=target,
component=c,
)
)
outs = list(map(lambda p: str(p.path), products))
shell.exec("ninja", "-f", ninjaPath, *(outs if not all else []))
return products
def build(componentSpec: str, targetSpec: str, props: Props = {}) -> ComponentInstance:
return buildMany([componentSpec], targetSpec, props)[0]
# --- Commands --------------------------------------------------------------- #
class Paths:
bin: str
lib: str
obj: str
def __init__(self, bin: str, lib: str, obj: str):
self.bin = bin
self.lib = lib
self.obj = obj
@cli.command("b", "build", "Build a component or all components")
def buildCmd(args: cli.Args):
registry = model.Registry.use(args)
target = model.Target.use(args)
componentSpec = args.consumeArg()
component = None
if componentSpec is not None:
component = registry.lookup(componentSpec, model.Component)
build(target, registry, component)[0]
def buildAll(targetSpec: str) -> Context:
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)
return context
@cli.command("r", "run", "Run a component")
def runCmd(args: cli.Args):
registry = model.Registry.use(args)
target = model.Target.use(args)
debug = args.consumeOpt("debug", False) is True
def testAll(targetSpec: str):
context = contextFor(targetSpec)
componentSpec = args.consumeArg() or "__main__"
component = registry.lookup(componentSpec, model.Component, includeProvides=True)
if component is None:
raise RuntimeError(f"Component {componentSpec} not found")
shell.mkdir(context.builddir())
ninjaPath = os.path.join(context.builddir(), "build.ninja")
product = build(target, registry, component)[0]
with open(ninjaPath, "w") as f:
gen(f, context)
os.environ["CK_TARGET"] = target.id
os.environ["CK_COMPONENT"] = product.component.id
os.environ["CK_BUILDDIR"] = target.builddir
shell.exec(f"ninja", "-v", "-f", ninjaPath, "all")
shell.exec(*(["lldb", "-o", "run"] if debug else []), str(product.path), *args.args)
for instance in context.enabledInstances():
if instance.isLib():
continue
@cli.command("t", "test", "Run all test targets")
def testCmd(args: cli.Args):
# This is just a wrapper around the `run` command that try
# to run a special hook component named __tests__.
args.args.insert(0, "__tests__")
runCmd(args)
@cli.command("d", "debug", "Debug a component")
def debugCmd(args: cli.Args):
# This is just a wrapper around the `run` command that
# always enable debug mode.
args.opts["debug"] = True
runCmd(args)
@cli.command("c", "clean", "Clean build files")
def cleanCmd(args: cli.Args):
model.Project.use(args)
shell.rmrf(const.BUILD_DIR)
@cli.command("n", "nuke", "Clean all build files and caches")
def nukeCmd(args: cli.Args):
model.Project.use(args)
shell.rmrf(const.PROJECT_CK_DIR)
if instance.id().endswith("-tests"):
print(f"Running {instance.id()}")
shell.exec(instance.outfile())

View file

@ -1,165 +0,0 @@
import inspect
import logging
import sys
import dataclasses as dt
from pathlib import Path
from typing import Optional, Union, Callable
from . import const, vt100
Value = Union[str, bool, int]
_logger = logging.getLogger(__name__)
class Args:
opts: dict[str, Value]
args: list[str]
def __init__(self):
self.opts = {}
self.args = []
def consumePrefix(self, prefix: str) -> dict[str, Value]:
result: dict[str, Value] = {}
copy = self.opts.copy()
for key, value in copy.items():
if key.startswith(prefix):
result[key[len(prefix) :]] = value
del self.opts[key]
return result
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) -> Optional[Value]:
if key in self.opts:
result = self.opts[key]
del self.opts[key]
return result
return None
def consumeArg(self, default: Optional[str] = None) -> Optional[str]:
if len(self.args) == 0:
return default
first = self.args[0]
del self.args[0]
return first
def parse(args: list[str]) -> Args:
result = Args()
for arg in args:
if arg.startswith("--"):
if "=" in arg:
key, value = arg[2:].split("=", 1)
result.opts[key] = value
else:
result.opts[arg[2:]] = True
else:
result.args.append(arg)
return result
Callback = Callable[[Args], None]
@dt.dataclass
class Command:
shortName: Optional[str]
longName: str
helpText: str
isPlugin: bool
callback: Callback
commands: list[Command] = []
def command(shortName: Optional[str], longName: str, helpText: str):
curframe = inspect.currentframe()
calframe = inspect.getouterframes(curframe, 2)
def wrap(fn: Callable[[Args], None]):
_logger.debug(f"Registering command {longName}")
commands.append(
Command(
shortName,
longName,
helpText,
Path(calframe[1].filename).parent != Path(__file__).parent,
fn,
)
)
return fn
return wrap
# --- Builtins Commands ------------------------------------------------------ #
@command("u", "usage", "Show usage information")
def usage(args: Optional[Args] = None):
print(f"Usage: {const.ARGV0} <command> [args...]")
def error(msg: str) -> None:
print(f"{vt100.RED}Error:{vt100.RESET} {msg}\n", file=sys.stderr)
@command("h", "help", "Show this help message")
def helpCmd(args: Args):
usage()
print()
vt100.title("Description")
print(f" {const.DESCRIPTION}")
print()
vt100.title("Commands")
for cmd in sorted(commands, key=lambda c: c.longName):
if cmd.longName.startswith("_"):
continue
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()
vt100.title("Logging")
print(" Logs are stored in:")
print(f" - {const.PROJECT_LOG_FILE}")
print(f" - {const.GLOBAL_LOG_FILE}")
@command("v", "version", "Show current version")
def versionCmd(args: Args):
print(f"CuteKit v{const.VERSION_STR}")
def exec(args: Args):
cmd = args.consumeArg()
if cmd is None:
raise RuntimeError("No command specified")
for c in commands:
if c.shortName == cmd or c.longName == cmd:
c.callback(args)
return
raise RuntimeError(f"Unknown command {cmd}")

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",
)
),
)
)

View file

@ -1,4 +1,3 @@
from pathlib import Path
from typing import Any
@ -6,11 +5,10 @@ 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"
)
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,
@ -22,16 +20,14 @@ UNSUPORTED_MANIFEST = {
}
def ensureSupportedManifest(manifest: Any, path: Path):
if "$schema" not in manifest:
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']]}"
)
f"Unsupported manifest schema {manifest['$schema']} in {path}: {UNSUPORTED_MANIFEST[manifest['$schema']]}")
if manifest["$schema"] not in SUPPORTED_MANIFEST:
if not manifest["$schema"] in SUPPORTED_MANIFEST:
raise RuntimeError(
f"Unsupported manifest schema {manifest['$schema']} in {path}"
)
f"Unsupported manifest schema {manifest['$schema']} in {path}")

View file

@ -1,8 +1,8 @@
import os
import sys
VERSION = (0, 6, 0, "dev")
VERSION_STR = f"{VERSION[0]}.{VERSION[1]}.{VERSION[2]}{'-' + VERSION[3] if len(VERSION) >= 4 else ''}"
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"
@ -11,7 +11,7 @@ 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 = "meta"
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"

303
cutekit/context.py Normal file
View file

@ -0,0 +1,303 @@
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
logger = logging.getLogger(__name__)
class IContext(Protocol):
target: TargetManifest
def builddir(self) -> str:
...
class ComponentInstance:
enabled: bool = True
disableReason = ""
manifest: ComponentManifest
sources: list[str] = []
res: list[str] = []
resolved: list[str] = []
context: IContext
def __init__(
self,
enabled: bool,
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 objdir(self) -> str:
return os.path.join(self.context.builddir(), f"{self.manifest.id}/obj")
def resdir(self) -> str:
return os.path.join(self.context.builddir(), f"{self.manifest.id}/res")
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 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 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 os.path.join(self.context.builddir(), self.manifest.id, f"lib/{self.manifest.id}.a")
else:
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()
elif self.manifest.type == Type.LIB:
return str(Path(self.manifest.dirname()).parent)
else:
return ""
class Context(IContext):
target: TargetManifest
instances: list[ComponentInstance]
tools: Tools
def enabledInstances(self) -> Iterable[ComponentInstance]:
return filter(lambda x: x.enabled, self.instances)
def __init__(self, target: TargetManifest, instances: list[ComponentInstance], tools: Tools):
self.target = target
self.instances = instances
self.tools = tools
def componentByName(self, name: str) -> ComponentInstance:
result = list(filter(lambda x: x.manifest.id == name, self.instances))
if len(result) == 0:
raise RuntimeError(f"Component '{name}' not found")
return result[0]
def cincls(self) -> list[str]:
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, [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]:
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:
path = os.path.join(path, "project.json")
return ProjectManifest(jexpr.evalRead(path), path)
def loadTarget(id: str) -> TargetManifest:
try:
return next(filter(lambda t: t.id == id, loadAllTargets()))
except StopIteration:
raise RuntimeError(f"Target '{id}' not found")
def loadAllComponents() -> list[ComponentManifest]:
files = shell.find(const.SRC_DIR, ["manifest.json"])
files += shell.find(const.EXTERN_DIR, ["manifest.json"])
return list(
map(
lambda path: ComponentManifest(jexpr.evalRead(path), path),
files))
def filterDisabled(components: list[ComponentManifest], target: TargetManifest) -> tuple[list[ComponentManifest], list[ComponentManifest]]:
return list(filter(lambda c: c.isEnabled(target)[0], components)), \
list(filter(lambda c: not c.isEnabled(target)[0], components))
def providerFor(what: str, components: list[ComponentManifest]) -> tuple[Optional[str], str]:
result: list[ComponentManifest] = list(
filter(lambda c: c.id == what, components))
if len(result) == 0:
# Try to find a provider
result = list(filter(lambda x: (what in x.provides), components))
if len(result) == 0:
logger.error(f"No provider for '{what}'")
return (None, f"No provider for '{what}'")
if len(result) > 1:
ids = list(map(lambda x: x.id, result))
logger.error(f"Multiple providers for '{what}': {result}")
return (None, f"Multiple providers for '{what}': {','.join(ids)}")
return (result[0].id, "")
def resolveDeps(componentSpec: str, components: list[ComponentManifest], target: TargetManifest) -> tuple[bool, str, list[str]]:
mapping = dict(map(lambda c: (c.id, c), components))
def resolveInner(what: str, stack: list[str] = []) -> tuple[bool, str, list[str]]:
result: list[str] = []
what = target.route(what)
resolved, unresolvedReason = providerFor(what, components)
if resolved is None:
return False, unresolvedReason, []
if resolved in stack:
raise RuntimeError(f"Dependency loop: {stack} -> {resolved}")
stack.append(resolved)
for req in mapping[resolved].requires:
keep, unresolvedReason, reqs = resolveInner(req, stack)
if not keep:
stack.pop()
logger.error(f"Dependency '{req}' not met for '{resolved}'")
return False, unresolvedReason, []
result.extend(reqs)
stack.pop()
result.insert(0, resolved)
return True, "", result
enabled, unresolvedReason, resolved = resolveInner(componentSpec)
return enabled, unresolvedReason, resolved
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, res, resolved[1:])
def instanciateDisabled(component: ComponentManifest, target: TargetManifest) -> ComponentInstance:
return ComponentInstance(
enabled=False,
disableReason=component.isEnabled(target)[1],
manifest=component,
sources=[],
res=[],
resolved=[])
context: dict[str, Context] = {}
def contextFor(targetSpec: str, props: Props = {}) -> Context:
if targetSpec in context:
return context[targetSpec]
logger.info(f"Loading context for '{targetSpec}'")
targetEls = targetSpec.split(":")
if targetEls[0] == "":
targetEls[0] = "host-" + shell.uname().machine
target = loadTarget(targetEls[0])
target.props |= props
components = loadAllComponents()
components, disabled = filterDisabled(components, target)
tools: Tools = {}
for toolSpec in target.tools:
tool = target.tools[toolSpec]
tools[toolSpec] = Tool(
strict=False,
cmd=tool.cmd,
args=tool.args,
files=tool.files)
tools[toolSpec].args += rules.rules[toolSpec].args
for m in targetEls[1:]:
mixin = mixins.byId(m)
tools = mixin(target, tools)
for component in components:
for toolSpec in component.tools:
tool = component.tools[toolSpec]
tools[toolSpec].args += tool.args
instances: list[ComponentInstance] = list(
map(lambda c: instanciateDisabled(c, target), disabled))
instances += cast(list[ComponentInstance], list(filter(lambda e: e != None, map(lambda c: instanciate(
c.id, components, target), components))))
context[targetSpec] = Context(
target,
instances,
tools,
)
for instance in instances:
instance.context = context[targetSpec]
return context[targetSpec]

View file

@ -1,95 +1,59 @@
import os
from typing import Optional, cast
from . import vt100, cli, model
from typing import Optional
from cutekit.context import Context
from cutekit import vt100
def view(
registry: model.Registry,
target: model.Target,
scope: Optional[str] = None,
showExe: bool = True,
showDisabled: bool = False,
):
from graphviz import Digraph # type: ignore
def view(context: Context, scope: Optional[str] = None, showExe: bool = True, showDisabled: bool = False):
from graphviz import Digraph
g = Digraph(target.id, filename="graph.gv")
g = Digraph(context.target.id, filename='graph.gv')
g.attr("graph", splines="ortho", rankdir="BT", ranksep="1.5")
g.attr("node", shape="ellipse")
g.attr('graph', splines='ortho', rankdir='BT', ranksep='1.5')
g.attr('node', shape='ellipse')
g.attr(
"graph",
label=f"<<B>{scope or 'Full Dependency Graph'}</B><BR/>{target.id}>",
labelloc="t",
)
'graph', label=f"<<B>{scope or 'Full Dependency Graph'}</B><BR/>{context.target.id}>", labelloc='t')
scopeInstance = None
if scope is not None:
scopeInstance = registry.lookup(scope, model.Component)
scopeInstance = context.componentByName(scope)
for component in registry.iterEnabled(target):
if not component.type == model.Kind.LIB and not showExe:
for instance in context.instances:
if not instance.isLib() and not showExe:
continue
if (
scopeInstance is not None
and component.id != scope
and component.id not in scopeInstance.resolved[target.id].resolved
):
if scopeInstance is not None and \
instance.manifest.id != scope and \
instance.manifest.id not in scopeInstance.resolved:
continue
if component.resolved[target.id].enabled:
fillcolor = "lightgrey" if component.type == model.Kind.LIB else "lightblue"
shape = "plaintext" if not scope == component.id else "box"
if instance.enabled:
fillcolor = "lightgrey" if instance.isLib() else "lightblue"
shape = "plaintext" if not scope == instance.manifest.id else 'box'
g.node(
component.id,
f"<<B>{component.id}</B><BR/>{vt100.wordwrap(component.decription, 40,newline='<BR/>')}>",
shape=shape,
style="filled",
fillcolor=fillcolor,
)
g.node(instance.manifest.id, f"<<B>{instance.manifest.id}</B><BR/>{vt100.wordwrap(instance.manifest.decription, 40,newline='<BR/>')}>",
shape=shape, style="filled", fillcolor=fillcolor)
for req in component.requires:
g.edge(component.id, req)
for req in instance.manifest.requires:
g.edge(instance.manifest.id, req)
for req in component.provides:
isChosen = target.routing.get(req, None) == component.id
for req in instance.manifest.provides:
isChosen = context.target.routing.get(
req, None) == instance.manifest.id
g.edge(
req,
component.id,
arrowhead="none",
color=("blue" if isChosen else "black"),
)
g.edge(req, instance.manifest.id, arrowhead="none", color=(
"blue" if isChosen else "black"))
elif showDisabled:
g.node(
component.id,
f"<<B>{component.id}</B><BR/>{vt100.wordwrap(component.decription, 40,newline='<BR/>')}<BR/><BR/><I>{vt100.wordwrap(str(component.resolved[target.id].reason), 40,newline='<BR/>')}</I>>",
shape="plaintext",
style="filled",
fontcolor="#999999",
fillcolor="#eeeeee",
)
g.node(instance.manifest.id, f"<<B>{instance.manifest.id}</B><BR/>{vt100.wordwrap(instance.manifest.decription, 40,newline='<BR/>')}<BR/><BR/><I>{vt100.wordwrap(instance.disableReason, 40,newline='<BR/>')}</I>>",
shape="plaintext", style="filled", fontcolor="#999999", fillcolor="#eeeeee")
for req in component.requires:
g.edge(component.id, req, color="#aaaaaa")
for req in instance.manifest.requires:
g.edge(instance.manifest.id, req, color="#aaaaaa")
for req in component.provides:
g.edge(req, component.id, arrowhead="none", color="#aaaaaa")
for req in instance.manifest.provides:
g.edge(req, instance.manifest.id,
arrowhead="none", color="#aaaaaa")
g.view(filename=os.path.join(target.builddir, "graph.gv"))
@cli.command("g", "graph", "Show the dependency graph")
def graphCmd(args: cli.Args):
registry = model.Registry.use(args)
target = model.Target.use(args)
scope = cast(Optional[str], args.tryConsumeOpt("scope"))
onlyLibs = args.consumeOpt("only-libs", False) is True
showDisabled = args.consumeOpt("show-disabled", False) is True
view(registry, target, scope=scope, showExe=not onlyLibs, showDisabled=showDisabled)
g.view(filename=os.path.join(context.builddir(), "graph.gv"))

View file

@ -1,37 +1,28 @@
import os
from typing import Any, cast, Callable, Final
import json
import re
import tomllib
from pathlib import Path
from typing import Any, Optional, cast, Callable, Final
from . import shell
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(Path(arg)),
"evalRead": lambda arg, ctx: evalRead(Path(arg)),
"join": lambda lhs, rhs, ctx: cast(
Json, {**lhs, **rhs} if isinstance(lhs, dict) else lhs + rhs
),
"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),
"first": lambda arg, ctx: arg[0],
"last": lambda arg, ctx: arg[-1],
"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)
),
"abspath": lambda *args, ctx: os.path.normpath(os.path.join(os.path.dirname(ctx["filepath"]), *args))
}
def eval(jexpr: Json, filePath: Path) -> Json:
def eval(jexpr: Json, filePath: str) -> Json:
if isinstance(jexpr, dict):
result = {}
for k in cast(dict[str, Json], jexpr):
@ -42,9 +33,7 @@ def eval(jexpr: Json, filePath: Path) -> Json:
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}
)
return BUILTINS[funcName](*eval(jexpr[1:], filePath), ctx={"filepath": filePath})
raise RuntimeError(f"Unknown macro {funcName}")
else:
@ -53,28 +42,15 @@ def eval(jexpr: Json, filePath: Path) -> Json:
return jexpr
def extraSchema(toml: str) -> Optional[str]:
schemaRegex = re.compile(r"#:schema\s+(.*)")
schema = schemaRegex.search(toml)
return schema.group(1) if schema else None
def read(path: Path) -> Json:
def read(path: str) -> Json:
try:
with open(path, "r") as f:
if path.suffix == ".toml":
tomlStr = f.read()
toml = tomllib.loads(tomlStr)
schema = extraSchema(tomlStr)
if schema:
toml["$schema"] = schema
return toml
else:
return json.load(f)
except Exception as e:
raise RuntimeError(f"Failed to read {path}: {e}")
return json.load(f)
except:
raise RuntimeError(f"Failed to read {path}")
def evalRead(path: Path) -> Json:
def evalRead(path: str) -> Json:
data = read(path)
ensureSupportedManifest(data, path)
return eval(data, path)

View file

@ -1,29 +1,31 @@
from typing import Callable
from cutekit.model import TargetManifest, Tools
from . import model
Mixin = Callable[[model.Target, model.Tools], model.Tools]
Mixin = Callable[[TargetManifest, Tools], Tools]
def patchToolArgs(tools: model.Tools, toolSpec: str, args: list[str]):
def patchToolArgs(tools: Tools, toolSpec: str, args: list[str]):
tools[toolSpec].args += args
def prefixToolCmd(tools: model.Tools, toolSpec: str, prefix: str):
def prefixToolCmd(tools: Tools, toolSpec: str, prefix: str):
tools[toolSpec].cmd = prefix + " " + tools[toolSpec].cmd
def mixinCache(target: model.Target, tools: model.Tools) -> model.Tools:
def mixinCache(target: TargetManifest, tools: Tools) -> Tools:
prefixToolCmd(tools, "cc", "ccache")
prefixToolCmd(tools, "cxx", "ccache")
return tools
def makeMixinSan(san: str) -> Mixin:
def mixinSan(target: model.Target, tools: model.Tools) -> model.Tools:
patchToolArgs(tools, "cc", [f"-fsanitize={san}"])
patchToolArgs(tools, "cxx", [f"-fsanitize={san}"])
patchToolArgs(tools, "ld", [f"-fsanitize={san}"])
def mixinSan(target: TargetManifest, tools: Tools) -> Tools:
patchToolArgs(
tools, "cc", [f"-fsanitize={san}"])
patchToolArgs(
tools, "cxx", [f"-fsanitize={san}"])
patchToolArgs(
tools, "ld", [f"-fsanitize={san}"])
return tools
@ -31,7 +33,7 @@ def makeMixinSan(san: str) -> Mixin:
def makeMixinOptimize(level: str) -> Mixin:
def mixinOptimize(target: model.Target, tools: model.Tools) -> model.Tools:
def mixinOptimize(target: TargetManifest, tools: Tools) -> Tools:
patchToolArgs(tools, "cc", [f"-O{level}"])
patchToolArgs(tools, "cxx", [f"-O{level}"])
@ -40,7 +42,7 @@ def makeMixinOptimize(level: str) -> Mixin:
return mixinOptimize
def mixinDebug(target: model.Target, tools: model.Tools) -> model.Tools:
def mixinDebug(target: TargetManifest, tools: Tools) -> Tools:
patchToolArgs(tools, "cc", ["-g", "-gdwarf-4"])
patchToolArgs(tools, "cxx", ["-g", "-gdwarf-4"])
@ -48,7 +50,7 @@ def mixinDebug(target: model.Target, tools: model.Tools) -> model.Tools:
def makeMixinTune(tune: str) -> Mixin:
def mixinTune(target: model.Target, tools: model.Tools) -> model.Tools:
def mixinTune(target: TargetManifest, tools: Tools) -> Tools:
patchToolArgs(tools, "cc", [f"-mtune={tune}"])
patchToolArgs(tools, "cxx", [f"-mtune={tune}"])
@ -65,7 +67,6 @@ mixins: dict[str, Mixin] = {
"tsan": makeMixinSan("thread"),
"ubsan": makeMixinSan("undefined"),
"tune": makeMixinTune("native"),
"fast": makeMixinOptimize("fast"),
"o3": makeMixinOptimize("3"),
"o2": makeMixinOptimize("2"),
"o1": makeMixinOptimize("1"),
@ -78,6 +79,4 @@ def append(mixinSpec: str, mixin: Mixin):
def byId(id: str) -> Mixin:
if id not in mixins:
raise RuntimeError(f"Unknown mixin {id}")
return mixins[id]

View file

@ -1,23 +1,17 @@
import os
import logging
import dataclasses as dt
from enum import Enum
from typing import Any, Generator, Optional, Type, cast
from pathlib import Path
from dataclasses_json import DataClassJsonMixin
from typing import Any
import logging
from cutekit import const, shell
from cutekit.jexpr import Json
from . import jexpr, compat, utils, cli, vt100
_logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__)
Props = dict[str, Any]
class Kind(Enum):
class Type(Enum):
UNKNOWN = "unknown"
PROJECT = "project"
TARGET = "target"
@ -25,608 +19,254 @@ class Kind(Enum):
EXE = "exe"
# --- Manifest --------------------------------------------------------------- #
class Manifest:
id: str = ""
type: Type = Type.UNKNOWN
path: str = ""
def __init__(self, json: Json = None, path: str = "", strict: bool = True, **kwargs: Any):
if json is not None:
if not "id" in json:
raise RuntimeError("Missing id")
@dt.dataclass
class Manifest(DataClassJsonMixin):
id: str
type: Kind = dt.field(default=Kind.UNKNOWN)
path: str = dt.field(default="")
SUFFIXES = [".json", ".toml"]
SUFFIXES_GLOBS = ["*.json", "*.toml"]
self.id = json["id"]
@staticmethod
def parse(path: Path, data: dict[str, Any]) -> "Manifest":
"""
Parse a manifest from a given path and data
"""
compat.ensureSupportedManifest(data, path)
kind = Kind(data["type"])
del data["$schema"]
obj = KINDS[kind].from_dict(data)
obj.path = str(path)
return obj
if not "type" in json and strict:
raise RuntimeError("Missing type")
@staticmethod
def tryLoad(path: Path) -> Optional["Manifest"]:
for suffix in Manifest.SUFFIXES:
pathWithSuffix = path.with_suffix(suffix)
if pathWithSuffix.exists():
_logger.debug(f"Loading manifest from '{pathWithSuffix}'")
return Manifest.parse(pathWithSuffix, jexpr.evalRead(pathWithSuffix))
return None
self.type = Type(json["type"])
@staticmethod
def load(path: Path) -> "Manifest":
"""
Load a manifest from a given path
"""
manifest = Manifest.tryLoad(path)
if manifest is None:
raise RuntimeError(f"Could not find manifest at '{path}'")
return manifest
self.path = path
elif strict:
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})"
def __repr__(self):
return f"Manifest({id})"
def dirname(self) -> str:
"""
Return the directory of the manifest
"""
return os.path.relpath(os.path.dirname(self.path), Path.cwd())
def subpath(self, path) -> Path:
return Path(self.dirname()) / path
def ensureType(self, t: Type[utils.T]) -> utils.T:
"""
Ensure that the manifest is of a given type
"""
if not isinstance(self, t):
raise RuntimeError(
f"{self.path} should be a {type.__name__} manifest but is a {self.__class__.__name__} manifest"
)
return cast(utils.T, self)
return os.path.dirname(self.path)
# --- Project ---------------------------------------------------------------- #
class Extern:
git: str = ""
tag: str = ""
_project: Optional["Project"] = None
def __init__(self, json: Json = None, strict: bool = True, **kwargs: Any):
if json is not None:
if not "git" in json and strict:
raise RuntimeError("Missing git")
self.git = json["git"]
if not "tag" in json and strict:
raise RuntimeError("Missing tag")
self.tag = json["tag"]
elif strict:
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})"
def __repr__(self):
return f"Extern({self.git})"
@dt.dataclass
class Extern(DataClassJsonMixin):
git: str
tag: str
class ProjectManifest(Manifest):
description: str = ""
extern: dict[str, Extern] = {}
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 RuntimeError("Missing description")
self.description = json["description"]
self.extern = {k: Extern(v)
for k, v in json.get("extern", {}).items()}
elif strict:
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})"
def __repr__(self):
return f"ProjectManifest({self.id})"
@dt.dataclass
class Project(Manifest):
description: str = dt.field(default="(No description)")
extern: dict[str, Extern] = dt.field(default_factory=dict)
class Tool:
cmd: str = ""
args: list[str] = []
files: list[str] = []
@property
def externDirs(self) -> list[str]:
res = map(lambda e: os.path.join(const.EXTERN_DIR, e), self.extern.keys())
return list(res)
def __init__(self, json: Json = None, strict: bool = True, **kwargs: Any):
if json is not None:
if not "cmd" in json and strict:
raise RuntimeError("Missing cmd")
@staticmethod
def topmost() -> Optional["Project"]:
cwd = Path.cwd()
topmost: Optional["Project"] = None
while str(cwd) != cwd.root:
projectManifest = Manifest.tryLoad(cwd / "project")
if projectManifest is not None:
topmost = projectManifest.ensureType(Project)
cwd = cwd.parent
return topmost
self.cmd = json.get("cmd", self.cmd)
@staticmethod
def ensure() -> "Project":
"""
Ensure that a project exists in the current directory or any parent directory
and chdir to the root of the project.
"""
project = Project.topmost()
if project is None:
raise RuntimeError(
"No project found in this directory or any parent directory"
)
os.chdir(project.dirname())
return project
if not "args" in json and strict:
raise RuntimeError("Missing args")
@staticmethod
def at(path: Path) -> Optional["Project"]:
projectManifest = Manifest.tryLoad(path / "project")
if projectManifest is None:
return None
return projectManifest.ensureType(Project)
self.args = json.get("args", [])
@staticmethod
def fetchs(extern: dict[str, Extern]):
for extSpec, ext in extern.items():
extPath = os.path.join(const.EXTERN_DIR, extSpec)
self.files = json.get("files", [])
elif strict:
raise RuntimeError("Missing json")
if os.path.exists(extPath):
print(f"Skipping {extSpec}, already installed")
continue
for key in kwargs:
setattr(self, key, kwargs[key])
print(f"Installing {extSpec}-{ext.tag} from {ext.git}...")
shell.popen(
"git",
"clone",
"--quiet",
"--depth",
"1",
"--branch",
ext.tag,
ext.git,
extPath,
)
project = Project.at(Path(extPath))
if project is not None:
Project.fetchs(project.extern)
def toJson(self) -> Json:
return {
"cmd": self.cmd,
"args": self.args,
"files": self.files
}
@staticmethod
def use(args: cli.Args) -> "Project":
global _project
if _project is None:
_project = Project.ensure()
return _project
def __str__(self):
return f"Tool(cmd={self.cmd}, args={self.args}, files={self.files})"
@cli.command("i", "install", "Install required external packages")
def installCmd(args: cli.Args):
project = Project.use(args)
Project.fetchs(project.extern)
@cli.command("I", "init", "Initialize a new project")
def initCmd(args: cli.Args):
import requests
repo = args.consumeOpt("repo", const.DEFAULT_REPO_TEMPLATES)
list = args.consumeOpt("list")
template = args.consumeArg()
name = args.consumeArg()
_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:
print(
"\n".join(f"* {entry['id']} - {entry['description']}" for entry in registry)
)
return
if not template:
raise RuntimeError("Template not specified")
def template_match(t: jexpr.Json) -> str:
return t["id"] == template
if not any(filter(template_match, registry)):
raise LookupError(f"Couldn't find a template named {template}")
if not name:
_logger.info(f"No name was provided, defaulting to {template}")
name = template
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}"
)
# --- Target ----------------------------------------------------------------- #
@dt.dataclass
class Tool(DataClassJsonMixin):
cmd: str = dt.field(default="")
args: list[str] = dt.field(default_factory=list)
files: list[str] = dt.field(default_factory=list)
def __repr__(self):
return f"Tool({self.cmd})"
Tools = dict[str, Tool]
DEFAULT_TOOLS: Tools = {
"cp": Tool("cp"),
}
class TargetManifest(Manifest):
props: Props
tools: Tools
routing: dict[str, str]
@dt.dataclass
class Target(Manifest):
props: Props = dt.field(default_factory=dict)
tools: Tools = dt.field(default_factory=dict)
routing: dict[str, str] = dt.field(default_factory=dict)
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 RuntimeError("Missing props")
@property
def hashid(self) -> str:
return utils.hash((self.props, [v.to_dict() for k, v in self.tools.items()]))
self.props = json["props"]
@property
def builddir(self) -> str:
return os.path.join(const.BUILD_DIR, f"{self.id}-{self.hashid[:8]}")
if not "tools" in json and strict:
raise RuntimeError("Missing tools")
@staticmethod
def use(args: cli.Args) -> "Target":
registry = Registry.use(args)
targetSpec = str(args.consumeOpt("target", "host-" + shell.uname().machine))
return registry.ensure(targetSpec, Target)
self.tools = {k: Tool(v) for k, v in json["tools"].items()}
self.routing = json.get("routing", {})
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})"
def route(self, componentSpec: str):
"""
Route a component spec to a target specific component spec
"""
return (
self.routing[componentSpec]
if componentSpec in self.routing
else componentSpec
)
return self.routing[componentSpec] if componentSpec in self.routing else componentSpec
def cdefs(self) -> list[str]:
defines: list[str] = []
for key in self.props:
macroname = key.lower().replace("-", "_")
prop = self.props[key]
macrovalue = str(prop).lower().replace(" ", "_").replace("-", "_")
if isinstance(prop, bool):
if prop:
defines += [f"-D__ck_{macroname}__"]
else:
defines += [f"-D__ck_{macroname}_{macrovalue}__"]
return defines
# --- Component -------------------------------------------------------------- #
class ComponentManifest(Manifest):
decription: str = "(No description)"
props: Props = {}
tools: Tools = {}
enableIf: dict[str, list[Any]] = {}
requires: list[str] = []
provides: list[str] = []
subdirs: list[str] = []
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)
self.tools = {k: Tool(v, strict=False)
for k, v in json.get("tools", {}).items()}
self.enableIf = json.get("enableIf", self.enableIf)
self.requires = json.get("requires", self.requires)
self.provides = json.get("provides", self.provides)
self.subdirs = list(map(lambda x: os.path.join(os.path.dirname(
path), x), json.get("subdirs", [""])))
@dt.dataclass
class Resolved:
reason: Optional[str] = None
resolved: list[str] = dt.field(default_factory=list)
super().__init__(json, path, strict, **kwargs)
@property
def enabled(self) -> bool:
return self.reason is None
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})"
@dt.dataclass
class Component(Manifest):
decription: str = dt.field(default="(No description)")
props: Props = dt.field(default_factory=dict)
tools: Tools = dt.field(default_factory=dict)
enableIf: dict[str, list[Any]] = dt.field(default_factory=dict)
requires: list[str] = dt.field(default_factory=list)
provides: list[str] = dt.field(default_factory=list)
subdirs: list[str] = dt.field(default_factory=list)
injects: list[str] = dt.field(default_factory=list)
resolved: dict[str, Resolved] = dt.field(default_factory=dict)
def isEnabled(self, target: Target) -> tuple[bool, str]:
def isEnabled(self, target: TargetManifest) -> tuple[bool, str]:
for k, v in self.enableIf.items():
if k not in target.props:
_logger.info(f"Component {self.id} disabled by missing {k} in target")
if not k in target.props:
logger.info(
f"Component {self.id} disabled by missing {k} in target")
return False, f"Missing props '{k}' in target"
if target.props[k] not in v:
if not target.props[k] in v:
vStrs = [f"'{str(x)}'" for x in v]
_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)}",
)
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)}"
return True, ""
KINDS: dict[Kind, Type[Manifest]] = {
Kind.PROJECT: Project,
Kind.TARGET: Target,
Kind.LIB: Component,
Kind.EXE: Component,
}
# --- Dependency resolution -------------------------------------------------- #
@dt.dataclass
class Resolver:
_registry: "Registry"
_target: Target
_mappings: dict[str, list[Component]] = dt.field(default_factory=dict)
_cache: dict[str, Resolved] = dt.field(default_factory=dict)
_baked = False
def _bake(self):
"""
Bake the resolver by building a mapping of all
components that provide a given spec.
"""
if self._baked:
return
for c in self._registry.iter(Component):
for p in c.provides + [c.id]:
if p not in self._mappings and [0]:
self._mappings[p] = []
self._mappings[p].append(c)
# Overide with target routing since it has priority
# over component provides and id
for k, v in self._target.routing.items():
component = self._registry.lookup(v, Component)
self._mappings[k] = [component] if component else []
self._baked = True
def _provider(self, spec: str) -> tuple[Optional[str], str]:
"""
Returns the provider for a given spec.
"""
result = self._mappings.get(spec, [])
if len(result) == 1:
enabled, reason = result[0].isEnabled(self._target)
if not enabled:
return (None, reason)
def checkIsEnabled(c: Component) -> bool:
enabled, reason = c.isEnabled(self._target)
if not enabled:
_logger.info(f"Component {c.id} cannot provide '{spec}': {reason}")
return enabled
result = list(filter(checkIsEnabled, result))
if result == []:
return (None, f"No provider for '{spec}'")
if len(result) > 1:
ids = list(map(lambda x: x.id, result))
return (None, f"Multiple providers for '{spec}': {','.join(ids)}")
return (result[0].id, "")
def resolve(self, what: str, stack: list[str] = []) -> Resolved:
"""
Resolve a given spec to a list of components.
"""
self._bake()
if what in self._cache:
return self._cache[what]
keep, unresolvedReason = self._provider(what)
if not keep:
_logger.error(f"Dependency '{what}' not found: {unresolvedReason}")
self._cache[what] = Resolved(reason=unresolvedReason)
return self._cache[what]
if keep in self._cache:
return self._cache[keep]
if keep in stack:
raise RuntimeError(
f"Dependency loop while resolving '{what}': {stack} -> {keep}"
)
stack.append(keep)
component = self._registry.lookup(keep, Component)
if not component:
return Resolved(reason="No provider for 'myembed'")
result: list[str] = []
for req in component.requires:
reqResolved = self.resolve(req, stack)
if reqResolved.reason:
stack.pop()
self._cache[keep] = Resolved(reason=reqResolved.reason)
return self._cache[keep]
result.extend(reqResolved.resolved)
stack.pop()
result.insert(0, keep)
self._cache[keep] = Resolved(resolved=utils.uniq(result))
return self._cache[keep]
# --- Registry --------------------------------------------------------------- #
_registry: Optional["Registry"] = None
@dt.dataclass
class Registry(DataClassJsonMixin):
project: Project
manifests: dict[str, Manifest] = dt.field(default_factory=dict)
def _append(self, m: Optional[Manifest]) -> Optional[Manifest]:
"""
Append a manifest to the model
"""
if m is None:
return m
if m.id in self.manifests:
raise RuntimeError(
f"Duplicated manifest '{m.id}' at '{m.path}' already loaded from '{self.manifests[m.id].path}'"
)
self.manifests[m.id] = m
return m
def iter(self, type: Type[utils.T]) -> Generator[utils.T, None, None]:
"""
Iterate over all manifests of a given type
"""
for m in self.manifests.values():
if isinstance(m, type):
yield m
def iterEnabled(self, target: Target) -> Generator[Component, None, None]:
for c in self.iter(Component):
resolve = c.resolved[target.id]
if resolve.enabled:
yield c
def lookup(
self, name: str, type: Type[utils.T], includeProvides: bool = False
) -> Optional[utils.T]:
"""
Lookup a manifest of a given type by name
"""
if name in self.manifests:
m = self.manifests[name]
if isinstance(m, type):
return m
if includeProvides and type is Component:
for m in self.iter(Component):
if name in m.provides:
return m # type: ignore
return None
def ensure(self, name: str, type: Type[utils.T]) -> utils.T:
"""
Ensure that a manifest of a given type exists
and return it.
"""
m = self.lookup(name, type)
if not m:
raise RuntimeError(f"Could not find {type.__name__} '{name}'")
return m
@staticmethod
def use(args: cli.Args) -> "Registry":
global _registry
if _registry is not None:
return _registry
project = Project.use(args)
mixins = str(args.consumeOpt("mixins", "")).split(",")
if mixins == [""]:
mixins = []
props = cast(dict[str, str], args.consumePrefix("prop:"))
_registry = Registry.load(project, mixins, props)
return _registry
@staticmethod
def load(project: Project, mixins: list[str], props: Props) -> "Registry":
r = Registry(project)
r._append(project)
# Lookup and load all extern projects
for externDir in project.externDirs:
extern = r._append(
Manifest.tryLoad(Path(externDir) / "project")
or Manifest.tryLoad(Path(externDir) / "manifest")
)
if extern is not None:
_logger.warn("Extern project does not have a project or manifest")
# Load all manifests from projects
for project in list(r.iter(Project)):
targetDir = os.path.join(project.dirname(), const.TARGETS_DIR)
targetFiles = shell.find(targetDir, Manifest.SUFFIXES_GLOBS)
for targetFile in targetFiles:
r._append(Manifest.load(Path(targetFile)).ensureType(Target))
componentFiles = shell.find(
os.path.join(project.dirname(), const.SRC_DIR),
["manifest" + s for s in Manifest.SUFFIXES],
)
rootComponent = Manifest.tryLoad(Path(project.dirname()) / "manifest")
if rootComponent is not None:
r._append(rootComponent)
for componentFile in componentFiles:
r._append(Manifest.load(Path(componentFile)).ensureType(Component))
# Resolve all dependencies for all targets
for target in r.iter(Target):
target.props |= props
resolver = Resolver(r, target)
# Apply injects
for c in r.iter(Component):
if c.isEnabled(target)[0]:
for inject in c.injects:
victim = r.lookup(inject, Component)
if not victim:
raise RuntimeError(f"Cannot find component '{inject}'")
victim.requires += [c.id]
# Resolve all components
for c in r.iter(Component):
resolved = resolver.resolve(c.id)
if resolved.reason:
_logger.info(f"Component '{c.id}' disabled: {resolved.reason}")
c.resolved[target.id] = resolved
# Resolve tooling
tools: Tools = target.tools
# Merge in default tools
for k, v in DEFAULT_TOOLS.items():
if k not in tools:
tools[k] = dt.replace(v)
from . import mixins as mxs
for mix in mixins:
mixin = mxs.byId(mix)
tools = mixin(target, tools)
# Apply tooling from components
for c in r.iter(Component):
if c.resolved[target.id].enabled:
for k, v in c.tools.items():
tools[k].args += v.args
return r
@cli.command("l", "list", "List all components and targets")
def listCmd(args: cli.Args):
registry = Registry.use(args)
components = list(registry.iter(Component))
targets = list(registry.iter(Target))
vt100.title("Components")
if len(components) == 0:
print(vt100.p("(No components available)"))
else:
print(vt100.p(", ".join(map(lambda m: m.id, components))))
print()
vt100.title("Targets")
if len(targets) == 0:
print(vt100.p("(No targets available)"))
else:
print(vt100.p(", ".join(map(lambda m: m.id, targets))))
print()

View file

@ -23,13 +23,13 @@ use Python.
"""
import textwrap
from typing import TextIO, Union
from . import utils
from cutekit.utils import asList
def escapePath(word: str) -> str:
return word.replace("$ ", "$$ ").replace(" ", "$ ").replace(":", "$:")
return word.replace('$ ', '$$ ').replace(' ', '$ ').replace(':', '$:')
VarValue = Union[int, str, list[str], None]
@ -42,98 +42,92 @@ class Writer(object):
self.width = width
def newline(self) -> None:
self.output.write("\n")
self.output.write('\n')
def comment(self, text: str) -> None:
for line in textwrap.wrap(
text, self.width - 2, break_long_words=False, break_on_hyphens=False
):
self.output.write("# " + line + "\n")
for line in textwrap.wrap(text, self.width - 2, break_long_words=False,
break_on_hyphens=False):
self.output.write('# ' + line + '\n')
def separator(self, text: str) -> None:
self.output.write(
f"# --- {text} ---" + "-" * (self.width - 10 - len(text)) + " #\n\n"
)
def separator(self, text : str) -> None:
self.output.write(f"# --- {text} ---" + '-' *
(self.width - 10 - len(text)) + " #\n\n")
def variable(self, key: str, value: VarValue, indent: int = 0) -> None:
if value is None:
return
if isinstance(value, list):
value = " ".join(filter(None, value)) # Filter out empty strings.
self._line("%s = %s" % (key, value), indent)
value = ' '.join(filter(None, value)) # Filter out empty strings.
self._line('%s = %s' % (key, value), indent)
def pool(self, name: str, depth: int) -> None:
self._line("pool %s" % name)
self.variable("depth", depth, indent=1)
self._line('pool %s' % name)
self.variable('depth', depth, indent=1)
def rule(
self,
name: str,
command: VarValue,
description: Union[str, None] = None,
depfile: VarValue = None,
generator: VarValue = False,
pool: VarValue = None,
restat: bool = False,
rspfile: VarValue = None,
rspfile_content: VarValue = None,
deps: VarValue = None,
) -> None:
self._line("rule %s" % name)
self.variable("command", command, indent=1)
def rule(self,
name: str,
command: VarValue,
description: Union[str, None] = None,
depfile: VarValue = None,
generator: VarValue = False,
pool: VarValue = None,
restat: bool = False,
rspfile: VarValue = None,
rspfile_content: VarValue = None,
deps: VarValue = None) -> None:
self._line('rule %s' % name)
self.variable('command', command, indent=1)
if description:
self.variable("description", description, indent=1)
self.variable('description', description, indent=1)
if depfile:
self.variable("depfile", depfile, indent=1)
self.variable('depfile', depfile, indent=1)
if generator:
self.variable("generator", "1", indent=1)
self.variable('generator', '1', indent=1)
if pool:
self.variable("pool", pool, indent=1)
self.variable('pool', pool, indent=1)
if restat:
self.variable("restat", "1", indent=1)
self.variable('restat', '1', indent=1)
if rspfile:
self.variable("rspfile", rspfile, indent=1)
self.variable('rspfile', rspfile, indent=1)
if rspfile_content:
self.variable("rspfile_content", rspfile_content, indent=1)
self.variable('rspfile_content', rspfile_content, indent=1)
if deps:
self.variable("deps", deps, indent=1)
self.variable('deps', deps, indent=1)
def build(
self,
outputs: Union[str, list[str]],
rule: str,
inputs: Union[VarPath, None],
implicit: VarPath = None,
order_only: VarPath = None,
variables: Union[dict[str, str], None] = None,
implicit_outputs: VarPath = None,
pool: Union[str, None] = None,
dyndep: Union[str, None] = None,
) -> list[str]:
outputs = utils.asList(outputs)
def build(self,
outputs: Union[str, list[str]],
rule: str,
inputs: Union[VarPath, None],
implicit: VarPath = None,
order_only: VarPath = None,
variables: Union[dict[str, str], None] = None,
implicit_outputs: VarPath = None,
pool: Union[str, None] = None,
dyndep: Union[str, None] = None) -> list[str]:
outputs = asList(outputs)
out_outputs = [escapePath(x) for x in outputs]
all_inputs = [escapePath(x) for x in utils.asList(inputs)]
all_inputs = [escapePath(x) for x in asList(inputs)]
if implicit:
implicit = [escapePath(x) for x in utils.asList(implicit)]
all_inputs.append("|")
implicit = [escapePath(x) for x in asList(implicit)]
all_inputs.append('|')
all_inputs.extend(implicit)
if order_only:
order_only = [escapePath(x) for x in utils.asList(order_only)]
all_inputs.append("||")
order_only = [escapePath(x) for x in asList(order_only)]
all_inputs.append('||')
all_inputs.extend(order_only)
if implicit_outputs:
implicit_outputs = [escapePath(x) for x in utils.asList(implicit_outputs)]
out_outputs.append("|")
implicit_outputs = [escapePath(x)
for x in asList(implicit_outputs)]
out_outputs.append('|')
out_outputs.extend(implicit_outputs)
self._line(
"build %s: %s" % (" ".join(out_outputs), " ".join([rule] + all_inputs))
)
self._line('build %s: %s' % (' '.join(out_outputs),
' '.join([rule] + all_inputs)))
if pool is not None:
self._line(" pool = %s" % pool)
self._line(' pool = %s' % pool)
if dyndep is not None:
self._line(" dyndep = %s" % dyndep)
self._line(' dyndep = %s' % dyndep)
if variables:
iterator = iter(variables.items())
@ -144,59 +138,58 @@ class Writer(object):
return outputs
def include(self, path: str) -> None:
self._line("include %s" % path)
self._line('include %s' % path)
def subninja(self, path: str) -> None:
self._line("subninja %s" % path)
self._line('subninja %s' % path)
def default(self, paths: VarPath) -> None:
self._line("default %s" % " ".join(utils.asList(paths)))
self._line('default %s' % ' '.join(asList(paths)))
def _count_dollars_before_index(self, s: str, i: int) -> int:
"""Returns the number of '$' characters right in front of s[i]."""
dollar_count = 0
dollar_index = i - 1
while dollar_index > 0 and s[dollar_index] == "$":
while dollar_index > 0 and s[dollar_index] == '$':
dollar_count += 1
dollar_index -= 1
return dollar_count
def _line(self, text: str, indent: int = 0) -> None:
"""Write 'text' word-wrapped at self.width characters."""
leading_space = " " * indent
leading_space = ' ' * indent
while len(leading_space) + len(text) > self.width:
# The text is too wide; wrap if possible.
# Find the rightmost space that would obey our width constraint and
# that's not an escaped space.
available_space = self.width - len(leading_space) - len(" $")
available_space = self.width - len(leading_space) - len(' $')
space = available_space
while True:
space = text.rfind(" ", 0, space)
if space < 0 or self._count_dollars_before_index(text, space) % 2 == 0:
space = text.rfind(' ', 0, space)
if (space < 0 or
self._count_dollars_before_index(text, space) % 2 == 0):
break
if space < 0:
# No such space; just use the first unescaped space we can find.
space = available_space - 1
while True:
space = text.find(" ", space + 1)
if (
space < 0
or self._count_dollars_before_index(text, space) % 2 == 0
):
space = text.find(' ', space + 1)
if (space < 0 or
self._count_dollars_before_index(text, space) % 2 == 0):
break
if space < 0:
# Give up on breaking.
break
self.output.write(leading_space + text[0:space] + " $\n")
text = text[space + 1 :]
self.output.write(leading_space + text[0:space] + ' $\n')
text = text[space+1:]
# Subsequent lines are continuations, so indent them.
leading_space = " " * (indent + 2)
leading_space = ' ' * (indent+2)
self.output.write(leading_space + text + "\n")
self.output.write(leading_space + text + '\n')
def close(self) -> None:
self.output.close()
@ -205,6 +198,6 @@ class Writer(object):
def escape(string: str) -> str:
"""Escape a string such that it can be embedded into a Ninja file without
further interpretation."""
assert "\n" not in string, "Ninja syntax does not allow newlines"
assert '\n' not in string, 'Ninja syntax does not allow newlines'
# We only have one special metacharacter: '$'.
return string.replace("$", "$$")
return string.replace('$', '$$')

View file

@ -1,19 +1,18 @@
import os
import logging
from . import shell, model, const
from cutekit import shell, project, const, context
import importlib.util as importlib
_logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__)
def load(path: str):
_logger.info(f"Loading plugin {path}")
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}")
logger.error(f"Failed to load plugin {path}")
return None
module = importlib.module_from_spec(spec)
@ -21,23 +20,25 @@ def load(path: str):
def loadAll():
_logger.info("Loading plugins...")
logger.info("Loading plugins...")
project = model.Project.topmost()
if project is None:
_logger.info("Not in project, skipping plugin loading")
projectRoot = project.root()
if projectRoot is None:
logger.info("Not in project, skipping plugin loading")
return
paths = list(
map(lambda e: os.path.join(const.EXTERN_DIR, e), project.extern.keys())
) + ["."]
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(project.dirname(), dirname, const.META_DIR, "plugins")
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}")
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

View file

@ -9,15 +9,7 @@ class Rule:
args: list[str]
deps: Optional[str] = None
def __init__(
self,
id: str,
fileIn: list[str],
fileOut: list[str],
rule: str,
args: list[str] = [],
deps: Optional[str] = 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
@ -27,23 +19,16 @@ class Rule:
rules: dict[str, Rule] = {
"cp": Rule("cp", ["*"], ["*"], "$in $out"),
"cc": Rule(
"cc",
["*.c"],
["*.o"],
"-c -o $out $in -MD -MF $out.d $flags $cincs $cdefs",
["-std=gnu2x", "-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",
),
"cc": Rule("cc", ["*.c"], ["*.o"], "-c -o $out $in -MD -MF $out.d $flags $cincs $cdefs", ["-std=gnu2x",
"-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"),
"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"),

View file

@ -13,15 +13,13 @@ import tempfile
from typing import Optional
from . import const
from cutekit import const
_logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__)
class Uname:
def __init__(
self, sysname: str, nodename: str, release: str, version: str, machine: str
):
def __init__(self, sysname: str, nodename: str, release: str, version: str, machine: str):
self.sysname = sysname
self.nodename = nodename
self.release = release
@ -49,10 +47,8 @@ def sha256sum(path: str) -> str:
return hashlib.sha256(f.read()).hexdigest()
def find(
path: str | list[str], wildcards: list[str] = [], recusive: bool = True
) -> list[str]:
_logger.info(f"Looking for files in {path} matching {wildcards}")
def find(path: str | list[str], wildcards: list[str] = [], recusive: bool = True) -> list[str]:
logger.info(f"Looking for files in {path} matching {wildcards}")
result: list[str] = []
@ -88,7 +84,7 @@ def find(
def mkdir(path: str) -> str:
_logger.info(f"Creating directory {path}")
logger.info(f"Creating directory {path}")
try:
os.makedirs(path)
@ -99,7 +95,7 @@ def mkdir(path: str) -> str:
def rmrf(path: str) -> bool:
_logger.info(f"Removing directory {path}")
logger.info(f"Removing directory {path}")
if not os.path.exists(path):
return False
@ -112,18 +108,18 @@ def wget(url: str, path: Optional[str] = None) -> str:
if path is None:
path = os.path.join(
const.CACHE_DIR, hashlib.sha256(url.encode("utf-8")).hexdigest()
)
const.CACHE_DIR,
hashlib.sha256(url.encode('utf-8')).hexdigest())
if os.path.exists(path):
return path
_logger.info(f"Downloading {url} to {path}")
logger.info(f"Downloading {url} to {path}")
r = requests.get(url, stream=True)
r.raise_for_status()
mkdir(os.path.dirname(path))
with open(path, "wb") as f:
with open(path, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
@ -132,20 +128,17 @@ def wget(url: str, path: Optional[str] = None) -> str:
def exec(*args: str, quiet: bool = False) -> bool:
_logger.info(f"Executing {args}")
logger.info(f"Executing {args}")
try:
proc = subprocess.run(
args,
stdout=sys.stdout if not quiet else subprocess.PIPE,
stderr=sys.stderr if not quiet else subprocess.PIPE,
)
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"))
logger.info(proc.stdout.decode('utf-8'))
if proc.stderr:
_logger.error(proc.stderr.decode("utf-8"))
logger.error(proc.stderr.decode('utf-8'))
except FileNotFoundError:
raise RuntimeError(f"{args[0]}: Command not found")
@ -157,13 +150,14 @@ def exec(*args: str, quiet: bool = False) -> bool:
raise RuntimeError(f"{args[0]}: Segmentation fault")
if proc.returncode != 0:
raise RuntimeError(f"{args[0]}: Process exited with code {proc.returncode}")
raise RuntimeError(
f"{args[0]}: Process exited with code {proc.returncode}")
return True
def popen(*args: str) -> str:
_logger.info(f"Executing {args}")
logger.info(f"Executing {args}")
try:
proc = subprocess.run(args, stdout=subprocess.PIPE, stderr=sys.stderr)
@ -174,13 +168,14 @@ def popen(*args: str) -> str:
raise RuntimeError(f"{args[0]}: Segmentation fault")
if proc.returncode != 0:
raise RuntimeError(f"{args[0]}: Process exited with code {proc.returncode}")
raise RuntimeError(
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]:
_logger.info(f"Reading directory {path}")
logger.info(f"Reading directory {path}")
try:
return os.listdir(path)
@ -189,19 +184,19 @@ def readdir(path: str) -> list[str]:
def cp(src: str, dst: str):
_logger.info(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}")
logger.info(f"Moving {src} to {dst}")
shutil.move(src, dst)
def cpTree(src: str, dst: str):
_logger.info(f"Copying {src} to {dst}")
logger.info(f"Copying {src} to {dst}")
shutil.copytree(src, dst, dirs_exist_ok=True)
@ -209,14 +204,10 @@ def cpTree(src: str, dst: str):
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", "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)
@ -241,14 +232,9 @@ def latest(cmd: str) -> str:
if cmd in LATEST_CACHE:
return LATEST_CACHE[cmd]
_logger.info(f"Finding latest version of {cmd}")
logger.info(f"Finding latest version of {cmd}")
regex: re.Pattern[str]
if platform.system() == "Windows":
regex = re.compile(r"^" + re.escape(cmd) + r"(-.[0-9]+)?(\.exe)?$")
else:
regex = re.compile(r"^" + re.escape(cmd) + r"(-[0-9]+)?$")
regex = re.compile(r"^" + re.escape(cmd) + r"(-.[0-9]+)?(\.exe)?$")
versions: list[str] = []
for path in os.environ["PATH"].split(os.pathsep):
@ -263,15 +249,8 @@ def latest(cmd: str) -> str:
versions.sort()
chosen = versions[-1]
_logger.info(f"Chosen {chosen} as latest version of {cmd}")
logger.info(f"Chosen {chosen} as latest version of {cmd}")
LATEST_CACHE[cmd] = chosen
return chosen
def which(cmd: str) -> Optional[str]:
"""
Find the path of a command
"""
return shutil.which(cmd)

View file

@ -3,21 +3,19 @@ from typing import Any, TypeVar, cast, Optional, Union
import json
import hashlib
T = TypeVar("T")
T = TypeVar('T')
def uniq(lst: list[T]) -> list[T]:
result: list[T] = []
for i in lst:
def uniq(l: list[str]) -> list[str]:
result: list[str] = []
for i in l:
if i in result:
result.remove(i)
result.append(i)
return result
def hash(
obj: Any, keys: list[str] = [], cls: Optional[type[json.JSONEncoder]] = None
) -> str:
def hash(obj: Any, keys: list[str] = [], cls: Optional[type[json.JSONEncoder]] = None) -> str:
toHash = {}
if len(keys) == 0:
toHash = obj
@ -30,7 +28,7 @@ def hash(
def camelCase(s: str) -> str:
s = "".join(x for x in s.title() if x != "_" and x != "-")
s = ''.join(x for x in s.title() if x != '_' and x != '-')
s = s[0].lower() + s[1:]
return s
@ -62,6 +60,4 @@ def asList(i: Optional[Union[T, list[T]]]) -> list[T]:
def isNewer(path1: str, path2: str) -> bool:
return not os.path.exists(path2) or os.path.getmtime(path1) > os.path.getmtime(
path2
)
return not os.path.exists(path2) or os.path.getmtime(path1) > os.path.getmtime(path2)

View file

@ -6,7 +6,6 @@ BLUE = "\033[34m"
PURPLE = "\033[35m"
CYAN = "\033[36m"
WHITE = "\033[37m"
YELLOW = "\033[33m"
BRIGHT_BLACK = "\033[90m"
@ -28,6 +27,10 @@ CROSSED = "\033[9m"
RESET = "\033[0m"
def title(text: str):
print(f"{BOLD}{text}{RESET}:")
def wordwrap(text: str, width: int = 60, newline: str = "\n") -> str:
result = ""
curr = 0
@ -45,11 +48,3 @@ def wordwrap(text: str, width: int = 60, newline: str = "\n") -> str:
def indent(text: str, indent: int = 4) -> str:
return " " * indent + text.replace("\n", "\n" + " " * indent)
def title(text: str):
print(f"{BOLD}{text}{RESET}:")
def p(text: str):
return indent(wordwrap(text))

View file

@ -1,114 +0,0 @@
# Cutekit
Cutekit is a build system that aims to be simple, fast and easy to use.
A project is described using json files.
## Project file
The project file is used to describe the project and its dependencies.
See: [doc/spec/project.md](doc/spec/project.md) for the full specification.
Example:
> project.json
```json
{
"$schema": "https://schemas.cute.engineering/stable/cutekit.manifest.project.v1",
"id": "skift-org/skift",
"type": "project",
"description": "The Skift Operating System",
"extern": {
"cute-engineering/libheap": {
"git": "https://github.com/cute-engineering/libheap.git",
"tag": "v1.1.0"
}
}
}
```
Here we describe a project with the id `skift-org/skift` and a dependency to `cute-engineering/libheap` at version `v1.1.0`.
## An executable package manifest
When you want to create an executable package, you need to create a `manifest.json` file in any directory under `src/`.
This is the file that describe an executable with its dependencies.
> src/nyan-cat-app/manifest.json
```json
{
"$schema": "https://schemas.cute.engineering/stable/cutekit.manifest.component.v1",
"id": "nyan-cat-app",
"type": "exe",
"description": "rainbows everywhere",
"requires": [
"easy-lib"
]
}
```
Here we describe an executable with the id `nyan-cat-app` and a dependency to `easy-lib` (which is a library built by the project).
You can run the executable by running `$ ck run nyan-cat-app`.
## A library package manifest
When you want to create a library package, you need to create a `manifest.json` file in any directory under `src/`, like an executable package.
> src/easy-lib/manifest.json
```json
{
"$schema": "https://schemas.cute.engineering/stable/cutekit.manifest.component.v1",
"id": "easy-lib",
"type": "lib",
"description": "easy to use library",
"requires": [
"cute-engineering/libheap"
]
}
```
Here we describe a library with the id `easy-lib` and a dependency to `cute-engineering/libheap` (which is an external dependency described above in the `project.json`).
## Using installed libraries
You can create a specific installed library through the use of `pkg-config` files.
For example here is how you add `SDL2` to your project:
> src/extern/sdl2/manifest.json
```json
{
"$schema": "https://schemas.cute.engineering/stable/cutekit.manifest.component.v1",
"id": "sdl2",
"type": "lib",
"description": "A cross-platform development library designed to provide low level access to hardware",
"tools": {
"cc": {
"args": [
"@exec",
"pkg-config",
"--cflags",
"sdl2"
]
},
"cxx": {
"args": [
"@exec",
"pkg-config",
"--cflags",
"sdl2"
]
},
"ld": {
"args": [
"@exec",
"pkg-config",
"--libs",
"sdl2"
]
}
}
}
```

View file

@ -1,18 +0,0 @@
# Extending cutekit
By writing custom Python plugins, you can extend Cutekit to do whatever you want.
First the file need to be located in `meta/plugins` and have the `.py` extension.
Then you can import cutekit and change/add whatever you want.
For example you can add a new command to the CLI:
```python
from cutekit import cli
@cli.command("h", "hello", "Print hello world")
def bootCmd(args: cli.Args) -> None:
print("Hello world!")
```
This feature is used - for example - by [SkiftOS](https://github.com/skift-org/skift/blob/main/meta/plugins/start-cmd.py) to add the `start` command, that build packages and run a virtual machine.

View file

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="93.217mm" height="69.497mm" version="1.1" viewBox="0 0 93.217 69.497" xmlns="http://www.w3.org/2000/svg"
xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<metadata>
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g transform="translate(0 19.497)">
<rect y="-19.497" width="93.217" height="69.497" fill="#a00" />
<rect x="5.6831" y="-14.441" width="8.8" height="40" ry="0" fill="#fff" />
<rect x="5.6332" y="21.559" width="4.45" height="4.05" fill="#a00" />
<rect x="10.083" y="17.559" width="4.45" height="4" fill="#a00" />
<rect x="5.6332" y="13.559" width="4.45" height="4" fill="#a00" />
<rect x="10.083" y="9.5594" width="4.45" height="4" fill="#a00" />
<rect x="5.6834" y="-14.441" width="35.2" height="8.8" fill="#fff" />
<rect x="32.083" y="-14.441" width="8.8" height="40" ry="0" fill="#fff" />
<rect x="36.483" y="21.559" width="4.45" height="4.05" fill="#a00" />
<rect x="32.033" y="17.559" width="4.45" height="4" fill="#a00" />
<rect x="36.483" y="13.559" width="4.45" height="4" fill="#a00" />
<rect x="32.033" y="9.5594" width="4.45" height="4" fill="#a00" />
<rect x="20.083" y="-14.441" width="6.6202" height="40" ry="0" fill="#fff" />
<rect x="46.083" y="-14.441" width="8.8" height="40" ry="0" fill="#fff" />
<rect x="70.083" y="-14.441" width="8.8" height="40" ry="0" fill="#fff" />
<rect x="60.283" y="-14.441" width="28.4" height="8.8" fill="#fff" />
<rect x="70.033" y="21.559" width="4.45" height="4.05" fill="#a00" />
<rect x="74.483" y="17.559" width="4.45" height="4" fill="#a00" />
<rect x="70.033" y="13.559" width="4.45" height="4" fill="#a00" />
<rect x="74.483" y="9.5594" width="4.45" height="4" fill="#a00" />
<text x="12.171427" y="44.462078" fill="#ffffff" font-family="'Liberation Sans'" font-size="11.289px"
letter-spacing="0px" stroke-width="1px" word-spacing="0px" style="line-height:125%" xml:space="preserve">
<tspan x="12.171427" y="44.462078" font-family="'Liberation Sans'" font-size="19.756px">License</tspan>
</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -1,92 +0,0 @@
## Macros
To avoid headache, Cutekit extends JSON through simple macros, this is what we call **Jexpr**.
### `@latest`
Find the latest version of a command in the path.
```json
"cc": {
"cmd": ["@latest", "clang"], // clang-14
/* ... */
```
### `@uname`
Query the system for information about the current operating system.
```json
"cc": {
"cmd": ["@uname", "machine"], // "x86_64"
/* ... */
```
The `@uname` commands has 1 argument that may be:
- `node`: to get the current machine hostname.
- `machine`: to get the current machine running architecture
- `aarch64` is renamed to `arm64`
- `AMD64` is renamed to `x86_64`
- `system`: to get the current machine running operating system:
- `Linux`
- `Windows`
- `release`: to get the current machine operating system's version
- `version`: to get more information about the host operating system.
### `@include`
Include a manifest file.
### `@read`
Read a Json file and output its value.
### `@join`
Join two objects
Example:
```json
["@join", {"a": 1}, {"b": 2}] // {"a": 1, "b": 2}
```
### `@concat`
Concatenate strings
Example:
```json
["@concat", "a", "b", "c"] // "abc"
```
### `@exec`
Execute a command and return the output
Example:
```json
["@exec", "uname", "-m"] // "x86_64"
```
### `@eval`
Execute python code and return the output
Example:
```json
"32limit": ["@eval", "2**32"]
```
### `@abspath`
Returns the absolute path of a path.

View file

@ -1,102 +0,0 @@
## Manifest file format
### `id`
The `id` of the package. This is used to identify the package in the manifest file.
Example:
```json
{
"id": "hello"
}
```
### `type`
The type of the package. This is used to identify the package in the manifest file.
Example:
```json
{
"type": "exe"
}
```
**Values:**
- `"exe"`
- `"lib"`
### `description`
The description of the package for the user.
Example:
```json
{
"description": "Hello world"
}
```
### `enabledIf`
A list of requirements for the package check against the build props. If the requirement is not met, the package will be disabled.
```json
{
"enabledIf": {
"freestanding": [
false
]
}
}
```
**Values:**
`enableIf` is a map of variable and values:
```
"variable-name": [array of expected value]
```
If `variable-name` is equal to one of the value in the table, then the package will be enabled.
### `requires`
Dependencies of the package. The name listed here must be the same as the `id` of the package or member of a provide list.
Example:
```json
{
"requires": [
"libc",
"libm"
]
}
```
### `provides`
An alias for the package.
Example:
```json
{
"provides": [
"hello"
]
}
```
This alias may be used by other package when using `requires`.
This is used when you have multiple package implementing the same features, but only one is enabled through `enableIf`.
**Value**:
- An array of `id`.

View file

@ -1,33 +0,0 @@
## project.json
The project file is the main file of the project.
It describes the project and its dependencies.
### `id`
The `id` of the project. This is used to identify the project.
### `type`
Should be `project`.
### `description`
The description of the project for the user.
### `extern`
A list of external dependencies for the project, for example:
```json
"externs": {
"cute-engineering/libheap": {
"git": "https://github.com/cute-engineering/libheap.git",
"tag": "v1.1.0"
}
}
```
You describe the project `id`, the `git` repository and the `tag` to use.

View file

@ -1,119 +0,0 @@
## Target file format
A target file is used for describing a new cross-compiler to use.
Generally, a target file is named like this: `{target}-{arch}.json`. For example, you could have:
- `host-x86-64`
- `windows-arm`
- ...
### `id`
The `id` of the target. This is used to identify the target in the target file.
### `type`
Should be `target`.
### `props`
A list of properties for the target.
Example:
```json
{
"props": {
"arch": "x86_64",
"vendor": "pc",
"os": "linux",
"env": "gnu",
"abi": "elf",
"cpu": "x86_64",
"features": "fxsr,sse,sse2"
}
}
```
Theses may be accessed during compilation as C-Macros: `__ck_{prop}__`.
### `tools`
A list of tools for the target.
Each tool is described like this:
```json
"name": {
"cmd": "",
"args": [
...
]
}
```
Where:
- `cmd` describe the command to run
- `arg` describes each argument to use (note: each entry of the table correspond to ONE argument).
You have different tools names that you can use:
- "cc"
- "cxx"
- "ld"
- "ar"
- "as"
**Example**:
```json
{
"tools": {
"cc": {
"cmd": ["@latest", "clang"],
"args": [
"-target",
"x86_64-unknown-windows",
"-ffreestanding",
"-fno-stack-protector",
"-fshort-wchar",
"-mno-red-zone"
]
},
"cxx": {
"cmd": ["@latest", "clang++"],
"args": [
"-target",
"x86_64-unknown-windows",
"-ffreestanding",
"-fno-stack-protector",
"-fshort-wchar",
"-mno-red-zone"
]
},
"ld": {
"cmd": ["@latest", "clang++"],
"args": [
"-target",
"x86_64-unknown-windows",
"-nostdlib",
"-Wl,-entry:efi_main",
"-Wl,-subsystem:efi_application",
"-fuse-ld=lld-link"
]
},
"ar": {
"cmd": ["@latest", "llvm-ar"],
"args": [
"rcs"
]
},
"as": {
"cmd": "nasm",
"args": [
"-f",
"win64"
]
}
}
}
```

View file

@ -1,40 +0,0 @@
# Templates
Templates are based on this [repository (cute-engineering/cutekit-templates)](https://github.com/cute-engineering/cutekit-templates).
Each directory correspond to a template.
You can create a new Cutekit project with the `ck I {template-name}` command.
If you want to use another repository as a template, you can use the `ck I --repo="github-link" name` command. For example:
```bash
ck I --repo="cute-engineering/cutekit-templates.git" host
```
## Writing a template
When writing a template, you do it through a github repository (only github for now).
Then add a `registry.json` file at the root of the repository contaning a table of entry directories.
For example, if you have a UI library called `cute-ui`, you can add a `registry.json` file like this:
```json
[
{
"id": "cute-ui-simple",
"description": "A simple template"
},
{
"id": "cute-ui-advanced",
"description": "A more advanced template"
}
]
```
And each "id" will correspond to a directory in the repository:
- `cute-ui-simple` will be in `cute-ui-simple/`
- `cute-ui-advanced` will be in `cute-ui-advanced/`

View file

@ -1,32 +0,0 @@
[build-system]
requires = ["setuptools ~= 68.0"]
build-backend = "setuptools.build_meta"
[project]
name = "cutekit"
description = "A build system and package manager for low-level software development"
authors = [
{ name = "Keyboard Slayer", email = "joda@cute.engineering" },
{ name = "Sleepy Monax", email = "nivb@cute.engineering" },
{ name = "Cyp", email = "cypb@cute.engineering" },
{ name = "Cute Engineering", email = "contact@cute.engineering" },
]
readme = "README.md"
requires-python = ">=3.11"
license = { text = "MIT" }
dynamic = ["version", "dependencies"]
[project.scripts]
ck = "cutekit:main"
cutekit = "cutekit:main"
cute-engineering-cutekit = "cutekit:main"
[tool.setuptools]
packages = ["cutekit"]
[tool.setuptools.dynamic]
version = { attr = "cutekit.const.VERSION" }
dependencies = { file = ["requirements.txt"] }
[tool.setuptools.package-data]
"cutekit" = ["py.typed"]

View file

@ -1,3 +1,2 @@
requests ~= 2.31.0
graphviz ~= 0.20.1
dataclasses-json ~= 0.6.2
requests ~= 2.28.0
graphviz ~= 0.20.1

4
setup.cfg Normal file
View file

@ -0,0 +1,4 @@
[metadata]
description-file=README.md
license_file=LICENSE

27
setup.py Normal file
View file

@ -0,0 +1,27 @@
from setuptools import setup
from cutekit.const import VERSION_STR, DESCRIPTION
setup(
name="cutekit",
version=VERSION_STR,
python_requires='>=3.10',
description=DESCRIPTION,
author="Cute Engineering",
author_email="contact@cute.engineering",
url="https://cute.engineering/",
packages=["cutekit"],
install_requires=[
"requests",
"graphviz"
],
entry_points={
"console_scripts": [
"ck = cutekit:main",
"cutekit = cutekit:main",
],
},
license="MIT",
platforms="any",
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
)

View file

@ -1,90 +0,0 @@
from cutekit import model
def test_direct_deps():
r = model.Registry("")
r._append(model.Component("myapp", requires=["mylib"]))
r._append(model.Component("mylib"))
t = model.Target("host")
res = model.Resolver(r, t)
resolved = res.resolve("myapp")
assert resolved.reason is None
assert resolved.resolved == ["myapp", "mylib"]
def test_indirect_deps():
r = model.Registry("")
r._append(model.Component("myapp", requires=["mylib"]))
r._append(model.Component("mylib", requires=["myembed"]))
r._append(model.Component("myimpl", provides=["myembed"]))
t = model.Target("host")
res = model.Resolver(r, t)
assert res.resolve("myapp").resolved == ["myapp", "mylib", "myimpl"]
def test_deps_routing():
r = model.Registry("")
r._append(model.Component("myapp", requires=["mylib"]))
r._append(model.Component("mylib", requires=["myembed"]))
r._append(model.Component("myimplA", provides=["myembed"]))
r._append(model.Component("myimplB", provides=["myembed"]))
t = model.Target("host", routing={"myembed": "myimplB"})
res = model.Resolver(r, t)
assert res.resolve("myapp").resolved == ["myapp", "mylib", "myimplB"]
t = model.Target("host", routing={"myembed": "myimplA"})
res = model.Resolver(r, t)
assert res.resolve("myapp").resolved == ["myapp", "mylib", "myimplA"]
t = model.Target("host", routing={"myembed": "myimplC"})
res = model.Resolver(r, t)
assert res.resolve("myapp").reason == "No provider for 'myembed'"
def test_deps_routing_with_props():
r = model.Registry("")
r._append(model.Component("myapp", requires=["mylib"]))
r._append(model.Component("mylib", requires=["myembed"]))
r._append(
model.Component("myimplA", provides=["myembed"], enableIf={"myprop": ["a"]})
)
r._append(
model.Component("myimplB", provides=["myembed"], enableIf={"myprop": ["b"]})
)
t = model.Target("host", routing={"myembed": "myimplB"}, props={"myprop": "b"})
res = model.Resolver(r, t)
assert res.resolve("myapp").resolved == ["myapp", "mylib", "myimplB"]
t = model.Target("host", routing={"myembed": "myimplA"}, props={"myprop": "a"})
res = model.Resolver(r, t)
assert res.resolve("myapp").resolved == ["myapp", "mylib", "myimplA"]
t = model.Target("host", routing={"myembed": "myimplC"}, props={"myprop": "c"})
res = model.Resolver(r, t)
resolved = res.resolve("myapp")
assert resolved.reason == "No provider for 'myembed'"
def test_deps_routing_with_props_and_requires():
r = model.Registry("")
r._append(model.Component("myapp", requires=["mylib"]))
r._append(model.Component("mylib", requires=["myembed"]))
r._append(
model.Component("myimplA", provides=["myembed"], enableIf={"myprop": ["a"]})
)
r._append(
model.Component("myimplB", provides=["myembed"], enableIf={"myprop": ["b"]})
)
t = model.Target("host", routing={"myembed": "myimplB"}, props={"myprop": "b"})
res = model.Resolver(r, t)
assert res.resolve("myapp").resolved == ["myapp", "mylib", "myimplB"]
t = model.Target("host", routing={"myembed": "myimplA"}, props={"myprop": "a"})
res = model.Resolver(r, t)
assert res.resolve("myapp").resolved == ["myapp", "mylib", "myimplA"]
t = model.Target("host", routing={"myembed": "myimplC"}, props={"myprop": "c"})
res = model.Resolver(r, t)
assert res.resolve("myapp").reason == "No provider for 'myembed'"