Compare commits

..

19 commits

Author SHA1 Message Date
keyboard-slayer
e54f8f5964 ref: move to pathlib 2023-11-13 21:25:48 +01:00
keyboard-slayer
8f59111ad7 fix: check plugin if script is in same directory as __init__.py 2023-11-13 20:37:41 +01:00
01f0868db0 Fix naming for ci step 2023-11-13 19:35:26 +01:00
f6f36ea79e Refactored build context and dependecy resolution code 2023-11-13 19:21:54 +01:00
3dbf269cdd Run tests in the CI 2023-11-13 19:19:59 +01:00
68cae44750 Added missing requirement 2023-11-11 19:08:53 +01:00
a472abb90f Use dataclasses for the model 2023-11-11 19:05:41 +01:00
39ee66364d Add ensure function to check cutekit version 2023-11-11 19:03:19 +01:00
9dc1575a57 Update Python version and MyPy command. 2023-11-11 17:12:44 +01:00
0a5ed0c444 Moved more stuff to the cli module. 2023-11-11 17:02:56 +01:00
2307a72564 Add ci checks. 2023-11-11 17:02:42 +01:00
ce3728ccfb Cleanups imports and got ride of cutekit.project 2023-11-11 16:44:40 +01:00
c8b23bc6c1 Removed the "manifest" suffix from classes names in the model. 2023-11-11 16:19:14 +01:00
e77e787547 Update cutekit CLI to use new cli module 2023-11-11 16:17:33 +01:00
keyboard-slayer
31ca0b19e8 feat: new command line decorator 2023-11-11 13:40:14 +01:00
keyboard-slayer
d842c6af2d fix: incorrect type comparaison lead to infinite loop when not in ck project 2023-11-11 11:43:42 +01:00
920762f56d Added command decorator. 2023-11-10 13:29:32 +01:00
97f9f0ddba Add _ prefix to logger 2023-11-10 11:31:44 +01:00
9572c6a3df Fix project lookup on windows. 2023-10-31 14:22:07 +01:00
19 changed files with 251 additions and 397 deletions

View file

@ -13,28 +13,22 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v3 uses: actions/setup-python@v3
with: with:
python-version: '3.11' python-version: '3.x'
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
python -m pip install -r requirements.txt python -m pip install -r .github/workflows/requirements.txt
python -m pip install mypy pytest ruff
- name: Type Checking - name: Run MyPy
run: | run: |
python -m mypy --install-types --non-interactive . python -m mypy --install-types --non-interactive .
- name: Linting - name: Run PyTest
run: |
ruff check cutekit
- name: Unit Testing
run: | run: |
python -m pytest python -m pytest

View file

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

5
.github/workflows/requirements.txt vendored Normal file
View file

@ -0,0 +1,5 @@
requests ~= 2.28.0
graphviz ~= 0.20.1
dataclasses-json ~= 0.6.2
mypy ~= 1.7.0
pytest ~= 7.4.3

View file

@ -6,7 +6,7 @@
</p> </p>
<h1 align="center">CuteKit</h1> <h1 align="center">CuteKit</h1>
<p align="center"> <p align="center">
The *magical* build system and package manager The Cute build system and package manager
</p> </p>
<br/> <br/>
<br/> <br/>
@ -20,17 +20,17 @@
- [Example](#example) - [Example](#example)
## Introduction ## Introduction
**CuteKit** is a simple - yet - powerful build system and package manager for C and C++. It: **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 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 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 **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 **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. - ✨ It's **portable**: Cutekit can run on MacOS Gnu/Linux and Windows.
## Installation ## Installation
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. 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.
@ -45,13 +45,13 @@ $ cd cutekit
$ pip install --user -e . $ pip install --user -e .
``` ```
## Quick-start ## Quick-start
-> 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). -> 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).
-> 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`. -> 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 ## Example
If you want to see how it works you can read the [doc/cutekit.md](doc/cutekit.md) file. If you want to see how it works you can read the [doc/cutekit.md](doc/cutekit.md) file.

View file

@ -3,12 +3,19 @@ import os
import logging import logging
from . import ( from . import (
builder, # noqa: F401 this is imported for side effects builder,
cli, cli,
compat,
const, const,
graph, # noqa: F401 this is imported for side effects graph,
jexpr,
mixins,
model, model,
ninja,
plugins, plugins,
rules,
shell,
utils,
vt100, vt100,
) )
@ -29,20 +36,19 @@ def ensure(version: tuple[int, int, int]):
def setupLogger(verbose: bool): def setupLogger(verbose: bool):
if verbose: if verbose:
logging.basicConfig( 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", 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", datefmt="%Y-%m-%d %H:%M:%S",
) )
else: else:
projectRoot = model.Project.topmost() projectRoot = model.Project.root()
logFile = const.GLOBAL_LOG_FILE logFile = const.GLOBAL_LOG_FILE
if projectRoot is not None: if projectRoot is not None:
logFile = os.path.join(projectRoot.dirname(), const.PROJECT_LOG_FILE) logfile = projectRoot / const.PROJECT_LOG_FILE
# create the directory if it doesn't exist # create the directory if it doesn't exist
logDir = os.path.dirname(logFile) if not logFile.parent.is_dir():
if not os.path.isdir(logDir): logFile.parent.mkdir(parents=True)
os.makedirs(logDir)
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,

View file

@ -1,11 +1,11 @@
import os import os
import logging import logging
import dataclasses as dt
from pathlib import Path from pathlib import Path
from typing import TextIO, Union from dataclasses import dataclass
from itertools import chain
from typing import Generator, TextIO, Union, cast
from . import shell, rules, model, ninja, const, cli from . import shell, rules, model, ninja, const, utils, cli
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -46,26 +46,28 @@ def buildpath(target: model.Target, component: model.Component, path) -> Path:
# --- Compilation ------------------------------------------------------------ # # --- Compilation ------------------------------------------------------------ #
def wilcard(component: model.Component, wildcards: list[str]) -> list[str]: def listSrc(component: model.Component) -> list[str]:
wildcards = set(chain(*map(lambda rule: rule.fileIn, rules.rules.values())))
dirs = [component.dirname()] + list( dirs = [component.dirname()] + list(
map(lambda d: os.path.join(component.dirname(), d), component.subdirs) map(lambda d: component.parent / d, component.subdirs)
) )
return shell.find(dirs, list(wildcards), recusive=False) return shell.find(dirs, list(wildcards), recusive=False)
def compile( def compileSrc(
w: ninja.Writer, w: ninja.Writer, target: model.Target, component: model.Component
target: model.Target,
component: model.Component,
rule: str,
srcs: list[str],
) -> list[str]: ) -> list[str]:
res: list[str] = [] res: list[str] = []
for src in srcs: for src in listSrc(component):
rel = Path(src).relative_to(component.dirname()) rel = Path(src).relative_to(component.dirname())
dest = buildpath(target, component, "obj") / rel.with_suffix(".o")
t = target.tools[rule] r = rules.byFileIn(src)
w.build(str(dest), rule, inputs=src, order_only=t.files) if r is None:
raise RuntimeError(f"Unknown rule for file {src}")
dest = buildpath(target, component, "obj") / rel.with_suffix(r.fileOut[0][1:])
t = target.tools[r.id]
w.build(str(dest), r.id, inputs=src, order_only=t.files)
res.append(str(dest)) res.append(str(dest))
return res return res
@ -125,16 +127,7 @@ def link(
) -> str: ) -> str:
w.newline() w.newline()
out = outfile(target, component) out = outfile(target, component)
objs: list[str] = compileSrc(w, 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) res = compileRes(w, target, component)
libs = collectLibs(registry, target, component) libs = collectLibs(registry, target, component)
if component.type == model.Kind.LIB: if component.type == model.Kind.LIB:
@ -171,6 +164,7 @@ def gen(out: TextIO, target: model.Target, registry: model.Registry):
w.variable("cdefs", " ".join(aggregateCdefs(target))) w.variable("cdefs", " ".join(aggregateCdefs(target)))
w.newline() w.newline()
w.rule("cp", "cp $in $out")
for i in target.tools: for i in target.tools:
tool = target.tools[i] tool = target.tools[i]
rule = rules.rules[i] rule = rules.rules[i]
@ -188,7 +182,7 @@ def gen(out: TextIO, target: model.Target, registry: model.Registry):
all(w, registry, target) all(w, registry, target)
@dt.dataclass @dataclass
class Product: class Product:
path: Path path: Path
target: model.Target target: model.Target
@ -201,12 +195,11 @@ def build(
components: Union[list[model.Component], model.Component, None] = None, components: Union[list[model.Component], model.Component, None] = None,
) -> list[Product]: ) -> list[Product]:
all = False all = False
shell.mkdir(target.builddir) target.builddir.mkdir(parents=True, exist_ok=True)
ninjaPath = os.path.join(target.builddir, "build.ninja") ninjaPath = target.builddir / "build.ninja"
if not os.path.exists(ninjaPath): with ninjaPath.open("w") as f:
with open(ninjaPath, "w") as f: gen(f, target, registry)
gen(f, target, registry)
if components is None: if components is None:
all = True all = True
@ -217,10 +210,6 @@ def build(
products: list[Product] = [] products: list[Product] = []
for c in components: 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( products.append(
Product( Product(
path=Path(outfile(target, c)), path=Path(outfile(target, c)),
@ -230,9 +219,10 @@ def build(
) )
outs = list(map(lambda p: str(p.path), products)) outs = list(map(lambda p: str(p.path), products))
if all:
shell.exec("ninja", "-f", ninjaPath, *(outs if not all else [])) shell.exec("ninja", "-v", "-f", ninjaPath)
else:
shell.exec("ninja", "-v", "-f", ninjaPath, *outs)
return products return products
@ -244,20 +234,23 @@ def buildCmd(args: cli.Args):
registry = model.Registry.use(args) registry = model.Registry.use(args)
target = model.Target.use(args) target = model.Target.use(args)
componentSpec = args.consumeArg() componentSpec = args.consumeArg()
component = None if componentSpec is None:
if componentSpec is not None: raise RuntimeError("No component specified")
component = registry.lookup(componentSpec, model.Component) component = registry.lookup(componentSpec, model.Component)
build(target, registry, component)[0] build(target, registry, component)[0]
@cli.command("r", "run", "Run a component") @cli.command("p", "project", "Show project information")
def runCmd(args: cli.Args): def runCmd(args: cli.Args):
registry = model.Registry.use(args) registry = model.Registry.use(args)
target = model.Target.use(args) target = model.Target.use(args)
debug = args.consumeOpt("debug", False) is True debug = args.consumeOpt("debug", False) is True
componentSpec = args.consumeArg() or "__main__" componentSpec = args.consumeArg()
component = registry.lookup(componentSpec, model.Component, includeProvides=True) if componentSpec is None:
raise RuntimeError("No component specified")
component = registry.lookup(componentSpec, model.Component)
if component is None: if component is None:
raise RuntimeError(f"Component {componentSpec} not found") raise RuntimeError(f"Component {componentSpec} not found")

View file

@ -1,17 +1,14 @@
import inspect import inspect
import logging
import sys import sys
import dataclasses as dt
from pathlib import Path from pathlib import Path
from typing import Optional, Union, Callable from typing import Optional, Union, Callable
from dataclasses import dataclass
from . import const, vt100 from . import const, vt100
Value = Union[str, bool, int] Value = Union[str, bool, int]
_logger = logging.getLogger(__name__)
class Args: class Args:
opts: dict[str, Value] opts: dict[str, Value]
@ -72,7 +69,7 @@ def parse(args: list[str]) -> Args:
Callback = Callable[[Args], None] Callback = Callable[[Args], None]
@dt.dataclass @dataclass
class Command: class Command:
shortName: Optional[str] shortName: Optional[str]
longName: str longName: str
@ -89,7 +86,6 @@ def command(shortName: Optional[str], longName: str, helpText: str):
calframe = inspect.getouterframes(curframe, 2) calframe = inspect.getouterframes(curframe, 2)
def wrap(fn: Callable[[Args], None]): def wrap(fn: Callable[[Args], None]):
_logger.debug(f"Registering command {longName}")
commands.append( commands.append(
Command( Command(
shortName, shortName,
@ -127,10 +123,7 @@ def helpCmd(args: Args):
print() print()
vt100.title("Commands") vt100.title("Commands")
for cmd in sorted(commands, key=lambda c: c.longName): for cmd in sorted(commands, key=lambda c: c.shortName or c.longName):
if cmd.longName.startswith("_"):
continue
pluginText = "" pluginText = ""
if cmd.isPlugin: if cmd.isPlugin:
pluginText = f"{vt100.CYAN}(plugin){vt100.RESET}" pluginText = f"{vt100.CYAN}(plugin){vt100.RESET}"

View file

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

View file

@ -80,7 +80,7 @@ def view(
for req in component.provides: for req in component.provides:
g.edge(req, component.id, arrowhead="none", color="#aaaaaa") g.edge(req, component.id, arrowhead="none", color="#aaaaaa")
g.view(filename=os.path.join(target.builddir, "graph.gv")) g.view(filename=str(target.builddir / "graph.gv"))
@cli.command("g", "graph", "Show the dependency graph") @cli.command("g", "graph", "Show the dependency graph")

View file

@ -1,20 +1,17 @@
import os import os
import json import json
import re
import tomllib
from pathlib import Path from pathlib import Path
from typing import Any, Optional, cast, Callable, Final from typing import Any, cast, Callable, Final
from . import shell from . import shell, compat
Json = Any Json = Any
Builtin = Callable[..., Json] Builtin = Callable[..., Json]
BUILTINS: Final[dict[str, Builtin]] = { BUILTINS: Final[dict[str, Builtin]] = {
"uname": lambda arg, ctx: getattr(shell.uname(), arg).lower(), "uname": lambda arg, ctx: getattr(shell.uname(), arg).lower(),
"include": lambda arg, ctx: evalRead(Path(arg)), "include": lambda arg, ctx: evalRead(arg),
"evalRead": lambda arg, ctx: evalRead(Path(arg)), "evalRead": lambda arg, ctx: evalRead(arg),
"join": lambda lhs, rhs, ctx: cast( "join": lambda lhs, rhs, ctx: cast(
Json, {**lhs, **rhs} if isinstance(lhs, dict) else lhs + rhs Json, {**lhs, **rhs} if isinstance(lhs, dict) else lhs + rhs
), ),
@ -53,26 +50,12 @@ def eval(jexpr: Json, filePath: Path) -> Json:
return jexpr 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: Path) -> Json:
try: try:
with open(path, "r") as f: with open(path, "r") as f:
if path.suffix == ".toml": return json.load(f)
tomlStr = f.read() except:
toml = tomllib.loads(tomlStr) raise RuntimeError(f"Failed to read {path}")
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}")
def evalRead(path: Path) -> Json: def evalRead(path: Path) -> Json:

View file

@ -65,7 +65,6 @@ mixins: dict[str, Mixin] = {
"tsan": makeMixinSan("thread"), "tsan": makeMixinSan("thread"),
"ubsan": makeMixinSan("undefined"), "ubsan": makeMixinSan("undefined"),
"tune": makeMixinTune("native"), "tune": makeMixinTune("native"),
"fast": makeMixinOptimize("fast"),
"o3": makeMixinOptimize("3"), "o3": makeMixinOptimize("3"),
"o2": makeMixinOptimize("2"), "o2": makeMixinOptimize("2"),
"o1": makeMixinOptimize("1"), "o1": makeMixinOptimize("1"),
@ -78,6 +77,4 @@ def append(mixinSpec: str, mixin: Mixin):
def byId(id: str) -> Mixin: def byId(id: str) -> Mixin:
if id not in mixins:
raise RuntimeError(f"Unknown mixin {id}")
return mixins[id] return mixins[id]

View file

@ -1,12 +1,12 @@
import os import os
import logging import logging
import dataclasses as dt
from enum import Enum from enum import Enum
from typing import Any, Generator, Optional, Type, cast from typing import Any, Generator, Optional, Type, cast
from pathlib import Path from pathlib import Path
from dataclasses_json import DataClassJsonMixin from dataclasses_json import DataClassJsonMixin
from dataclasses import dataclass, field
from cutekit import const, shell from cutekit import const, shell
@ -28,13 +28,11 @@ class Kind(Enum):
# --- Manifest --------------------------------------------------------------- # # --- Manifest --------------------------------------------------------------- #
@dt.dataclass @dataclass
class Manifest(DataClassJsonMixin): class Manifest(DataClassJsonMixin):
id: str id: str
type: Kind = dt.field(default=Kind.UNKNOWN) type: Kind = field(default=Kind.UNKNOWN)
path: str = dt.field(default="") path: Path = field(default=Path())
SUFFIXES = [".json", ".toml"]
SUFFIXES_GLOBS = ["*.json", "*.toml"]
@staticmethod @staticmethod
def parse(path: Path, data: dict[str, Any]) -> "Manifest": def parse(path: Path, data: dict[str, Any]) -> "Manifest":
@ -45,36 +43,24 @@ class Manifest(DataClassJsonMixin):
kind = Kind(data["type"]) kind = Kind(data["type"])
del data["$schema"] del data["$schema"]
obj = KINDS[kind].from_dict(data) obj = KINDS[kind].from_dict(data)
obj.path = str(path) obj.path = path
return obj return obj
@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
@staticmethod @staticmethod
def load(path: Path) -> "Manifest": def load(path: Path) -> "Manifest":
""" """
Load a manifest from a given path Load a manifest from a given path
""" """
manifest = Manifest.tryLoad(path) return Manifest.parse(path, jexpr.evalRead(path))
if manifest is None:
raise RuntimeError(f"Could not find manifest at '{path}'")
return manifest
def dirname(self) -> str: def dirname(self) -> Path:
""" """
Return the directory of the manifest Return the directory of the manifest
""" """
return os.path.relpath(os.path.dirname(self.path), Path.cwd()) return self.path.parent
def subpath(self, path) -> Path: def subpath(self, path) -> Path:
return Path(self.dirname()) / path return self.dirname() / path
def ensureType(self, t: Type[utils.T]) -> utils.T: def ensureType(self, t: Type[utils.T]) -> utils.T:
""" """
@ -92,60 +78,71 @@ class Manifest(DataClassJsonMixin):
_project: Optional["Project"] = None _project: Optional["Project"] = None
@dt.dataclass @dataclass
class Extern(DataClassJsonMixin): class Extern(DataClassJsonMixin):
git: str git: str
tag: str tag: str
@dt.dataclass @dataclass
class Project(Manifest): class Project(Manifest):
description: str = dt.field(default="(No description)") description: str = field(default="(No description)")
extern: dict[str, Extern] = dt.field(default_factory=dict) extern: dict[str, Extern] = field(default_factory=dict)
@property @property
def externDirs(self) -> list[str]: def externDirs(self) -> list[Path]:
res = map(lambda e: os.path.join(const.EXTERN_DIR, e), self.extern.keys()) res = map(lambda e: const.EXTERN_DIR / e, self.extern.keys())
return list(res) return list(res)
@staticmethod @staticmethod
def topmost() -> Optional["Project"]: def root() -> Optional[Path]:
"""
Find the root of the project by looking for a project.json
"""
cwd = Path.cwd() cwd = Path.cwd()
topmost: Optional["Project"] = None
while str(cwd) != cwd.root: while str(cwd) != cwd.root:
projectManifest = Manifest.tryLoad(cwd / "project") if (cwd / "project.json").is_file():
if projectManifest is not None: return cwd
topmost = projectManifest.ensureType(Project)
cwd = cwd.parent cwd = cwd.parent
return topmost return None
@staticmethod
def chdir() -> None:
"""
Change the current working directory to the root of the project
"""
path = Project.root()
if path is None:
raise RuntimeError(
"No project.json found in this directory or any parent directory"
)
os.chdir(path)
@staticmethod
def at(path: str | Path) -> Optional["Project"]:
path = Path(path) / "project.json"
if not path.exists():
return None
return Manifest.load(path).ensureType(Project)
@staticmethod @staticmethod
def ensure() -> "Project": def ensure() -> "Project":
""" root = Project.root()
Ensure that a project exists in the current directory or any parent directory if root is None:
and chdir to the root of the project.
"""
project = Project.topmost()
if project is None:
raise RuntimeError( raise RuntimeError(
"No project found in this directory or any parent directory" "No project.json found in this directory or any parent directory"
) )
os.chdir(project.dirname()) os.chdir(root)
return project return Manifest.load(Path(root / "project.json")).ensureType(
Project
)
@staticmethod @staticmethod
def at(path: Path) -> Optional["Project"]: def fetchs(extern: dict[str | Path, Extern]):
projectManifest = Manifest.tryLoad(path / "project")
if projectManifest is None:
return None
return projectManifest.ensureType(Project)
@staticmethod
def fetchs(extern: dict[str, Extern]):
for extSpec, ext in extern.items(): for extSpec, ext in extern.items():
extPath = os.path.join(const.EXTERN_DIR, extSpec) extPath = const.EXTERN_DIR / extSpec
if os.path.exists(extPath): if extPath.exists():
print(f"Skipping {extSpec}, already installed") print(f"Skipping {extSpec}, already installed")
continue continue
@ -161,7 +158,7 @@ class Project(Manifest):
ext.git, ext.git,
extPath, extPath,
) )
project = Project.at(Path(extPath)) project = Project.at(extPath)
if project is not None: if project is not None:
Project.fetchs(project.extern) Project.fetchs(project.extern)
@ -187,10 +184,9 @@ def initCmd(args: cli.Args):
list = args.consumeOpt("list") list = args.consumeOpt("list")
template = args.consumeArg() template = args.consumeArg()
name = args.consumeArg() name = Path(args.consumeArg())
_logger.info("Fetching registry...") _logger.info("Fetching registry...")
r = requests.get(f"https://raw.githubusercontent.com/{repo}/main/registry.json") r = requests.get(f"https://raw.githubusercontent.com/{repo}/main/registry.json")
if r.status_code != 200: if r.status_code != 200:
@ -218,7 +214,7 @@ def initCmd(args: cli.Args):
_logger.info(f"No name was provided, defaulting to {template}") _logger.info(f"No name was provided, defaulting to {template}")
name = template name = template
if os.path.exists(name): if name.exists():
raise RuntimeError(f"Directory {name} already exists") raise RuntimeError(f"Directory {name} already exists")
print(f"Creating project {name} from template {template}...") print(f"Creating project {name} from template {template}...")
@ -238,33 +234,29 @@ def initCmd(args: cli.Args):
# --- Target ----------------------------------------------------------------- # # --- Target ----------------------------------------------------------------- #
@dt.dataclass @dataclass
class Tool(DataClassJsonMixin): class Tool(DataClassJsonMixin):
cmd: str = dt.field(default="") cmd: str = field(default="")
args: list[str] = dt.field(default_factory=list) args: list[str] = field(default_factory=list)
files: list[str] = dt.field(default_factory=list) files: list[str] = field(default_factory=list)
Tools = dict[str, Tool] Tools = dict[str, Tool]
DEFAULT_TOOLS: Tools = {
"cp": Tool("cp"),
}
@dataclass
@dt.dataclass
class Target(Manifest): class Target(Manifest):
props: Props = dt.field(default_factory=dict) props: Props = field(default_factory=dict)
tools: Tools = dt.field(default_factory=dict) tools: Tools = field(default_factory=dict)
routing: dict[str, str] = dt.field(default_factory=dict) routing: dict[str, str] = field(default_factory=dict)
@property @property
def hashid(self) -> str: def hashid(self) -> str:
return utils.hash((self.props, [v.to_dict() for k, v in self.tools.items()])) return utils.hash((self.props, [v.to_dict() for k, v in self.tools.items()]))
@property @property
def builddir(self) -> str: def builddir(self) -> Path:
return os.path.join(const.BUILD_DIR, f"{self.id}-{self.hashid[:8]}") return const.BUILD_DIR / f"{self.id}-{self.hashid[:8]}"
@staticmethod @staticmethod
def use(args: cli.Args) -> "Target": def use(args: cli.Args) -> "Target":
@ -286,27 +278,27 @@ class Target(Manifest):
# --- Component -------------------------------------------------------------- # # --- Component -------------------------------------------------------------- #
@dt.dataclass @dataclass
class Resolved: class Resolved:
reason: Optional[str] = None reason: Optional[str] = None
resolved: list[str] = dt.field(default_factory=list) resolved: list[str] = field(default_factory=list)
@property @property
def enabled(self) -> bool: def enabled(self) -> bool:
return self.reason is None return self.reason is None
@dt.dataclass @dataclass
class Component(Manifest): class Component(Manifest):
decription: str = dt.field(default="(No description)") decription: str = field(default="(No description)")
props: Props = dt.field(default_factory=dict) props: Props = field(default_factory=dict)
tools: Tools = dt.field(default_factory=dict) tools: Tools = field(default_factory=dict)
enableIf: dict[str, list[Any]] = dt.field(default_factory=dict) enableIf: dict[str, list[Any]] = field(default_factory=dict)
requires: list[str] = dt.field(default_factory=list) requires: list[str] = field(default_factory=list)
provides: list[str] = dt.field(default_factory=list) provides: list[str] = field(default_factory=list)
subdirs: list[str] = dt.field(default_factory=list) subdirs: list[str] = field(default_factory=list)
injects: list[str] = dt.field(default_factory=list) injects: list[str] = field(default_factory=list)
resolved: dict[str, Resolved] = dt.field(default_factory=dict) resolved: dict[str, Resolved] = field(default_factory=dict)
def isEnabled(self, target: Target) -> tuple[bool, str]: def isEnabled(self, target: Target) -> tuple[bool, str]:
for k, v in self.enableIf.items(): for k, v in self.enableIf.items():
@ -337,12 +329,12 @@ KINDS: dict[Kind, Type[Manifest]] = {
# --- Dependency resolution -------------------------------------------------- # # --- Dependency resolution -------------------------------------------------- #
@dt.dataclass @dataclass
class Resolver: class Resolver:
_registry: "Registry" _registry: "Registry"
_target: Target _target: Target
_mappings: dict[str, list[Component]] = dt.field(default_factory=dict) _mappings: dict[str, list[Component]] = field(default_factory=dict)
_cache: dict[str, Resolved] = dt.field(default_factory=dict) _cache: dict[str, Resolved] = field(default_factory=dict)
_baked = False _baked = False
def _bake(self): def _bake(self):
@ -449,25 +441,21 @@ class Resolver:
_registry: Optional["Registry"] = None _registry: Optional["Registry"] = None
@dt.dataclass @dataclass
class Registry(DataClassJsonMixin): class Registry(DataClassJsonMixin):
project: Project project: Project
manifests: dict[str, Manifest] = dt.field(default_factory=dict) manifests: dict[str, Manifest] = field(default_factory=dict)
def _append(self, m: Optional[Manifest]) -> Optional[Manifest]: def _append(self, m: Manifest):
""" """
Append a manifest to the model Append a manifest to the model
""" """
if m is None:
return m
if m.id in self.manifests: if m.id in self.manifests:
raise RuntimeError( raise RuntimeError(
f"Duplicated manifest '{m.id}' at '{m.path}' already loaded from '{self.manifests[m.id].path}'" f"Duplicated manifest '{m.id}' at '{m.path}' already loaded from '{self.manifests[m.id].path}'"
) )
self.manifests[m.id] = m self.manifests[m.id] = m
return m
def iter(self, type: Type[utils.T]) -> Generator[utils.T, None, None]: def iter(self, type: Type[utils.T]) -> Generator[utils.T, None, None]:
""" """
@ -484,9 +472,7 @@ class Registry(DataClassJsonMixin):
if resolve.enabled: if resolve.enabled:
yield c yield c
def lookup( def lookup(self, name: str, type: Type[utils.T]) -> Optional[utils.T]:
self, name: str, type: Type[utils.T], includeProvides: bool = False
) -> Optional[utils.T]:
""" """
Lookup a manifest of a given type by name Lookup a manifest of a given type by name
""" """
@ -496,11 +482,6 @@ class Registry(DataClassJsonMixin):
if isinstance(m, type): if isinstance(m, type):
return m 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 return None
def ensure(self, name: str, type: Type[utils.T]) -> utils.T: def ensure(self, name: str, type: Type[utils.T]) -> utils.T:
@ -532,55 +513,62 @@ class Registry(DataClassJsonMixin):
@staticmethod @staticmethod
def load(project: Project, mixins: list[str], props: Props) -> "Registry": def load(project: Project, mixins: list[str], props: Props) -> "Registry":
r = Registry(project) registry = Registry(project)
r._append(project) registry._append(project)
# Lookup and load all extern projects # Lookup and load all extern projects
for externDir in project.externDirs: for externDir in project.externDirs:
extern = r._append( projectPath = externDir / "project.json"
Manifest.tryLoad(Path(externDir) / "project") manifestPath = externDir / "manifest.json"
or Manifest.tryLoad(Path(externDir) / "manifest")
)
if extern is not None: if projectPath.exists():
_logger.warn("Extern project does not have a project or manifest") registry._append(Manifest.load(projectPath).ensureType(Project))
elif manifestPath.exists():
# For simple library allow to have a manifest.json instead of a project.json
registry._append(
Manifest.load(manifestPath).ensureType(Component)
)
else:
_logger.warn(
"Extern project does not have a project.json or manifest.json"
)
# Load all manifests from projects # Load all manifests from projects
for project in list(r.iter(Project)): for project in list(registry.iter(Project)):
targetDir = os.path.join(project.dirname(), const.TARGETS_DIR) targetDir = project.parent / const.TARGETS_DIR
targetFiles = shell.find(targetDir, Manifest.SUFFIXES_GLOBS) targetFiles = targetDir.glob("*.json")
for targetFile in targetFiles: for targetFile in targetFiles:
r._append(Manifest.load(Path(targetFile)).ensureType(Target)) registry._append(Manifest.load(Path(targetFile)).ensureType(Target))
componentFiles = shell.find( componentDir = project.parent / const.COMPONENTS_DIR
os.path.join(project.dirname(), const.SRC_DIR), rootComponent = project.parent / "manifest.json"
["manifest" + s for s in Manifest.SUFFIXES], componentFiles = list(componentDir.glob("manifest.json"))
)
rootComponent = Manifest.tryLoad(Path(project.dirname()) / "manifest") if rootComponent.exists():
if rootComponent is not None: componentFiles += [rootComponent]
r._append(rootComponent)
for componentFile in componentFiles: for componentFile in componentFiles:
r._append(Manifest.load(Path(componentFile)).ensureType(Component)) registry._append(
Manifest.load(componentFile).ensureType(Component)
)
# Resolve all dependencies for all targets # Resolve all dependencies for all targets
for target in r.iter(Target): for target in registry.iter(Target):
target.props |= props target.props |= props
resolver = Resolver(r, target) resolver = Resolver(registry, target)
# Apply injects # Apply injects
for c in r.iter(Component): for c in registry.iter(Component):
if c.isEnabled(target)[0]: if c.isEnabled(target)[0]:
for inject in c.injects: for inject in c.injects:
victim = r.lookup(inject, Component) victim = registry.lookup(inject, Component)
if not victim: if not victim:
raise RuntimeError(f"Cannot find component '{inject}'") raise RuntimeError(f"Cannot find component '{inject}'")
victim.requires += [c.id] victim.requires += [c.id]
# Resolve all components # Resolve all components
for c in r.iter(Component): for c in registry.iter(Component):
resolved = resolver.resolve(c.id) resolved = resolver.resolve(c.id)
if resolved.reason: if resolved.reason:
_logger.info(f"Component '{c.id}' disabled: {resolved.reason}") _logger.info(f"Component '{c.id}' disabled: {resolved.reason}")
@ -588,12 +576,6 @@ class Registry(DataClassJsonMixin):
# Resolve tooling # Resolve tooling
tools: Tools = target.tools 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 from . import mixins as mxs
for mix in mixins: for mix in mixins:
@ -601,12 +583,12 @@ class Registry(DataClassJsonMixin):
tools = mixin(target, tools) tools = mixin(target, tools)
# Apply tooling from components # Apply tooling from components
for c in r.iter(Component): for c in registry.iter(Component):
if c.resolved[target.id].enabled: if c.resolved[target.id].enabled:
for k, v in c.tools.items(): for k, v in c.tools.items():
tools[k].args += v.args tools[k].args += v.args
return r return registry
@cli.command("l", "list", "List all components and targets") @cli.command("l", "list", "List all components and targets")

View file

@ -23,21 +23,21 @@ def load(path: str):
def loadAll(): def loadAll():
_logger.info("Loading plugins...") _logger.info("Loading plugins...")
project = model.Project.topmost() root = model.Project.root()
if project is None:
if root is None:
_logger.info("Not in project, skipping plugin loading") _logger.info("Not in project, skipping plugin loading")
return return
paths = list(
map(lambda e: os.path.join(const.EXTERN_DIR, e), project.extern.keys()) project = model.Project.at(root)
) + ["."] paths = list(map(lambda e: const.EXTERN_DIR / e, project.extern.keys())) + ["."]
for dirname in paths: for dirname in paths:
pluginDir = os.path.join(project.dirname(), dirname, const.META_DIR, "plugins") pluginDir = root / dirname / const.META_DIR / "plugins"
for files in shell.readdir(pluginDir): for script in pluginDir.glob("*.py"):
if files.endswith(".py"): plugin = load(script)
plugin = load(os.path.join(pluginDir, files))
if plugin: if plugin:
_logger.info(f"Loaded plugin {plugin.name}") _logger.info(f"Loaded plugin {plugin.name}")
plugin.init() plugin.init()

View file

@ -27,7 +27,6 @@ class Rule:
rules: dict[str, Rule] = { rules: dict[str, Rule] = {
"cp": Rule("cp", ["*"], ["*"], "$in $out"),
"cc": Rule( "cc": Rule(
"cc", "cc",
["*.c"], ["*.c"],

View file

@ -12,6 +12,7 @@ import logging
import tempfile import tempfile
from pathlib import Path
from typing import Optional from typing import Optional
from . import const from . import const
@ -49,81 +50,30 @@ def sha256sum(path: str) -> str:
return hashlib.sha256(f.read()).hexdigest() return hashlib.sha256(f.read()).hexdigest()
def find( def rmrf(path: Path) -> bool:
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] = []
if isinstance(path, list):
for p in path:
result += find(p, wildcards, recusive)
return result
if not os.path.isdir(path):
return []
if recusive:
for root, _, files in os.walk(path):
for f in files:
if len(wildcards) == 0:
result.append(os.path.join(root, f))
else:
for wildcard in wildcards:
if fnmatch.fnmatch(f, wildcard):
result.append(os.path.join(root, f))
break
else:
for f in os.listdir(path):
if len(wildcards) == 0:
result.append(os.path.join(path, f))
else:
for wildcard in wildcards:
if fnmatch.fnmatch(f, wildcard):
result.append(os.path.join(path, f))
break
return result
def mkdir(path: str) -> str:
_logger.info(f"Creating directory {path}")
try:
os.makedirs(path)
except OSError as exc:
if not (exc.errno == errno.EEXIST and os.path.isdir(path)):
raise
return path
def rmrf(path: str) -> bool:
_logger.info(f"Removing directory {path}") _logger.info(f"Removing directory {path}")
if not os.path.exists(path): if not path.exists():
return False return False
shutil.rmtree(path, ignore_errors=True) shutil.rmtree(path, ignore_errors=True)
return True return True
def wget(url: str, path: Optional[str] = None) -> str: def wget(url: str, path: Optional[Path] = None) -> Path:
import requests import requests
if path is None: if path is None:
path = os.path.join( path = 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): if path.exists():
return path return path
_logger.info(f"Downloading {url} to {path}") _logger.info(f"Downloading {url} to {path}")
r = requests.get(url, stream=True) r = requests.get(url, stream=True)
r.raise_for_status() r.raise_for_status()
mkdir(os.path.dirname(path)) path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "wb") as f: with path.open("wb") as f:
for chunk in r.iter_content(chunk_size=8192): for chunk in r.iter_content(chunk_size=8192):
if chunk: if chunk:
f.write(chunk) f.write(chunk)
@ -179,36 +129,28 @@ def popen(*args: str) -> str:
return proc.stdout.decode("utf-8") return proc.stdout.decode("utf-8")
def readdir(path: str) -> list[str]: def cp(src: Path, dst: Path):
_logger.info(f"Reading directory {path}")
try:
return os.listdir(path)
except FileNotFoundError:
return []
def cp(src: str, dst: str):
_logger.info(f"Copying {src} to {dst}") _logger.info(f"Copying {src} to {dst}")
shutil.copy(src, dst) shutil.copy(src, dst)
def mv(src: str, dst: str): def mv(src: Path, dst: Path):
_logger.info(f"Moving {src} to {dst}") _logger.info(f"Moving {src} to {dst}")
shutil.move(src, dst) shutil.move(src, dst)
def cpTree(src: str, dst: str): def cpTree(src: Path, dst: Path):
_logger.info(f"Copying {src} to {dst}") _logger.info(f"Copying {src} to {dst}")
shutil.copytree(src, dst, dirs_exist_ok=True) shutil.copytree(src, dst, dirs_exist_ok=True)
def cloneDir(url: str, path: str, dest: str) -> str: def cloneDir(url: str, path: Path, dest: Path) -> Path:
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
mkdir(tmp) tmp = Path(tmp)
tmp.mkdir(parents=True, exist_ok=True)
exec( exec(
*["git", "clone", "-n", "--depth=1", "--filter=tree:0", url, tmp, "-q"], *["git", "clone", "-n", "--depth=1", "--filter=tree:0", url, tmp, "-q"],
quiet=True, quiet=True,
@ -218,7 +160,7 @@ def cloneDir(url: str, path: str, dest: str) -> str:
quiet=True, quiet=True,
) )
exec(*["git", "-C", tmp, "checkout", "-q", "--no-progress"], quiet=True) exec(*["git", "-C", tmp, "checkout", "-q", "--no-progress"], quiet=True)
mv(os.path.join(tmp, path), dest) mv(tmp / path, dest)
return dest return dest

View file

@ -6,9 +6,9 @@ import hashlib
T = TypeVar("T") T = TypeVar("T")
def uniq(lst: list[T]) -> list[T]: def uniq(l: list[T]) -> list[T]:
result: list[T] = [] result: list[T] = []
for i in lst: for i in l:
if i in result: if i in result:
result.remove(i) result.remove(i)
result.append(i) result.append(i)

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

(image error) Size: 2.5 KiB

View file

@ -12,9 +12,14 @@ authors = [
{ name = "Cute Engineering", email = "contact@cute.engineering" }, { name = "Cute Engineering", email = "contact@cute.engineering" },
] ]
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.10"
license = { text = "MIT" } license = { text = "MIT" }
dynamic = ["version", "dependencies"] dependencies = [
"requests ~= 2.28.0",
"graphviz ~= 0.20.1",
"dataclasses-json ~= 0.6.2",
]
dynamic = ["version"]
[project.scripts] [project.scripts]
ck = "cutekit:main" ck = "cutekit:main"
@ -26,7 +31,6 @@ packages = ["cutekit"]
[tool.setuptools.dynamic] [tool.setuptools.dynamic]
version = { attr = "cutekit.const.VERSION" } version = { attr = "cutekit.const.VERSION" }
dependencies = { file = ["requirements.txt"] }
[tool.setuptools.package-data] [tool.setuptools.package-data]
"cutekit" = ["py.typed"] "cutekit" = ["py.typed"]

View file

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