diff --git a/cutekit/compat.py b/cutekit/compat.py index dbcacc3..4ff7d9b 100644 --- a/cutekit/compat.py +++ b/cutekit/compat.py @@ -1,3 +1,4 @@ +from pathlib import Path from typing import Any @@ -5,10 +6,11 @@ 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, @@ -20,14 +22,16 @@ UNSUPORTED_MANIFEST = { } -def ensureSupportedManifest(manifest: Any, path: str): +def ensureSupportedManifest(manifest: Any, path: Path): if "$schema" not 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: raise RuntimeError( - f"Unsupported manifest schema {manifest['$schema']} in {path}") + f"Unsupported manifest schema {manifest['$schema']} in {path}" + ) diff --git a/cutekit/const.py b/cutekit/const.py index 0b10e91..6124018 100644 --- a/cutekit/const.py +++ b/cutekit/const.py @@ -1,7 +1,7 @@ import os import sys -VERSION = (0, 5, 4) +VERSION = (0, 6, 0, "dev") VERSION_STR = f"{VERSION[0]}.{VERSION[1]}.{VERSION[2]}{'-' + VERSION[3] if len(VERSION) >= 4 else ''}" MODULE_DIR = os.path.dirname(os.path.realpath(__file__)) ARGV0 = os.path.basename(sys.argv[0]) diff --git a/cutekit/context.py b/cutekit/context.py index 351ecc1..90e92c1 100644 --- a/cutekit/context.py +++ b/cutekit/context.py @@ -46,7 +46,7 @@ class ComponentInstance: return self.manifest.id def isLib(self): - return self.manifest.type == model.Type.LIB + return self.manifest.type == model.Kind.LIB def objdir(self) -> str: return os.path.join(self.context.builddir(), f"{self.manifest.id}/obj") @@ -88,7 +88,7 @@ class ComponentInstance: def cinclude(self) -> str: if "cpp-root-include" in self.manifest.props: return self.manifest.dirname() - elif self.manifest.type == model.Type.LIB: + elif self.manifest.type == model.Kind.LIB: return str(Path(self.manifest.dirname()).parent) else: return "" @@ -131,7 +131,7 @@ class Context(IContext): def hashid(self) -> str: return utils.hash( - (self.target.props, [self.tools[t].toJson() for t in self.tools]) + (self.target.props, [self.tools[t].to_dict() for t in self.tools]) )[0:8] def builddir(self) -> str: @@ -154,14 +154,19 @@ def loadAllTargets() -> list[model.Target]: ret = [] for entry in paths: files = shell.find(entry, ["*.json"]) - ret += list(map(lambda path: model.Target(jexpr.evalRead(path), path), files)) + ret += list( + map( + lambda path: model.Manifest.load(Path(path)).ensureType(model.Target), + files, + ) + ) return ret def loadProject(path: str) -> model.Project: path = os.path.join(path, "project.json") - return model.Project(jexpr.evalRead(path), path) + return model.Manifest.load(Path(path)).ensureType(model.Project) def loadTarget(id: str) -> model.Target: @@ -175,7 +180,12 @@ def loadAllComponents() -> list[model.Component]: files = shell.find(const.SRC_DIR, ["manifest.json"]) files += shell.find(const.EXTERN_DIR, ["manifest.json"]) - return list(map(lambda path: model.Component(jexpr.evalRead(path), path), files)) + return list( + map( + lambda path: model.Manifest.load(Path(path)).ensureType(model.Component), + files, + ) + ) def filterDisabled( @@ -242,7 +252,7 @@ def resolveDeps( enabled, unresolvedReason, resolved = resolveInner(componentSpec) - return enabled, unresolvedReason, resolved + return enabled, unresolvedReason, utils.uniq(resolved) def instanciate( @@ -250,7 +260,10 @@ def instanciate( ) -> 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) + dirs = [manifest.dirname()] + list( + map(lambda d: os.path.join(manifest.dirname(), d), manifest.subdirs) + ) + sources = shell.find(dirs, list(wildcards), recusive=False) res = shell.find(os.path.join(manifest.dirname(), "res")) @@ -299,9 +312,7 @@ def contextFor(targetSpec: str, props: model.Props = {}) -> Context: for toolSpec in target.tools: tool = target.tools[toolSpec] - tools[toolSpec] = model.Tool( - strict=False, cmd=tool.cmd, args=tool.args, files=tool.files - ) + tools[toolSpec] = model.Tool(cmd=tool.cmd, args=tool.args, files=tool.files) tools[toolSpec].args += rules.rules[toolSpec].args diff --git a/cutekit/jexpr.py b/cutekit/jexpr.py index 91b727f..4293954 100644 --- a/cutekit/jexpr.py +++ b/cutekit/jexpr.py @@ -1,5 +1,6 @@ import os import json +from pathlib import Path from typing import Any, cast, Callable, Final from . import shell, compat @@ -9,8 +10,8 @@ Builtin = Callable[..., Json] BUILTINS: Final[dict[str, Builtin]] = { "uname": lambda arg, ctx: getattr(shell.uname(), arg).lower(), - "include": lambda arg, ctx: evalRead(arg, compatibilityCheck=False), - "evalRead": lambda arg, ctx: evalRead(arg, compatibilityCheck=False), + "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 ), @@ -27,7 +28,7 @@ BUILTINS: Final[dict[str, Builtin]] = { } -def eval(jexpr: Json, filePath: str) -> Json: +def eval(jexpr: Json, filePath: Path) -> Json: if isinstance(jexpr, dict): result = {} for k in cast(dict[str, Json], jexpr): @@ -49,7 +50,7 @@ def eval(jexpr: Json, filePath: str) -> Json: return jexpr -def read(path: str) -> Json: +def read(path: Path) -> Json: try: with open(path, "r") as f: return json.load(f) @@ -57,8 +58,6 @@ def read(path: str) -> Json: raise RuntimeError(f"Failed to read {path}") -def evalRead(path: str, compatibilityCheck: bool = True) -> Json: +def evalRead(path: Path) -> Json: data = read(path) - if compatibilityCheck: - compat.ensureSupportedManifest(data, path) return eval(data, path) diff --git a/cutekit/model.py b/cutekit/model.py index 3bf1e45..8a2cd58 100644 --- a/cutekit/model.py +++ b/cutekit/model.py @@ -1,18 +1,21 @@ import os import logging -from enum import Enum -from typing import Any -from pathlib import Path -from . import jexpr +from enum import Enum +from typing import Any, Type, cast +from pathlib import Path +from dataclasses_json import DataClassJsonMixin, config +from dataclasses import dataclass, field + +from . import jexpr, compat, utils _logger = logging.getLogger(__name__) Props = dict[str, Any] -class Type(Enum): +class Kind(Enum): UNKNOWN = "unknown" PROJECT = "project" TARGET = "target" @@ -20,115 +23,46 @@ class Type(Enum): EXE = "exe" -class Manifest: - id: str = "" - type: Type = Type.UNKNOWN - path: str = "" +@dataclass +class Manifest(DataClassJsonMixin): + id: str + type: Kind = field(default=Kind.UNKNOWN) + path: str = field(default="") - def __init__( - self, - json: jexpr.Json = None, - path: str = "", - strict: bool = True, - **kwargs: Any, - ): - if json is not None: - if "id" not in json: - raise RuntimeError("Missing id") + @staticmethod + def parse(path: Path, data: dict[str, Any]) -> "Manifest": + compat.ensureSupportedManifest(data, path) + kind = Kind(data["type"]) + del data["$schema"] + obj = KINDS[kind].from_dict(data) + obj.path = str(path) + return obj - self.id = json["id"] - - if "type" not in json and strict: - raise RuntimeError("Missing type") - - self.type = Type(json["type"]) - - self.path = path - elif strict: - raise RuntimeError("Missing json") - - for key in kwargs: - setattr(self, key, kwargs[key]) - - def toJson(self) -> jexpr.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})" + @staticmethod + def load(path: Path) -> "Manifest": + return Manifest.parse(path, jexpr.evalRead(path)) def dirname(self) -> str: return os.path.dirname(self.path) - -class Extern: - git: str = "" - tag: str = "" - - def __init__(self, json: jexpr.Json = None, strict: bool = True, **kwargs: Any): - if json is not None: - if "git" not in json and strict: - raise RuntimeError("Missing git") - - self.git = json["git"] - - if "tag" not 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) -> jexpr.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})" + def ensureType(self, t: Type[utils.T]) -> utils.T: + 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) +@dataclass +class Extern(DataClassJsonMixin): + git: str + tag: str + + +@dataclass class Project(Manifest): - description: str = "" - extern: dict[str, Extern] = {} - - def __init__( - self, - json: jexpr.Json = None, - path: str = "", - strict: bool = True, - **kwargs: Any, - ): - if json is not None: - if "description" not 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) -> jexpr.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})" + description: str = field(default="(No description)") + extern: dict[str, Extern] = field(default_factory=dict) @staticmethod def root() -> str | None: @@ -149,80 +83,21 @@ class Project(Manifest): os.chdir(path) -class Tool: - cmd: str = "" - args: list[str] = [] - files: list[str] = [] - - def __init__(self, json: jexpr.Json = None, strict: bool = True, **kwargs: Any): - if json is not None: - if "cmd" not in json and strict: - raise RuntimeError("Missing cmd") - - self.cmd = json.get("cmd", self.cmd) - - if "args" not in json and strict: - raise RuntimeError("Missing args") - - self.args = json.get("args", []) - - self.files = json.get("files", []) - elif strict: - raise RuntimeError("Missing json") - - for key in kwargs: - setattr(self, key, kwargs[key]) - - def toJson(self) -> jexpr.Json: - return {"cmd": self.cmd, "args": self.args, "files": self.files} - - def __str__(self): - return f"Tool(cmd={self.cmd}, args={self.args}, files={self.files})" - - def __repr__(self): - return f"Tool({self.cmd})" +@dataclass +class Tool(DataClassJsonMixin): + cmd: str = field(default="") + args: list[str] = field(default_factory=list) + files: list[str] = field(default_factory=list) Tools = dict[str, Tool] +@dataclass class Target(Manifest): - props: Props - tools: Tools - routing: dict[str, str] - - def __init__( - self, - json: jexpr.Json = None, - path: str = "", - strict: bool = True, - **kwargs: Any, - ): - if json is not None: - if "props" not in json and strict: - raise RuntimeError("Missing props") - - self.props = json["props"] - - if "tools" not in json and strict: - raise RuntimeError("Missing tools") - - 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) -> jexpr.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})" + props: Props = field(default_factory=dict) + tools: Tools = field(default_factory=dict) + routing: dict[str, str] = field(default_factory=dict) def route(self, componentSpec: str): return ( @@ -250,54 +125,15 @@ class Target(Manifest): return defines +@dataclass class Component(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: jexpr.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", [""]), - ) - ) - - super().__init__(json, path, strict, **kwargs) - - def toJson(self) -> jexpr.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})" + decription: str = field(default="(No description)") + props: Props = field(default_factory=dict) + tools: Tools = field(default_factory=dict) + enableIf: dict[str, list[Any]] = field(default_factory=dict) + requires: list[str] = field(default_factory=list) + provides: list[str] = field(default_factory=list) + subdirs: list[str] = field(default_factory=list) def isEnabled(self, target: Target) -> tuple[bool, str]: for k, v in self.enableIf.items(): @@ -316,3 +152,11 @@ class Component(Manifest): ) return True, "" + + +KINDS: dict[Kind, Type[Manifest]] = { + Kind.PROJECT: Project, + Kind.TARGET: Target, + Kind.LIB: Component, + Kind.EXE: Component, +} diff --git a/cutekit/utils.py b/cutekit/utils.py index 281afac..560e2b3 100644 --- a/cutekit/utils.py +++ b/cutekit/utils.py @@ -3,11 +3,11 @@ from typing import Any, TypeVar, cast, Optional, Union import json import hashlib -T = TypeVar('T') +T = TypeVar("T") -def uniq(l: list[str]) -> list[str]: - result: list[str] = [] +def uniq(l: list[T]) -> list[T]: + result: list[T] = [] for i in l: if i in result: result.remove(i) @@ -15,7 +15,9 @@ def uniq(l: list[str]) -> list[str]: 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 @@ -28,7 +30,7 @@ def hash(obj: Any, keys: list[str] = [], cls: Optional[type[json.JSONEncoder]] = 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 @@ -60,4 +62,6 @@ 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 + )