diff --git a/.editorconfig b/.editorconfig index c169ca5..5dac4f7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,3 +6,10 @@ indent_style = tab indent_size = 2 trim_trailing_whitespace = true insert_final_newline = true + +[*.yaml,chinookfile] +charset = utf-8 +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true \ No newline at end of file diff --git a/chinook/__main__.py b/chinook/__main__.py index a84ce38..1d4ce13 100644 --- a/chinook/__main__.py +++ b/chinook/__main__.py @@ -20,14 +20,42 @@ def _build(args: Namespace) -> None: try: projpath = Path(args.project) cfgpath = projpath / "chinookconfig" - return pipeline.build( - Config.create_default_or_load(cfgpath), - management.load(projpath) - ) + appcfg = Config.create_default_or_load(cfgpath) + projcfg = management.load(projpath) + result = pipeline.build(appcfg, projcfg, logger) + logger.info(f"[100%] Built {projcfg.cid}") + return result except Exception as err: logger.exception(str(err)) logger.critical("Failed to build project") +def _test(args: Namespace) -> None: + from chinook import management + from chinook import pipeline + + from chinook._config import Config + from chinook._logger import logger + + try: + projpath = Path(args.project) + cfgpath = projpath / "chinookconfig" + appcfg = Config.create_default_or_load(cfgpath) + projcfg = management.load(projpath) + result = pipeline.test(appcfg, projcfg, logger) + + passed = 0 + count = 0 + for item in result: + if item.action == "Testing": + count += 1 + if item.retcode == 0: + passed += 1 + + logger.info(f"Tested {projcfg.cid} [{passed}/{count}]") + except Exception as err: + logger.exception(str(err)) + logger.critical("Failed to test project") + def main() -> None: print("\n".join([ @@ -50,7 +78,18 @@ def main() -> None: nargs="?", default=".", type=str, - help="The path to the project to build", + help="The path to the project to build.", + metavar="PROJECT" + ) + + testcmd = commands.add_parser("test",help="Tests your Chinook project") + testcmd.set_defaults(func=_test) + testcmd.add_argument( + "project", + nargs="?", + default=".", + type=str, + help="The path to the project to test.", metavar="PROJECT" ) diff --git a/chinook/compiler/_compiler.py b/chinook/compiler/_compiler.py index 912d379..a29bcc9 100644 --- a/chinook/compiler/_compiler.py +++ b/chinook/compiler/_compiler.py @@ -1,7 +1,8 @@ from dataclasses import dataclass from dataclasses import field - -from pathlib import Path +from logging import getLogger +from logging import Logger +from pathlib import Path from ..models import Abi from ..models import Result @@ -13,6 +14,8 @@ from ._mingw import MingwDriver from ._msvc import MsvcDriver from ._options import CompilerOptions +_logger = getLogger("chinook") + @dataclass class Compiler: @@ -25,10 +28,11 @@ class Compiler: } tooling: Tooling + logger: Logger = field(default=_logger) _driver: CompilerDriver = field(init=False) def __post_init__(self) -> None: - self._driver = Compiler._driver_for(self.tooling) + self._driver = Compiler._driver_for(self.tooling, self.logger) def compile_program( self, @@ -60,8 +64,12 @@ class Compiler: return self._driver.compile_object(options, srcpath, dstPath) @classmethod - def _driver_for(cls, tooling: Tooling) -> CompilerDriver: + def _driver_for( + cls, + tooling: Tooling, + logger: Logger + ) -> CompilerDriver: from ..models import Triple triples = tooling.when or [Triple.current()] driver = cls.__COMPILER_MAPPING.get(triples[0].abi, GnucDriver) - return driver(tooling) + return driver(tooling, logger) diff --git a/chinook/compiler/_driver.py b/chinook/compiler/_driver.py index 43e3cd4..b29fde0 100644 --- a/chinook/compiler/_driver.py +++ b/chinook/compiler/_driver.py @@ -1,18 +1,26 @@ -from abc import ABC -from abc import abstractmethod - +from abc import ABC +from abc import abstractmethod +from logging import getLogger +from logging import Logger from pathlib import Path from ..models import Tooling from ..models import Result from ._options import CompilerOptions +_logger = getLogger("chinook") + class CompilerDriver(ABC): - def __init__(self, tooling: Tooling) -> None: + def __init__( + self, + tooling: Tooling, + logger: Logger = _logger + ) -> None: if tooling is None: raise ValueError self.tooling = tooling + self.logger = logger @abstractmethod def compile_program( diff --git a/chinook/compiler/_gnuc.py b/chinook/compiler/_gnuc.py index 7fe60aa..d7c8e7f 100644 --- a/chinook/compiler/_gnuc.py +++ b/chinook/compiler/_gnuc.py @@ -2,10 +2,7 @@ from pathlib import Path from ..models import Optimization from ..models import Result -from ..models import StdC -from ..models import StdCpp from ..shell import execute -from .._logger import logger from ._driver import CompilerDriver from ._options import CompilerOptions @@ -48,17 +45,30 @@ class GnucDriver(CompilerDriver): for directory in options.libdirs: args.append(f"-L{str(directory)}") for archive in options.archives: args.append(f"-l{archive}") cmdstr = ' '.join(args) - # logger.info(cmdstr) + # self.logger.info(cmdstr) - result: list[Result] = [None] - execute( - cmdstr, - lambda cmdstr, stdout, stderr: - self.handle_on_data(result, str(dstpath), cmdstr, stdout, stderr), - lambda cmdstr, stdout, stderr: - self.handle_on_error(result, str(dstpath), cmdstr, stdout, stderr) + result = Result( + success = False, + action = "Linked", + target = str(dstpath), + command = cmdstr, + retcode = -1 ) - return result[0] + + def handle_success(retcode: int, cmdstr: str, stdout: str) -> None: + result.success = True + result.command = cmdstr + result.retcode = retcode + result.diagnostics.append(stdout) + + def handle_failure(retcode: int, cmdstr: str, stderr: str) -> None: + result.success = False + result.command = cmdstr + result.retcode = retcode + result.diagnostics.append(stderr) + + execute(cmdstr, handle_success, handle_failure) + return result def compile_archive( self, @@ -70,15 +80,28 @@ class GnucDriver(CompilerDriver): args.append(str(obj)) cmdstr = ' '.join(args) - result: list[Result] = [None] - execute( - cmdstr, - lambda cmdstr, stdout, stderr: - self.handle_on_data(result, str(dstpath), cmdstr, stdout, stderr), - lambda cmdstr, stdout, stderr: - self.handle_on_error(result, str(dstpath), cmdstr, stdout, stderr) + result = Result( + success = False, + action = "Linked", + target = str(dstpath), + command = cmdstr, + retcode = -1 ) - return result[0] + + def handle_success(retcode: int, cmdstr: str, stdout: str) -> None: + result.success = True + result.command = cmdstr + result.retcode = retcode + result.diagnostics.append(stdout) + + def handle_failure(retcode: int, cmdstr: str, stderr: str) -> None: + result.success = False + result.command = cmdstr + result.retcode = retcode + result.diagnostics.append(stderr) + + execute(cmdstr, handle_success, handle_failure) + return result def compile_library( self, @@ -106,17 +129,30 @@ class GnucDriver(CompilerDriver): for directory in options.libdirs: args.append(f"-L{str(directory)}") for archive in options.archives: args.append(f"-l{archive}") cmdstr = ' '.join(args) - # logger.info(cmdstr) + # self.logger.info(cmdstr) - result: list[Result] = [None] - execute( - cmdstr, - lambda cmdstr, stdout, stderr: - self.handle_on_data(result, str(dstpath), cmdstr, stdout, stderr), - lambda cmdstr, stdout, stderr: - self.handle_on_error(result, str(dstpath), cmdstr, stdout, stderr) + result = Result( + success = False, + action = "Linked", + target = str(dstpath), + command = cmdstr, + retcode = -1 ) - return result[0] + + def handle_success(retcode: int, cmdstr: str, stdout: str) -> None: + result.success = True + result.command = cmdstr + result.retcode = retcode + result.diagnostics.append(stdout) + + def handle_failure(retcode: int, cmdstr: str, stderr: str) -> None: + result.success = False + result.command = cmdstr + result.retcode = retcode + result.diagnostics.append(stderr) + + execute(cmdstr, handle_success, handle_failure) + return result def compile_object( self, @@ -150,44 +186,27 @@ class GnucDriver(CompilerDriver): for archive in options.archives: args.append(f"-l{archive}") for define in options.defines: args.append(f"-D{define}") cmdstr = ' '.join(args) - # logger.info(cmdstr) + # self.logger.info(cmdstr) - result: list[Result] = [None] - execute( - cmdstr, - lambda cmdstr, stdout, stderr: - self.handle_on_data(result, str(dstpath), cmdstr, stdout, stderr), - lambda cmdstr, stdout, stderr: - self.handle_on_error(result, str(dstpath), cmdstr, stdout, stderr) - ) - return result[0] - - def handle_on_data( - self, - result: list[Result], - target: str, - cmdstr: str, - stdout: str, - stderr: str - ) -> None: - result[0] = Result( - success = True, - target = target, - command = cmdstr - ) - result[0].diagnostics.append(stdout) - - def handle_on_error( - self, - result: list[Result], - target: str, - cmdstr: str, - stdout: str, - stderr: str - ) -> None: - result[0] = Result( + result = Result( success = False, - target = target, - command = cmdstr + action = "Compiled", + target = str(dstpath), + command = cmdstr, + retcode = -1 ) - result[0].diagnostics.append(stderr) + + def handle_success(retcode: int, cmdstr: str, stdout: str) -> None: + result.success = True + result.command = cmdstr + result.retcode = retcode + result.diagnostics.append(stdout) + + def handle_failure(retcode: int, cmdstr: str, stderr: str) -> None: + result.success = False + result.command = cmdstr + result.retcode = retcode + result.diagnostics.append(stderr) + + execute(cmdstr, handle_success, handle_failure) + return result diff --git a/chinook/management/__init__.py b/chinook/management/__init__.py index 27c8a5f..e6038ef 100644 --- a/chinook/management/__init__.py +++ b/chinook/management/__init__.py @@ -2,7 +2,6 @@ from typing import Optional from ..models import Project from ..models import Target -from ._clone import clone from ._except import NoProjectFoundError from ._latest import latest from ._load import load diff --git a/chinook/management/_load.py b/chinook/management/_load.py index 4336ddf..c22a687 100644 --- a/chinook/management/_load.py +++ b/chinook/management/_load.py @@ -32,6 +32,7 @@ def load(path: Path) -> Project: project = Project.from_dict(yamlobj) project.origination = Origination(path) project.destination = Destination(path) + _collect_profiles(project) _collect_required(project) _collect_targets(project) _flatten_targets(project) @@ -39,6 +40,12 @@ def load(path: Path) -> Project: except Exception as rethrowme: raise rethrowme +def _collect_profiles(project: Project) -> None: + # TODO: Determine what profile is supposed to be active and load the targets + # so that they will be compiled. + for profile in project.profiles: + ... + def _collect_required(project: Project) -> None: for required in project.requires: _clone_then_load_required(required) diff --git a/chinook/models/_dependency.py b/chinook/models/_dependency.py index 8d3d819..0b24eea 100644 --- a/chinook/models/_dependency.py +++ b/chinook/models/_dependency.py @@ -58,8 +58,12 @@ class Dependency(yaml.YAMLError): return cls(**data) @classmethod - def from_list(cls, data: list[str]) -> list[Self]: - return [cls.from_dict(v) for v in data] + def from_list(cls, data: list[dict | Self]) -> list[Self]: + return [ + v if isinstance(v, Dependency) \ + else cls.from_dict(v) + for v in data + ] @classmethod def from_yaml( diff --git a/chinook/models/_profile.py b/chinook/models/_profile.py new file mode 100644 index 0000000..c6ddfab --- /dev/null +++ b/chinook/models/_profile.py @@ -0,0 +1,88 @@ +import yaml + +from dataclasses import dataclass +from dataclasses import field + +from typing import ClassVar +from typing import Self + +from ._triple import Triple +from ._target import Target + + +@dataclass +class Profile(yaml.YAMLObject): + yaml_dumper: ClassVar[type[yaml.SafeDumper]] = yaml.SafeDumper + yaml_loader: ClassVar[type[yaml.SafeLoader]] = yaml.SafeLoader + yaml_tag: ClassVar[str] = u"!profile" + + name: str + when: list[Triple] = field(default_factory=list) + targets: list[Target] = field(default_factory=list) + + @property + def native(self) -> bool: + return not self.when or any(t.native for t in self.when) + + @classmethod + def to_dict(cls, data: Self) -> dict: + return { + "name": data.name, + "when": Triple.to_list(data.when), + "targets": Target.to_list(data.targets) + } + + @classmethod + def to_list(cls, data: list[Self]) -> list[dict]: + return [cls.to_dict(v) for v in data] + + @classmethod + def to_yaml( + cls, + dumper: yaml.SafeDumper, + data: yaml.YAMLObject + ) -> yaml.Node: + if not isinstance(data, Profile): + raise ValueError + return dumper.represent_mapping( + "tag:yaml.org,2002:map", + cls.to_dict(data) + ) + + @classmethod + def from_dict(cls, data: dict) -> Self: + def bad_attr(name: str, obj: dict) -> None: + raise ArithmeticError(name=name,obj=obj) + + return cls(**{ + "name": data.get("name") or bad_attr("name", data), + "when": Triple.from_list(data.get("when", [])), + "targets": Target.from_list(data.get("targets", [])) + }) + + @classmethod + def from_list(cls, data: list[dict | Self]) -> list[Self]: + return [ + v if isinstance(v, Profile) \ + else cls.from_dict(v) + for v in data + ] + + @classmethod + def from_yaml( + cls, + loader: yaml.SafeLoader, + node: yaml.Node + ) -> Self: + return cls.from_dict(loader.construct_mapping(node, deep=True)) + + +Profile.yaml_dumper.add_representer( + Profile.yaml_tag, + Profile.to_yaml +) + +Profile.yaml_loader.add_constructor( + Profile.yaml_tag, + Profile.from_yaml +) diff --git a/chinook/models/_project.py b/chinook/models/_project.py index aab98e7..5d30ce7 100644 --- a/chinook/models/_project.py +++ b/chinook/models/_project.py @@ -9,6 +9,7 @@ from ._dependency import Dependency from ._destination import Destination from ._identity import Identity from ._origination import Origination +from ._profile import Profile from ._target import Target @@ -18,8 +19,10 @@ class Project(Identity): yaml_loader: ClassVar[type[yaml.SafeLoader]] = yaml.SafeLoader yaml_tag: ClassVar[str] = u"!project" + profiles: list[Profile] = field(default_factory=list) requires: list[Dependency] = field(default_factory=list) targets: list[Target] = field(default_factory=list) + tests: list[Target] = field(default_factory=list) exports: list[str] = field(default_factory=list) origination: Origination = field(default=Origination.cwd()) @@ -28,8 +31,10 @@ class Project(Identity): @classmethod def to_dict(cls, data: Self) -> dict: return { + "profiles": Profile.to_list(data.profiles), "requires": Dependency.to_list(data.requires), "targets": Target.to_list(data.targets), + "tests": Target.to_list(data.targets), "exports": data.exports } @@ -53,8 +58,10 @@ class Project(Identity): "name": data.get("name") or bad_attr("name", data), "gpid": data.get("gpid") or bad_attr("gpid", data), "semv": data.get("semv") or bad_attr("semv", data), + "profiles": Profile.from_list(data.get("profiles", [])), "requires": Dependency.from_list(data.get("requires", [])), "targets": Target.from_list(data.get("targets", [])), + "tests": Target.from_list(data.get("tests", [])), "exports": data.get("exports", []) }) @@ -67,3 +74,13 @@ class Project(Identity): return cls.from_dict( loader.construct_mapping(node, deep=True) ) + +Project.yaml_dumper.add_representer( + Project.yaml_tag, + Project.to_yaml +) + +Project.yaml_loader.add_constructor( + Project.yaml_tag, + Project.from_yaml +) diff --git a/chinook/models/_result.py b/chinook/models/_result.py index 93b578c..a4a799b 100644 --- a/chinook/models/_result.py +++ b/chinook/models/_result.py @@ -5,6 +5,8 @@ from dataclasses import field @dataclass class Result: success: bool + action: str target: str command: str + retcode: int diagnostics: list[str] = field(default_factory=list) diff --git a/chinook/models/_tooling.py b/chinook/models/_tooling.py index 1808861..43179e5 100644 --- a/chinook/models/_tooling.py +++ b/chinook/models/_tooling.py @@ -54,18 +54,14 @@ class Tooling(yaml.YAMLObject): def from_dict(cls, data: dict) -> Self: return cls(**{ **data, - "when": [ - v if isinstance(v, Triple) \ - else Triple.from_string(v) - for v in data.get("when", []) - ] + "when": Triple.from_list(data.get("when", [])) }) @classmethod - def from_list(cls, data: list[dict]) -> list[Self]: + def from_list(cls, data: list[dict | Self]) -> list[Self]: return [ v if isinstance(v, Tooling) \ - else Tooling.from_dict(v) + else cls.from_dict(v) for v in data ] diff --git a/chinook/models/_triple.py b/chinook/models/_triple.py index 5667e8a..19da0f6 100644 --- a/chinook/models/_triple.py +++ b/chinook/models/_triple.py @@ -24,7 +24,7 @@ class Arch(enum.StrEnum): S390X = "s390x" WASM32 = "wasm32" WASM64 = "wasm64" - UNKNOWN = "unknown" + ANY = "any" class Sys(enum.StrEnum): LINUX = "linux" @@ -36,8 +36,7 @@ class Sys(enum.StrEnum): ANDROID = "android" IOS = "ios" WASI = "wasi" - NONE = "none" - UNKNOWN = "unknown" + ANY = "any" class Abi(enum.StrEnum): GNU = "gnu" @@ -45,8 +44,7 @@ class Abi(enum.StrEnum): MINGW = "mingw" ANDROID = "android" ELF = "elf" - NONE = "none" - UNKNOWN = "unknown" + ANY = "any" @dataclass @@ -91,9 +89,9 @@ class Triple(yaml.YAMLObject): def native(self) -> bool: host = Triple.current() return ( - (self.arch == Arch.UNKNOWN or self.arch == host.arch) and - (self.sys == Sys.UNKNOWN or self.sys == host.sys) and - (self.abi == Abi.UNKNOWN or self.abi == host.abi) + (self.arch == Arch.ANY or self.arch == host.arch) and + (self.sys == Sys.ANY or self.sys == host.sys) and + (self.abi == Abi.ANY or self.abi == host.abi) ) def __str__(self) -> str: @@ -111,6 +109,10 @@ class Triple(yaml.YAMLObject): def to_string(cls, triple: Self) -> str: return str(triple) + @classmethod + def to_list(cls, data: list[Self]) -> list[str]: + return [cls.to_string(v) for v in data] + @classmethod def to_yaml( cls, @@ -141,11 +143,19 @@ class Triple(yaml.YAMLObject): return fallback return cls( - arch = _coerce(Arch, parts[0].lower(), Arch.UNKNOWN), - sys = _coerce(Sys, parts[1].lower(), Sys.UNKNOWN), - abi = _coerce(Abi, parts[2].lower(), Abi.UNKNOWN) + arch = _coerce(Arch, parts[0].lower(), Arch.ANY), + sys = _coerce(Sys, parts[1].lower(), Sys.ANY), + abi = _coerce(Abi, parts[2].lower(), Abi.ANY) ) + @classmethod + def from_list(cls, data: list[str | Self]) -> list[Self]: + return [ + v if isinstance(v, Triple) \ + else cls.from_string(v) + for v in data + ] + @classmethod def from_yaml( cls, @@ -157,18 +167,18 @@ class Triple(yaml.YAMLObject): @classmethod def _select_arch(cls) -> Arch: curr = platform.machine().lower() - return cls.__arch_map.get(curr, Arch.UNKNOWN) + return cls.__arch_map.get(curr, Arch.ANY) @classmethod def _select_sys(cls) -> Sys: curr = platform.system().lower() - return cls.__sys_map.get(curr, Sys.UNKNOWN) \ + return cls.__sys_map.get(curr, Sys.ANY) \ if system.platform != "android" \ else Sys.ANDROID @classmethod def _select_abi(cls, arch: Arch, sys: Sys) -> Abi: - res = Abi.UNKNOWN + res = Abi.ANY if sys == Sys.WINDOWS: res = Abi.MSVC elif sys == Sys.ANDROID: diff --git a/chinook/pipeline/__init__.py b/chinook/pipeline/__init__.py index 0800897..d6fa14b 100644 --- a/chinook/pipeline/__init__.py +++ b/chinook/pipeline/__init__.py @@ -1 +1,4 @@ -from ._build import build +from ._build import build +from ._commands import Command +from ._prepare import prepare +from ._test import test diff --git a/chinook/pipeline/_build.py b/chinook/pipeline/_build.py index ce3a695..684c07f 100644 --- a/chinook/pipeline/_build.py +++ b/chinook/pipeline/_build.py @@ -1,106 +1,26 @@ -from pathlib import Path +from logging import getLogger +from logging import Logger -from ..compiler import Compiler -from ..compiler import CompilerOptions -from ..management import latest -from ..management import find_target_by_name -from ..management import find_project_by_target -from ..models import Access -from ..models import Accessor -from ..models import Output -from ..models import Project -from ..models import Result -from ..models import Target -from .._config import Config -from .._logger import logger -from ._commands import Command -from ._commands import CompileArchive -from ._commands import CompileDynlib -from ._commands import CompileObject -from ._commands import CompileProgram -from ._except import NoTargetsFoundError +from ..models import Project +from ..models import Result +from .._config import Config +from ._commands import Command +from ._except import NoTargetsFoundError +from ._execute import execute +from ._prepare import prepare + +_logger = getLogger("chinook") -def build(appcfg: Config, project: Project) -> list[Result]: +def build( + appcfg: Config, + project: Project, + logger: Logger = _logger +) -> list[Result]: if len(project.targets) == 0: raise NoTargetsFoundError(project) - # Put together list of commands and then execute them. commands: list[Command] = [] - results: list[Result] = [] for target in project.targets: - _prepare_build_commands(appcfg, project, target, commands) - - executed: int = 0 - cmdcount: int = len(commands) - for command in commands: - result = command.execute() - results.append(result) - percentage = (executed / cmdcount) - if result.success: - logger.info(f"[{percentage:.0%}] Compiled {result.target}") - executed += 1 - else: - logger.error(f"[{percentage:.0%}] Failed {result.target}") - for diagnostic in result.diagnostics: - logger.error(f"Because {diagnostic}") - return - logger.info(f"[100%] Built {project.cid}") - - -def _prepare_build_commands( - appcfg: Config, - project: Project, - target: Target, - commands: list[Command] -) -> None: - objects: list[Path] = [] - - # TODO: clean this up. - compiler = Compiler(appcfg.native_tooling) - defopts = CompilerOptions() - - defopts.pic = target.type == Output.DLL - defopts.incdirs.update([v.value for v in target.incs.includes]) - defopts.defines.update([v.value for v in target.defs]) - defopts.flags.update([v.value for v in target.opts]) - for file in target.srcs: - srcfile = project.origination.src(file.value) - dstfile = project.destination.obj(file.value) - if latest(srcfile, dstfile): - continue - commands.append(CompileObject(compiler, defopts, srcfile, dstfile)) - objects.append(dstfile) - - defopts.objfiles.update(objects) - del objects - - defopts.pic = False - defopts.defines.clear() - if len(target.libs) > 0: - for library in target.libs: - libtgt = find_target_by_name(library.value) - libprj = find_project_by_target(libtgt.name) - if libtgt.type == Output.LIB: - defopts.libdirs.add(libprj.destination.libpath) - defopts.archives.add(library.value) - elif libtgt.type == Output.DLL: - defopts.objfiles.add(libprj.destination.dll(library.value)) - - if len(defopts.objfiles) == 0: - return - - command: Command - match target.type: - case Output.EXE: - outfile = project.destination.bin(target.name) - command = CompileProgram(compiler, defopts, outfile) - case Output.LIB: - outfile = project.destination.lib(target.name) - command = CompileArchive(compiler, defopts, outfile) - case Output.DLL: - outfile = project.destination.dll(target.name) - command = CompileDynlib(compiler, defopts, outfile) - case Output.OBJ: - assert False, "Not Implemented" - commands.append(command) + commands.extend(prepare(appcfg, project, target, logger)) + return execute(commands, "Building") diff --git a/chinook/pipeline/_commands.py b/chinook/pipeline/_commands.py index 8d1af46..e187183 100644 --- a/chinook/pipeline/_commands.py +++ b/chinook/pipeline/_commands.py @@ -2,33 +2,82 @@ from abc import ABC from abc import abstractmethod from copy import deepcopy from dataclasses import dataclass +from logging import getLogger +from logging import Logger from pathlib import Path from typing import override from ..compiler import Compiler from ..compiler import CompilerOptions from ..models import Result +from ..shell import test + +_logger = getLogger("chinook") @dataclass class Command(ABC): compiler: Compiler options: CompilerOptions + logger: Logger def __post_init__(self) -> None: + if self.logger == None: + self.logger = _logger self.options = deepcopy(self.options) + def execute( + self, + percentage: float, + reason: str + ) -> Result | None: + if reason != "Testing": + return self._execute_building(percentage) + else: + return self._execute_testing() + @abstractmethod - def execute(self) -> Result: + def _execute(self) -> Result: ... + def _execute_building(self, percentage: float) -> Result | None: + message = f"[{percentage:.0%}] " + try: + result = self._execute() + if result.success: + message += f"{result.action} {result.target}" + self.logger.info(message) + else: + message += f"Failed {result.target} because " + for index in range(len(result.diagnostics) - 1): + message += f"{result.diagnostics[index]} " + message += result.diagnostics[-1] + self.logger.error(message) + return result + except Exception as error: + message += f"Failed {result.target} because {str(error)}" + self.logger.error(message) + return None + + def _execute_testing(self) -> Result | None: + try: + result = self._execute() + message = f"{result.action} {result.target}" + message += " - Passed" if result.retcode == 0 else " - Failed" + self.logger.info(message) + return result + except Exception as error: + message = f"Failed {result.target} because {str(error)}" + self.logger.error(message) + return None + @dataclass class CompileProgram(Command): exefile: Path @override - def execute(self) -> Result: + def _execute(self) -> Result: return self.compiler.compile_program( self.options, self.exefile @@ -39,7 +88,7 @@ class CompileArchive(Command): libfile: Path @override - def execute(self) -> Result: + def _execute(self) -> Result: return self.compiler.compile_archive( self.options, self.libfile @@ -50,7 +99,7 @@ class CompileDynlib(Command): dynfile: Path @override - def execute(self) -> Result: + def _execute(self) -> Result: return self.compiler.compile_library( self.options, self.dynfile @@ -62,9 +111,39 @@ class CompileObject(Command): dstfile: Path @override - def execute(self) -> Result: + def _execute(self) -> Result: return self.compiler.compile_object( self.options, self.srcfile, self.dstfile ) + +@dataclass +class ExecuteTest(Command): + testfile: Path + + @override + def _execute(self) -> Result: + abspath = Path.absolute(self.testfile) + result = Result( + success = False, + action = "Testing", + target = str(self.testfile), + command = "", + retcode = -1 + ) + + def handle_success(retcode: int, cmdstr: str, stdout: str) -> None: + result.success = True + result.command = cmdstr + result.retcode = retcode + result.diagnostics.append(stdout) + + def handle_failure(retcode: int, cmdstr: str, stderr: str) -> None: + result.success = False + result.command = cmdstr + result.retcode = retcode + result.diagnostics.append(stderr) + + test(abspath, handle_success, handle_failure) + return result diff --git a/chinook/pipeline/_except.py b/chinook/pipeline/_except.py index 8806578..ab17611 100644 --- a/chinook/pipeline/_except.py +++ b/chinook/pipeline/_except.py @@ -9,3 +9,12 @@ class NoTargetsFoundError(Exception): def __init__(self, project: Project) -> None: super().__init__(f"No targets for project: {project.cid}") self._project = project + +class NoTestsFoundError(Exception): + @property + def project(self) -> Project: + return self._project + + def __init__(self, project: Project) -> None: + super().__init__(f"No tests for project: {project.cid}") + self._project = project diff --git a/chinook/pipeline/_execute.py b/chinook/pipeline/_execute.py new file mode 100644 index 0000000..6b7aec7 --- /dev/null +++ b/chinook/pipeline/_execute.py @@ -0,0 +1,17 @@ +from ..models import Result +from ._commands import Command + + +def execute(commands: list[Command], reason: str) -> list[Result]: + results: list[Result] = [] + + cmdcount: int = len(commands) + if cmdcount == 1: + results.append(commands[0].execute(1, reason)) + return results + + executed: int = 0 + for command in commands: + results.append(command.execute(executed / cmdcount, reason)) + executed += 1 + return results diff --git a/chinook/pipeline/_prepare.py b/chinook/pipeline/_prepare.py new file mode 100644 index 0000000..07ae2f5 --- /dev/null +++ b/chinook/pipeline/_prepare.py @@ -0,0 +1,80 @@ +from logging import getLogger +from logging import Logger +from pathlib import Path + +from ..compiler import Compiler +from ..compiler import CompilerOptions +from ..management import latest +from ..management import find_target_by_name +from ..management import find_project_by_target +from ..models import Output +from ..models import Project +from ..models import Target +from .._config import Config +from ._commands import Command +from ._commands import CompileArchive +from ._commands import CompileDynlib +from ._commands import CompileObject +from ._commands import CompileProgram + +_logger = getLogger("chinook") + + +def prepare( + appcfg: Config, + project: Project, + target: Target, + logger: Logger = _logger +) -> list[Command]: + objects: list[Path] = [] + commands: list[Command] = [] + + # TODO: clean this up. + compiler = Compiler(appcfg.native_tooling, logger) + defopts = CompilerOptions() + + defopts.pic = target.type == Output.DLL + defopts.incdirs.update([v.value for v in target.incs.includes]) + defopts.defines.update([v.value for v in target.defs]) + defopts.flags.update([v.value for v in target.opts]) + for file in target.srcs: + srcfile = project.origination.src(file.value) + dstfile = project.destination.obj(file.value) + if latest(srcfile, dstfile): + continue + commands.append(CompileObject(compiler, defopts, logger, srcfile, dstfile)) + objects.append(dstfile) + + defopts.objfiles.update(objects) + del objects + + defopts.pic = False + defopts.defines.clear() + if len(target.libs) > 0: + for library in target.libs: + libtgt = find_target_by_name(library.value) + libprj = find_project_by_target(libtgt.name) + if libtgt.type == Output.LIB: + defopts.libdirs.add(libprj.destination.libpath) + defopts.archives.add(library.value) + elif libtgt.type == Output.DLL: + defopts.objfiles.add(libprj.destination.dll(library.value)) + + if len(defopts.objfiles) == 0: + return commands + + command: Command + match target.type: + case Output.EXE: + outfile = project.destination.bin(target.name) + command = CompileProgram(compiler, defopts, logger, outfile) + case Output.LIB: + outfile = project.destination.lib(target.name) + command = CompileArchive(compiler, defopts, logger, outfile) + case Output.DLL: + outfile = project.destination.dll(target.name) + command = CompileDynlib(compiler, defopts, logger, outfile) + case Output.OBJ: + assert False, "Not Implemented" + commands.append(command) + return commands diff --git a/chinook/pipeline/_test.py b/chinook/pipeline/_test.py new file mode 100644 index 0000000..21bf0e9 --- /dev/null +++ b/chinook/pipeline/_test.py @@ -0,0 +1,34 @@ +from logging import getLogger +from logging import Logger + +from ..models import Project +from ..models import Result +from .._config import Config +from ._commands import Command +from ._commands import ExecuteTest +from ._except import NoTestsFoundError +from ._execute import execute +from ._prepare import prepare + +_logger = getLogger("chinook") + + +def test( + appcfg: Config, + project: Project, + logger: Logger = _logger +) -> list[Result]: + if len(project.tests) == 0: + raise NoTestsFoundError(project) + + commands: list[Command] = [] + for target in project.tests: + commands.extend(prepare(appcfg, project, target, logger)) + results = execute(commands, "Building") + + commands.clear() + for target in project.tests: + testfile = project.destination.bin(target.name) + commands.append(ExecuteTest(None, None, logger, testfile)) + results.extend(execute(commands, "Testing")) + return results diff --git a/chinook/shell/__init__.py b/chinook/shell/__init__.py index 7e0e573..2f43a70 100644 --- a/chinook/shell/__init__.py +++ b/chinook/shell/__init__.py @@ -1 +1,2 @@ from ._execute import execute +from ._execute import test diff --git a/chinook/shell/_execute.py b/chinook/shell/_execute.py index f804aa5..00549c2 100644 --- a/chinook/shell/_execute.py +++ b/chinook/shell/_execute.py @@ -4,8 +4,8 @@ from subprocess import run from typing import Callable from typing import Optional -DataCallback = Callable[[str, str, str], None] -ErrorCallback = Callable[[str, str, str], None] +DataCallback = Callable[[int, str, str], None] +ErrorCallback = Callable[[int, str, str], None] def execute( @@ -24,14 +24,42 @@ def execute( if on_data != None: on_data( - command, - result.stdout.decode("utf-8"), - result.stderr.decode("utf-8") + result.returncode, + result.args, + result.stdout.decode("utf-8") ) except CalledProcessError as error: if on_error != None: on_error( + error.returncode, + error.cmd, + error.stderr.decode("utf-8") + ) + +def test( + command: str, + on_data: Optional[DataCallback], + on_error: Optional[ErrorCallback] +) -> None: + try: + result = run( + command, + shell = False, + check = False, + stdout = PIPE, + stderr = PIPE + ) + + if on_data != None: + on_data( + result.returncode, + result.args, + result.stdout.decode("utf-8") + ) + except CalledProcessError as error: + if on_error != None: + on_error( + error.returncode, error.cmd, - error.stdout.decode("utf-8"), error.stderr.decode("utf-8") ) diff --git a/examples/testing/chinookfile b/examples/testing/chinookfile new file mode 100644 index 0000000..0e37860 --- /dev/null +++ b/examples/testing/chinookfile @@ -0,0 +1,9 @@ +name: testing +gpid: examples +semv: 1.0.0 + +tests: +- name: factorial_test + type: program + srcs: + - ./entry.cpp \ No newline at end of file diff --git a/examples/testing/entry.cpp b/examples/testing/entry.cpp new file mode 100644 index 0000000..e9e69fe --- /dev/null +++ b/examples/testing/entry.cpp @@ -0,0 +1,13 @@ +int factorial(int start) { + if (start < 0) + return 0; + + auto result = 1; + for (auto index = 2; index < start; index++) + result *= index; + return result; +} + +int main(int argc, char** argv) { + return factorial(5) == 120; +} \ No newline at end of file