Added testing
Preparing profiles and dependencies, but added testing to separate them from regular targets and avoid the need for a 'testing' profile which could negatively impact configuration size and readability.
This commit is contained in:
@@ -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
|
||||
+44
-5
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import field
|
||||
|
||||
from logging import getLogger
|
||||
from logging import Logger
|
||||
from pathlib import Path
|
||||
|
||||
from ..models import Abi
|
||||
@@ -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)
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
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(
|
||||
|
||||
+86
-67
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
|
||||
+24
-14
@@ -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:
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
from ._build import build
|
||||
from ._commands import Command
|
||||
from ._prepare import prepare
|
||||
from ._test import test
|
||||
|
||||
+13
-93
@@ -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 ._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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1 +1,2 @@
|
||||
from ._execute import execute
|
||||
from ._execute import test
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
name: testing
|
||||
gpid: examples
|
||||
semv: 1.0.0
|
||||
|
||||
tests:
|
||||
- name: factorial_test
|
||||
type: program
|
||||
srcs:
|
||||
- ./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;
|
||||
}
|
||||
Reference in New Issue
Block a user