diff --git a/.gitignore b/.gitignore index db3ca83..ff527a5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .chinook/ .environ/ .pytest_cache/ +.vscode/ *.egg-info/ __pycache__/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f569a1..ae51977 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,13 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [0.0.1] - - -### Added -### Changed -### Fixed -### Removed +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.0.1] - + +### Added +### Changed +### Fixed +### Removed diff --git a/LICENSE.md b/LICENSE.md index b407907..db1cb99 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,7 +1,7 @@ -Copyright 2026 John Christman - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Copyright 2026 John Christman + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 2493519..ae1566c 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# Chinook +# Chinook diff --git a/chinook/__init__.py b/chinook/__init__.py index e69de29..d399eaf 100644 --- a/chinook/__init__.py +++ b/chinook/__init__.py @@ -0,0 +1 @@ +from ._logger import logger diff --git a/chinook/__main__.py b/chinook/__main__.py index 17e19ed..4456604 100644 --- a/chinook/__main__.py +++ b/chinook/__main__.py @@ -1,21 +1,38 @@ -import logging +import yaml -from chinook import pipeline from argparse import ArgumentParser +from argparse import Namespace from pathlib import Path from semver import Version +# Load application configurations. +cfgdir = Path.home() / ".config" / "chinook" / "remotes.yaml" +remotes = yaml.full_load(cfgdir) if Path.exists(cfgdir) else [] + + +def _build(args: Namespace) -> None: + from chinook import management + from chinook import pipeline + + from chinook._logger import logger + from chinook.models import Destination + from chinook.models import Origination + + try: + projpath = Path(args.project) + projcfg = management.load(projpath) + if projcfg != None: + root = projpath.parent + projcfg.origination = Origination(root) + projcfg.destination = Destination(root) + return pipeline.build(projcfg) + except Exception as err: + logger.exception(str(err)) + logger.critical("Failed to build project") + + def main() -> None: - formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s") - console = logging.StreamHandler() - console.setLevel(logging.DEBUG) - console.setFormatter(formatter) - - logger = logging.getLogger("chinook") - logger.addHandler(console) - logger.setLevel(logging.INFO) - # Parent parser to all subcommands. parser = ArgumentParser( prog = "chinook", @@ -24,12 +41,12 @@ def main() -> None: commands = parser.add_subparsers(required=True) buildcmd = commands.add_parser("build",help="Builds your Chinook project") - buildcmd.set_defaults(func=pipeline.build) + buildcmd.set_defaults(func=_build) buildcmd.add_argument( "project", nargs="?", default=".", - type=Path, + type=str, help="The path to the project to build", metavar="PROJECT" ) diff --git a/chinook/_config.py b/chinook/_config.py new file mode 100644 index 0000000..e190b42 --- /dev/null +++ b/chinook/_config.py @@ -0,0 +1,3 @@ + + +config = None diff --git a/chinook/_logger.py b/chinook/_logger.py new file mode 100644 index 0000000..a90483b --- /dev/null +++ b/chinook/_logger.py @@ -0,0 +1,10 @@ +import logging + +_formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s") +_console = logging.StreamHandler() +_console.setLevel(logging.DEBUG) +_console.setFormatter(_formatter) + +logger = logging.getLogger("chinook") +logger.addHandler(_console) +logger.setLevel(logging.INFO) diff --git a/chinook/compiler/gcc.py b/chinook/compiler/gcc.py new file mode 100644 index 0000000..7b66cc3 --- /dev/null +++ b/chinook/compiler/gcc.py @@ -0,0 +1,6 @@ +from ..models import Project +from ..models import Target + + +def compile(target: Target, project: Project) -> None: + ... \ No newline at end of file diff --git a/chinook/management/__init__.py b/chinook/management/__init__.py new file mode 100644 index 0000000..bc7fb65 --- /dev/null +++ b/chinook/management/__init__.py @@ -0,0 +1,6 @@ +from ._except import NoProjectFoundError +from ._except import NoPackageFoundError +from ._except import NoRemotesFoundError +from ._load import load +from ._shared import projects +from ._shared import targets diff --git a/chinook/management/_except.py b/chinook/management/_except.py new file mode 100644 index 0000000..bcce490 --- /dev/null +++ b/chinook/management/_except.py @@ -0,0 +1,30 @@ +from pathlib import Path +from ..models import Project + +class NoProjectFoundError(Exception): + @property + def path(self) -> Path: + return self._path + + def __init__(self, path: Path) -> None: + super().__init__(f"Project could not be found: {path}") + self._path = path + +class NoPackageFoundError(Exception): + @property + def path(self) -> Path: + return self._path + + def __init__(self, path: Path) -> None: + super().__init__(f"Package could not be found: {path}") + self._path = path + +class NoRemotesFoundError(Exception): + @property + def project(self) -> Project: + return self._project + + def __init__(self, project: Project) -> None: + super().__init__(f"Project remotes not present: {project.cid}") + self._project = project + diff --git a/chinook/management/_load.py b/chinook/management/_load.py new file mode 100644 index 0000000..8a9508d --- /dev/null +++ b/chinook/management/_load.py @@ -0,0 +1,77 @@ +import yaml + +from pathlib import Path +from ..models import Project +from ..remote import clone +from ..remote import RemoteDomainUnavailableError +from ..remote import RemoteAddressNonexistentError +from ..remote import PackageCloneFailedError +from .._config import config +from .._logger import logger +from ._except import NoProjectFoundError +from ._except import NoPackageFoundError +from ._except import NoRemotesFoundError +from ._shared import projects +from ._shared import targets + + +def load(path: Path) -> Project: + if path in projects: + return projects[path] + + fullpath = path / "chinookfile" + if not Path.exists(fullpath): + raise NoProjectFoundError(fullpath) + + try: + with open(fullpath, 'r') as file: + project = yaml.full_load(file.read()) + # projects[path] = config + _load_packages(project) + _collect_targets(project) + return project + except Exception as rethrowme: + raise rethrowme + + +def _load_packages(project: Project) -> None: + # Don't process packages if none are present. + if not hasattr(project, "packages"): + return + + # Packages present, but no remotes is not good :( + if len(project.packages) != 0 and \ + len(config.remotes) == 0: + raise NoRemotesFoundError(project) + + for pckg in project.packages: + try: + # Get available versions, find highest possible version supported. + + path = pckg.diskpath + if not Path.exists(path): + clone(config.remotes, pckg, path) + load(path) + except RemoteDomainUnavailableError as err: + # We tried to ping the remote to make sure we could download from it, and + # we got no response from the remote. + logger.warning(str(err)) + except RemoteAddressNonexistentError as err: + # We checked if the remote address existed and we found that it did not. + logger.warning(str(err)) + except PackageCloneFailedError as err: + # Remotes and pacakge exist, clone was uncessuccessful. + # Either clone command failed, or path did not exist afterwards. + logger.error(str(err)) + except NoProjectFoundError as err: + # Remotes and package exist, clone was successful, but chinookfile is not + # present in the package directory. + raise NoPackageFoundError(err.project) + except Exception as rethrowme: + raise rethrowme + +def _collect_targets(project: Project) -> None: + for target in project.targets: + name = f"{project.cid}#{target.name}" + if name not in targets: + targets[name] = target diff --git a/chinook/management/_shared.py b/chinook/management/_shared.py new file mode 100644 index 0000000..552b912 --- /dev/null +++ b/chinook/management/_shared.py @@ -0,0 +1,7 @@ +from ..models import Package +from ..models import Project +from ..models import Target + +packages: dict[str, Package] = {} +projects: dict[str, Project] = {} +targets: dict[str, Target] = {} diff --git a/chinook/models/__init__.py b/chinook/models/__init__.py index 1f6ad00..1a873b5 100644 --- a/chinook/models/__init__.py +++ b/chinook/models/__init__.py @@ -1 +1,13 @@ -from ._chinookfile import ChinookFile +from ._accessor import Accessor +from ._accessor import PublicAccessor +from ._accessor import ProtectedAccessor +from ._accessor import PrivateAccessor +from ._branch import Branch +from ._destination import Destination +from ._identity import Identity +from ._level import Level +from ._origination import Origination +from ._output import Output +from ._project import Project +from ._package import Package +from ._target import Target diff --git a/chinook/models/_accessor.py b/chinook/models/_accessor.py new file mode 100644 index 0000000..daa99c8 --- /dev/null +++ b/chinook/models/_accessor.py @@ -0,0 +1,63 @@ +import yaml + +from dataclasses import dataclass +from typing import Optional + +from ._level import Level + + +@dataclass +class Accessor: + level: Level + value: str + + def __hash__(self) -> int: + return hash((self.level, self.value)) + + @staticmethod + def from_list(value: Optional[list]) -> list['Accessor']: + return [Accessor.from_value(v) for v in value] if value != None else [] + + @staticmethod + def from_value(value: dict | str) -> 'Accessor': + if isinstance(value, str): + return Accessor(level = Level.PUBLIC, value = value) + if isinstance(value, dict): + return Accessor(**dict) + if isinstance(value, Accessor): + return value + raise ValueError + +class PublicAccessor(yaml.YAMLObject, Accessor): + yaml_tag = u"!public" + def __init__(self, value: str) -> None: + super().__init__(level = Level.PUBLIC, value = value) + +class ProtectedAccessor(yaml.YAMLObject, Accessor): + yaml_tag = u"!protected" + def __init__(self, value: str) -> None: + super().__init__(level = Level.PROTECTED, value = value) + +class PrivateAccessor(yaml.YAMLObject, Accessor): + yaml_tag = u"!private" + def __init__(self, value: str) -> None: + super().__init__(level = Level.PRIVATE, value = value) + + +yaml.add_constructor( + PublicAccessor.yaml_tag, + lambda loader, node: + PublicAccessor(loader.construct_scalar(node)) +) + +yaml.add_constructor( + ProtectedAccessor.yaml_tag, + lambda loader, node: + ProtectedAccessor(loader.construct_scalar(node)) +) + +yaml.add_constructor( + PrivateAccessor.yaml_tag, + lambda loader, node: + PrivateAccessor(loader.construct_scalar(node)) +) diff --git a/chinook/models/_activation.py b/chinook/models/_activation.py new file mode 100644 index 0000000..a2b7989 --- /dev/null +++ b/chinook/models/_activation.py @@ -0,0 +1,6 @@ +import yaml + + + +class Activation(yaml.YAMLObject): + pass diff --git a/chinook/models/_branch.py b/chinook/models/_branch.py new file mode 100644 index 0000000..7eeab22 --- /dev/null +++ b/chinook/models/_branch.py @@ -0,0 +1,34 @@ +import yaml + +from dataclasses import dataclass +from dataclasses import field + +from semantic_version import Version +from semantic_version import NpmSpec +from semantic_version import validate + +from typing import ClassVar +from typing import Optional + + +@dataclass +class Branch(yaml.YAMLObject): + yaml_tag: ClassVar[str] = u"!branch" + + value: str + _valid: Optional[bool] = field(init=False,default=None) + + @property + def as_version(self) -> Version: + if not self.is_version: + return Version(0, 0, 0, self.value) + return Version.parse(self.value) + + @property + def is_version(self) -> bool: + if self._valid == None: + self._valid = validate(self.value) + return self._valid + + def __str__(self) -> str: + return self.value diff --git a/chinook/models/_chinookfile.py b/chinook/models/_chinookfile.py deleted file mode 100644 index d098494..0000000 --- a/chinook/models/_chinookfile.py +++ /dev/null @@ -1,5 +0,0 @@ -from dataclasses import dataclass - -@dataclass -class ChinookFile: - pass diff --git a/chinook/models/_destination.py b/chinook/models/_destination.py new file mode 100644 index 0000000..27836b0 --- /dev/null +++ b/chinook/models/_destination.py @@ -0,0 +1,53 @@ +from pathlib import Path + + +# TODO: Support custom output file extension based on user +# or platform specification. +class Destination: + @property + def root(self) -> Path: + return self._root + + @property + def binpath(self) -> Path: + return self._binpath + + @property + def dllpath(self) -> Path: + return self._binpath + + @property + def libpath(self) -> Path: + return self._libpath + + @property + def objpath(self) -> Path: + return self._objpath + + def __init__(self, outpath: Path, mkdirs=True) -> None: + self._root = outpath / ".chinook" + self._binpath = self._root / "bin" + self._libpath = self._root / "lib" + self._objpath = self._root / "obj" + + if mkdirs: + self._root.mkdir(parents=True, exist_ok=True) + self._binpath.mkdir(parents=True, exist_ok=True) + self._libpath.mkdir(parents=True, exist_ok=True) + self._objpath.mkdir(parents=True, exist_ok=True) + + @staticmethod + def cwd() -> 'Destination': + return Destination(Path.cwd(), mkdirs=False) + + def bin(self, file: str) -> Path: + return self.binpath / file + + def dll(self, file: str) -> Path: + return self.dllpath / f"{file}.so" + + def lib(self, file: str) -> Path: + return self.libpath / f"lib{file}.a" + + def obj(self, file: Path) -> Path: + return self.objpath / f"{file}.o" diff --git a/chinook/models/_identity.py b/chinook/models/_identity.py new file mode 100644 index 0000000..d016824 --- /dev/null +++ b/chinook/models/_identity.py @@ -0,0 +1,36 @@ +import re +import yaml + +from dataclasses import dataclass +from ._branch import Branch + + +@dataclass +class Identity(yaml.YAMLObject): + name: str + gpid: str + semv: Branch + + @property + def cid(self) -> str: + return f"{self.gpid}/{self.name}@{self.semv}" + + @staticmethod + def from_cid(cid: str) -> dict[str, str]: + pattern = r"^(?P[^/]+)/(?P[^@]+)@(?P.+)$" + match = re.match(pattern, cid) + return match.groupdict() if match else {} + + @staticmethod + def from_fields(fields: dict) -> 'Identity': + if name:=fields.get("name") == None: + raise AttributeError(name="name",obj=fields) + if gpid:=fields.get("gpid") == None: + raise AttributeError(name="gpid",obj=fields) + if semv:=fields.get("semv") == None: + raise AttributeError(name="semv",obj=fields) + return Identity( + name=name, + gpid=gpid, + semv=Branch(semv) + ) \ No newline at end of file diff --git a/chinook/models/_level.py b/chinook/models/_level.py new file mode 100644 index 0000000..876129f --- /dev/null +++ b/chinook/models/_level.py @@ -0,0 +1,8 @@ +from enum import IntEnum +from enum import auto + + +class Level(IntEnum): + PUBLIC = auto() + PROTECTED = auto() + PRIVATE = auto() diff --git a/chinook/models/_origination.py b/chinook/models/_origination.py new file mode 100644 index 0000000..7b01bba --- /dev/null +++ b/chinook/models/_origination.py @@ -0,0 +1,19 @@ +from pathlib import Path + + +class Origination: + @property + def root(self) -> Path: + return self._root + + def __init__(self, inpath: Path) -> None: + self._root = inpath + self._srcpath = self._root / "source" + self._incpath = self._root / "include" + + @staticmethod + def cwd() -> 'Origination': + return Origination(Path.cwd()) + + def src(self, file: Path) -> Path: + return self._root / file diff --git a/chinook/models/_output.py b/chinook/models/_output.py new file mode 100644 index 0000000..5e0e227 --- /dev/null +++ b/chinook/models/_output.py @@ -0,0 +1,9 @@ +from enum import StrEnum + + +class Output(StrEnum): + EXE = "executable" + LIB = "archive" + DLL = "dynlib" + OBJ = "objfile" + INT = "interface" \ No newline at end of file diff --git a/chinook/models/_package.py b/chinook/models/_package.py new file mode 100644 index 0000000..bebca50 --- /dev/null +++ b/chinook/models/_package.py @@ -0,0 +1,59 @@ +import yaml + +from dataclasses import dataclass +from dataclasses import field + +from pathlib import Path +from semantic_version import NpmSpec + +from typing import ClassVar +from typing import Optional + +from ._identity import Identity + + +@dataclass +class Package(yaml.YAMLObject): + yaml_tag: ClassVar[str] = u"!package" + + name: str + gpid: str + spec: NpmSpec + tag: Optional[str] = field(init=False,default=None) + domain: Optional[str] = field(init=False,default=None) + + @property + def reponame(self) -> str: + return f"{self.gpid}/{self.name}" + + @property + def chinookid(self) -> str: + if self.tag == None: + raise ValueError + return f"{self.reponame}@{self.tag}" + + @property + def remote(self) -> str: + if self.domain == None: + raise ValueError + return f"https://{self.domain}" + + @property + def repository(self) -> str: + return f"{self.remote}/{self.reponame}" + + @property + def dirname(self) -> Path: + return self.chinookid + + @property + def diskpath(self) -> Path: + return Path.home() / ".chinook" / self.chinookid + + @staticmethod + def from_list(items: list) -> list['Package']: + return [Package.from_cid(item) for item in items] + + @staticmethod + def from_chinookid(cid: str) -> 'Package': + return Package(**Identity.from_cpid(cid)) diff --git a/chinook/models/_profile.py b/chinook/models/_profile.py new file mode 100644 index 0000000..107e6f7 --- /dev/null +++ b/chinook/models/_profile.py @@ -0,0 +1,13 @@ +import yaml + +from dataclasses import dataclass +from typing import ClassVar +from typing import Optional + + +@dataclass +class Profile(yaml.YAMLObject): + yaml_tag: ClassVar[str] = u"!profile" + + name: str + when: Optional[Triple] diff --git a/chinook/models/_project.py b/chinook/models/_project.py new file mode 100644 index 0000000..5270f46 --- /dev/null +++ b/chinook/models/_project.py @@ -0,0 +1,30 @@ +from dataclasses import dataclass +from dataclasses import field +from typing import ClassVar +from ._identity import Identity +from ._package import Package +from ._target import Target + + +@dataclass +class Project(Identity): + yaml_tag: ClassVar[str] = u"!project" + + packages: list[Package] = field(default_factory=list) + targets: list[Target] = field(default_factory=list) + options: dict[str, str] = field(default_factory=dict) + + @staticmethod + def from_fields(fields: dict) -> 'Project': + identity = Identity.from_fields(fields) + packages = fields.get("packages") or [] + if targets:=fields.get("targets") == None: + raise AttributeError(name="targets", obj=fields) + return Project( + name = identity.name, + gpid = identity.gpid, + semv = identity.semv, + packages = [Package.from_fields(item) for item in packages], + targets = [Target.from_fields(item) for item in targets], + options = {} + ) diff --git a/chinook/models/_target.py b/chinook/models/_target.py new file mode 100644 index 0000000..23a5b65 --- /dev/null +++ b/chinook/models/_target.py @@ -0,0 +1,29 @@ +import yaml + +from dataclasses import dataclass +from dataclasses import field +from typing import ClassVar + +from ._accessor import Accessor +from ._output import Output + + +@dataclass +class Target(yaml.YAMLObject): + yaml_tag: ClassVar[str] = u"!target" + + name: str + type: Output + options: set[Accessor] + sources: set[Accessor] + inherits: set[Accessor] + + archives: set[Accessor] = field(default=set) + dynlibs: set[Accessor] = field(default=set) + + def consolidate(self, parent: 'Target') -> None: + # NOTE: Do not need to append parents of parent. + self.options.update(parent.options) + self.sources.update(parent.sources) + self.archives.update(parent.archives) + self.dynlibs.update(parent.dynlibs) diff --git a/chinook/models/_toolchain.py b/chinook/models/_toolchain.py new file mode 100644 index 0000000..8cfcc2f --- /dev/null +++ b/chinook/models/_toolchain.py @@ -0,0 +1,19 @@ +import yaml + +from dataclasses import dataclass +from pathlib import Path +from typing import ClassVar + +from ._triple import Triple + + +@dataclass +class Toolchain(yaml.YAMLObject): + yaml_tag: ClassVar[str] = u"!toolchain" + + name: str + when: list[Triple] + compiler: Path + linker: Path + archiver: Path + debugger: Path diff --git a/chinook/models/_triple.py b/chinook/models/_triple.py new file mode 100644 index 0000000..04576b6 --- /dev/null +++ b/chinook/models/_triple.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass +from enum import Enum +from typing import ClassVar + +from ._activation import Activation + + +@dataclass +class Triple(Activation): + yaml_tag: ClassVar[str] = u"!triple" + + arch: str + vend: str + sys: str + abi: str + diff --git a/chinook/pipeline/__init__.py b/chinook/pipeline/__init__.py index acb421c..0800897 100644 --- a/chinook/pipeline/__init__.py +++ b/chinook/pipeline/__init__.py @@ -1,3 +1 @@ from ._build import build - -__all__ = [ build ] diff --git a/chinook/pipeline/_build.py b/chinook/pipeline/_build.py index 735df15..d6b5d97 100644 --- a/chinook/pipeline/_build.py +++ b/chinook/pipeline/_build.py @@ -1,25 +1,11 @@ -from ..models import ChinookFile +from ..models import Project +from .._logger import logger +from ._compile import compile +from ._except import NoTargetsFoundError -from pathlib import Path -from typing import overload - - -@overload -def build(path: Path) -> None: ... - -@overload -def build(file: ChinookFile) -> None: ... - - -def build(value) -> None: - if isinstance(value, Path): - return _build_from_path(value) - elif isinstance(value, ChinookFile): - return _build_from_file(value) - - -def _build_from_path(path: Path) -> None: - pass - -def _build_from_file(file: ChinookFile) -> None: - pass +def build(project: Project) -> None: + # TODO: Download, catalog, and build dependencies + # TODO: Build the current configuration + if len(project.targets) == 0: + raise NoTargetsFoundError(project) + logger.info(f"Building {project.cid}") diff --git a/chinook/pipeline/_compile.py b/chinook/pipeline/_compile.py new file mode 100644 index 0000000..d5c8275 --- /dev/null +++ b/chinook/pipeline/_compile.py @@ -0,0 +1,4 @@ +from ..models import Target + +def compile(target: Target) -> None: + ... \ No newline at end of file diff --git a/chinook/pipeline/_except.py b/chinook/pipeline/_except.py new file mode 100644 index 0000000..3303283 --- /dev/null +++ b/chinook/pipeline/_except.py @@ -0,0 +1,2 @@ +class NoTargetsFoundError(Exception): + pass \ No newline at end of file diff --git a/chinook/pipeline/_shared.py b/chinook/pipeline/_shared.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/chinook/pipeline/_shared.py @@ -0,0 +1 @@ + diff --git a/chinook/remote/__init__.py b/chinook/remote/__init__.py new file mode 100644 index 0000000..ab53318 --- /dev/null +++ b/chinook/remote/__init__.py @@ -0,0 +1,6 @@ +from ._clone import clone +from ._exists import exists +from ._except import RemoteDomainUnavailableError +from ._except import RemoteAddressNonexistentError +from ._except import PackageCloneFailedError +from ._ping import ping diff --git a/chinook/remote/_clone.py b/chinook/remote/_clone.py new file mode 100644 index 0000000..dce3390 --- /dev/null +++ b/chinook/remote/_clone.py @@ -0,0 +1,63 @@ +from pathlib import Path +from typing import overload +from ..models import Package +from ..shell import execute +from ._except import RemoteDomainUnavailableError +from ._except import RemoteAddressNonexistentError +from ._except import PackageCloneFailedError +from ._exists import exists +from ._ping import ping + + +@overload +def clone( + domains: list[str], + fspath: Path, + package: Package +) -> None: ... + +@overload +def clone( + domain: str, + fspath: Path, + pacakge: Package +) -> None: ... + + +def clone( + domain: str | list[str], + fspath: Path, + package: Package +) -> None: + if package.remote != None: + return _clone_from_domain(package.remote, fspath, package) + + if isinstance(domain, str): + return _clone_from_domain(domain, fspath, package) + + +def _clone_from_domain(package: Package) -> None: + if package.tag == None: + raise ValueError("No valid tag for package.") + + cloned = True + cmdstr = "git clone --branch {} {} {}" + def _handle_error(_: Exception) -> None: + nonlocal cloned + cloned = False + + if not ping(package.remote): + raise RemoteDomainUnavailableError(package.remote) + + if not exists(package.repository): + raise RemoteAddressNonexistentError(package.repository) + + cmd = cmdstr.format( + package.tag, + package.repository, + package.diskpath + ) + + execute(command = cmd, on_error = _handle_error) + if not cloned or not Path.exists(package.diskpath): + raise PackageCloneFailedError(package.cid) diff --git a/chinook/remote/_except.py b/chinook/remote/_except.py new file mode 100644 index 0000000..99050d6 --- /dev/null +++ b/chinook/remote/_except.py @@ -0,0 +1,11 @@ +class RemoteDomainUnavailableError(Exception): + def __init__(self, domain: str) -> None: + super().__init__(f"Domain is unavailable: {domain}") + +class RemoteAddressNonexistentError(Exception): + def __init__(self, address: str) -> None: + super().__init__(f"Address does not exist: {address}") + +class PackageCloneFailedError(Exception): + def __init__(self, pckgid: str) -> None: + super().__init__(f"Failed to clone package: {pckgid}") diff --git a/chinook/remote/_exists.py b/chinook/remote/_exists.py new file mode 100644 index 0000000..c3d8259 --- /dev/null +++ b/chinook/remote/_exists.py @@ -0,0 +1,11 @@ +from ..shell import execute + + +def exists(addr: str) -> bool: + repo_exists = True + command_str = "git ls-remote --exit-code -h {}" + def _handle_error(error: Exception) -> None: + nonlocal repo_exists + repo_exists = False + execute(command_str.format(addr), _handle_error) + return repo_exists diff --git a/chinook/remote/_ping.py b/chinook/remote/_ping.py new file mode 100644 index 0000000..ba2cb94 --- /dev/null +++ b/chinook/remote/_ping.py @@ -0,0 +1,11 @@ +from ..shell import execute + + +def ping(addr: str) -> bool: + can_ping = True + cmd_str = "ping {} -W 1" + def _handle_error(error: Exception) -> None: + nonlocal can_ping + can_ping = False + execute(cmd_str.format(addr), _handle_error) + return can_ping diff --git a/chinook/shell/__init__.py b/chinook/shell/__init__.py new file mode 100644 index 0000000..7e0e573 --- /dev/null +++ b/chinook/shell/__init__.py @@ -0,0 +1 @@ +from ._execute import execute diff --git a/chinook/shell/_execute.py b/chinook/shell/_execute.py new file mode 100644 index 0000000..76f87ef --- /dev/null +++ b/chinook/shell/_execute.py @@ -0,0 +1,29 @@ +from subprocess import CalledProcessError +from subprocess import PIPE +from subprocess import run + +from typing import Callable +from typing import Optional +DataCallback = Callable[[str, str, str], None] +ErrorCallback = Callable[[Exception], None] + + +def execute( + command: str, + on_data: Optional[DataCallback], + on_error: Optional[ErrorCallback] +) -> None: + try: + result = run( + command, + shell = True, + check = True, + stdout = PIPE, + stderr = PIPE + ) + + if on_data != None: + on_data(command, result.stdout, result.stderr) + except CalledProcessError as error: + if on_error != None: + on_error(error) diff --git a/chinook/triple/__init__.py b/chinook/triple/__init__.py new file mode 100644 index 0000000..4cfee4a --- /dev/null +++ b/chinook/triple/__init__.py @@ -0,0 +1,5 @@ +from ._arch import Architecture +from ._environ import Environment +from ._system import System +from ._system import SystemData +from ._vendor import Vendor diff --git a/chinook/triple/_arch.py b/chinook/triple/_arch.py new file mode 100644 index 0000000..d42539a --- /dev/null +++ b/chinook/triple/_arch.py @@ -0,0 +1,49 @@ +from dataclasses import dataclass +from enum import Enum +from enum import auto +from enum import unique + + +@dataclass +class ArchDataMixin: + pass + + + +class Architecture(Enum): + UNKNOWN = auto() + ARM = None # CUSTOM + AMDGCN = auto() + AARCH64 = None # CUSTOM + ASMJS = auto() + AVR = auto() + BPFEB = auto() + BPFEL = auto() + HEXAGON = auto() + X86_32 = None # CUSTOM + M68K = auto() + LOONGARCH64 = auto() + MIPS32 = None # CUSTOM + MIPS64 = None # CUSTOM + MSP430 = auto() + NVPTX64 = auto() + PULLEY32 = auto() + PULLEY64 = auto() + PULLEY32BE = auto() + PULLEY64BE = auto() + POWERPC = auto() + POWERPC64LE = auto() + RISCV32 = None # CUSTOM + RISCV64 = None # CUSTOM + S390X = auto() + SPARC = auto() + SPARC64 = auto() + SPARCV9 = auto() + WASM32 = auto() + WASM64 = auto() + X86_64 = auto() + X86_64H = auto() + XTENSA = auto() + CLEVER = None # CUSTOM + ZKASM = auto() + Z80 = None # CUSTOM diff --git a/chinook/triple/_environ.py b/chinook/triple/_environ.py new file mode 100644 index 0000000..644f9d0 --- /dev/null +++ b/chinook/triple/_environ.py @@ -0,0 +1,39 @@ +from enum import StrEnum +from enum import auto + + +class Environment(StrEnum): + UNKNOWN = auto() + AMDGIZ = auto() + ANDROID = auto() + ANDROIDEABI = auto() + EABI = auto() + EABIHF = auto() + GNU = auto() + GNUABI64 = auto() + GNUEABI = auto() + GNUSPE = auto() + GNUX32 = auto() + GNU_ILP32 = auto() + GNULLVM = auto() + HERMITKERNEL = auto() + HURDKERNEL = auto() + LINUXKERNEL = auto() + MACABI = auto() + MUSL = auto() + MUSLEABI = auto() + MUSLEABIHF = auto() + MUSLABI64 = auto() + MSVC = auto() + NEWLIB = auto() + NONE = auto() + KERNEL = auto() + UCLIBC = auto() + UCLIBCEABI = auto() + UCLIBCEABIHF = auto() + SGX = auto() + SIM = auto() + SOFTFLOAT = auto() + SPE = auto() + THREADS = auto() + OHOS = auto() diff --git a/chinook/triple/_system.py b/chinook/triple/_system.py new file mode 100644 index 0000000..b1d2b6e --- /dev/null +++ b/chinook/triple/_system.py @@ -0,0 +1,66 @@ +from dataclasses import dataclass +from enum import Enum +from enum import auto +from semver import Version +from typing import Any +from typing import Optional + + +@dataclass +class SystemData: + name: str + semv: Optional[Version] + + +class _SystemDataMixin(Enum): + @staticmethod + def _generate_next_value_( + name: str, + start: int, + count: int, + prevs: list[Any] + ) -> SystemData: + return SystemData(name, None) + + +class System(_SystemDataMixin): + UNKNOWN = auto() + AIX = auto() + AMDHSA = auto() + BITRIG = auto() + CLOUDABI = auto() + CUDA = auto() + CYGWIN = auto() + DARWIN = auto() + DRAGONFLY = auto() + EMSCRIPTEN = auto() + ESPIDF = auto() + FREEBSD = auto() + FUCHSIA = auto() + HAIKU = auto() + HERMIT = auto() + HORIZON = auto() + HURD = auto() + ILLUMOS = auto() + IOS = auto() + L4RE = auto() + LINUX = auto() + MACOSX = auto() + NEBULET = auto() + NETBSD = auto() + NONE = auto() + OPENBSD = auto() + PSP = auto() + REDOX = auto() + SOLARIS = auto() + SOLIDASP3 = auto() + TVOS = auto() + UEFI = auto() + VISIONOS = auto() + VXWORKS = auto() + WASI = auto() + WASIP1 = auto() + WASIP2 = auto() + WATCHOS = auto() + WINDOWS = auto() + XROS = auto() diff --git a/chinook/triple/_vendor.py b/chinook/triple/_vendor.py new file mode 100644 index 0000000..a6d5421 --- /dev/null +++ b/chinook/triple/_vendor.py @@ -0,0 +1,19 @@ +from enum import StrEnum +from enum import auto + + +class Vendor(StrEnum): + UNKNOWN = auto() + AMD = auto() + APPLE = auto() + ESPRESSIF = auto() + FORTANIX = auto() + IBM = auto() + KMC = auto() + NINTENDO = auto() + NVIDIA = auto() + PC = auto() + RUMPRUN = auto() + SUN = auto() + UWP = auto() + WRS = auto() diff --git a/example/executable/chinookfile b/example/executable/chinookfile new file mode 100644 index 0000000..7f14934 --- /dev/null +++ b/example/executable/chinookfile @@ -0,0 +1,9 @@ +name: executable +gpid: examples +semv: 1.0.0 + +targets: +- name: example + type: executable + srcs: + - ./entry.cpp diff --git a/example/executable/entry.cpp b/example/executable/entry.cpp new file mode 100644 index 0000000..e12d14c --- /dev/null +++ b/example/executable/entry.cpp @@ -0,0 +1,11 @@ +#include +#if __cplusplus >= 202302L +# define print(x) std::print(#x) +#else // Old C++ version. +# define print(x) std::cout << #x << std::endl +#endif // Check C++ version. + + +int main() { + print("Hello, World!"); +} diff --git a/pyproject.toml b/pyproject.toml index 45fb7a9..d51b1ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "chinook" version = "1.0.0-alpha" description = "Opinionated build tool for C/C++." -dependencies = ["PyYAML", "semver"] +dependencies = ["PyYAML", "semantic-version"] [project.scripts] chinook = "chinook.__main__:main"