diff --git a/chinook/compiler/_gnuc.py b/chinook/compiler/_gnuc.py index 7652a51..1b78943 100644 --- a/chinook/compiler/_gnuc.py +++ b/chinook/compiler/_gnuc.py @@ -174,7 +174,8 @@ class GnucDriver(CompilerDriver): args = [ f"{compiler}", f"-c {str(srcpath)}", - f"-o {str(dstpath)}" + f"-o {str(dstpath)}", + f"-MMD -MP -MF {str(dstpath.with_suffix('.d'))}" ] if options.pic: args.append("-fPIC") diff --git a/chinook/management/__init__.py b/chinook/management/__init__.py index e6038ef..00610ae 100644 --- a/chinook/management/__init__.py +++ b/chinook/management/__init__.py @@ -4,6 +4,8 @@ from ..models import Project from ..models import Target from ._except import NoProjectFoundError from ._latest import latest +from ._latest import read_depfile +from ._latest import stale from ._load import load diff --git a/chinook/management/_latest.py b/chinook/management/_latest.py index a59580e..416c5f1 100644 --- a/chinook/management/_latest.py +++ b/chinook/management/_latest.py @@ -1,10 +1,37 @@ +import re + from pathlib import Path +from typing import Iterable def latest(src: Path, dst: Path) -> bool: - if not Path.exists(dst): - return False + return not stale(dst, [src]) + +def stale(dst: Path, inputs: Iterable[Path]) -> bool: + if not Path.exists(dst): + return True - srcmtime = src.stat().st_mtime dstmtime = dst.stat().st_mtime - return srcmtime < dstmtime + for src in inputs: + if not Path.exists(src) or src.stat().st_mtime >= dstmtime: + return True + return False + +def read_depfile(path: Path) -> list[Path]: + if not Path.exists(path): + return [] + + with open(path, 'r') as file: + text = file.read().replace("\\\n", " ") + + # Only the first rule matters, the rest are phony rules from -MP. + rule = text.split("\n", 1)[0] + _, _, prereqs = rule.partition(": ") + + paths: list[Path] = [] + for token in re.split(r"(? Path: return self.objpath / f"{file}.o" + + def dep(self, file: Path) -> Path: + return self.objpath / f"{file}.d" diff --git a/chinook/models/_target.py b/chinook/models/_target.py index 57763c1..a8d4337 100644 --- a/chinook/models/_target.py +++ b/chinook/models/_target.py @@ -30,6 +30,7 @@ class Target(yaml.YAMLObject): # Internal to the tool. _flattend: bool = field(init=False,default=False) _prepared: bool = field(init=False,default=False) + _rebuilt: bool = field(init=False,default=False) def __deepcopy__(self, memo) -> Self: return Target( diff --git a/chinook/pipeline/_build.py b/chinook/pipeline/_build.py index 7454cbe..cec9f26 100644 --- a/chinook/pipeline/_build.py +++ b/chinook/pipeline/_build.py @@ -1,15 +1,14 @@ from logging import getLogger from logging import Logger -from ..models import Output from ..models import Project from ..models import Result -from ..models import Target from .._config import Config from ._commands import Command from ._except import NoTargetsFoundError from ._execute import execute from ._prepare import prepare +from ._prepare import prepare_dependencies _logger = getLogger("chinook") @@ -24,31 +23,6 @@ def build( commands: list[Command] = [] for target in project.targets: - commands.extend(_prepare_dependencies(appcfg, project, target, logger)) + commands.extend(prepare_dependencies(appcfg, project, target, logger)) commands.extend(prepare(appcfg, project, target, logger)) return execute(commands, "Building") - -def _prepare_dependencies( - appcfg: Config, - project: Project, - target: Target, - logger: Logger = _logger -) -> list[Command]: - from ..management import find_target_by_name - from ..management import find_project_by_target - - commands: list[Command] = [] - - for required in target.deps: - dependency = find_target_by_name(required.value) - if dependency.type == Output.IMP or \ - dependency.type == Output.INT or \ - dependency._prepared: - continue - - parent = find_project_by_target(required.value) - if project.cid != parent.cid: - if required.value not in parent.exports: - raise Exception(f"Cannot depend on non-exported target: {required.value}") - commands.extend(prepare(appcfg, parent, dependency, logger)) - return commands diff --git a/chinook/pipeline/_prepare.py b/chinook/pipeline/_prepare.py index 19d9429..1cb672e 100644 --- a/chinook/pipeline/_prepare.py +++ b/chinook/pipeline/_prepare.py @@ -4,9 +4,10 @@ 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 ..management import read_depfile +from ..management import stale from ..models import Output from ..models import Project from ..models import Target @@ -28,8 +29,10 @@ def prepare( ) -> list[Command]: commands: list[Command] = [] if target.type == Output.IMP or \ - target.type == Output.INT: + target.type == Output.INT or \ + target._prepared: return commands + target._prepared = True match target.type: case Output.EXE: outfile = project.destination.bin(target.name) @@ -49,13 +52,14 @@ def prepare( for file in target.srcs: srcfile = project.origination.src(file.value) dstfile = project.destination.obj(file.value) - if not latest(srcfile, dstfile): + depfile = project.destination.dep(file.value) + objects.append(dstfile) + if not Path.exists(depfile) or \ + stale(dstfile, [srcfile, *read_depfile(depfile)]): dstfile.parent.mkdir(parents=True, exist_ok=True) commands.append(CompileObject(compiler, defopts, logger, srcfile, dstfile)) - if not latest(srcfile, outfile): - outfile.parent.mkdir(parents=True, exist_ok=True) - objects.append(dstfile) + relink = len(commands) > 0 or stale(outfile, objects) defopts.objfiles.extend(objects) del objects @@ -66,12 +70,21 @@ def prepare( libtgt = find_target_by_name(library.value) libprj = find_project_by_target(libtgt.name) if libtgt.type == Output.LIB: + libfile = libprj.destination.lib(library.value) defopts.libdirs.append(libprj.destination.libpath) defopts.archives.append(library.value) elif libtgt.type == Output.DLL: - defopts.objfiles.append(libprj.destination.dll(library.value)) + libfile = libprj.destination.dll(library.value) + defopts.objfiles.append(libfile) + else: + continue - if len(defopts.objfiles) == 0: + # Archives don't embed their libraries, so only relink linked outputs. + if target.type != Output.LIB and \ + (libtgt._rebuilt or stale(outfile, [libfile])): + relink = True + + if not relink or len(defopts.objfiles) == 0: return commands command: Command @@ -81,6 +94,30 @@ def prepare( case Output.DLL: command = CompileDynlib(compiler, defopts, logger, outfile) case Output.OBJ: assert False, "Not Implemented" - target._prepared = True + outfile.parent.mkdir(parents=True, exist_ok=True) + target._rebuilt = True commands.append(command) return commands + +def prepare_dependencies( + appcfg: Config, + project: Project, + target: Target, + logger: Logger = _logger +) -> list[Command]: + commands: list[Command] = [] + + for required in target.deps: + dependency = find_target_by_name(required.value) + if dependency.type == Output.IMP or \ + dependency.type == Output.INT or \ + dependency._prepared: + continue + + parent = find_project_by_target(required.value) + if project.cid != parent.cid: + if required.value not in parent.exports: + raise Exception(f"Cannot depend on non-exported target: {required.value}") + commands.extend(prepare_dependencies(appcfg, parent, dependency, logger)) + commands.extend(prepare(appcfg, parent, dependency, logger)) + return commands diff --git a/chinook/pipeline/_test.py b/chinook/pipeline/_test.py index 7fafd82..55bc9e3 100644 --- a/chinook/pipeline/_test.py +++ b/chinook/pipeline/_test.py @@ -1,16 +1,15 @@ from logging import getLogger from logging import Logger -from ..models import Output from ..models import Project from ..models import Result -from ..models import Target from .._config import Config from ._commands import Command from ._commands import ExecuteTest from ._except import NoTestsFoundError from ._execute import execute from ._prepare import prepare +from ._prepare import prepare_dependencies _logger = getLogger("chinook") @@ -25,7 +24,7 @@ def test( commands: list[Command] = [] for target in project.tests: - commands.extend(_prepare_dependencies(appcfg, project, target, logger)) + commands.extend(prepare_dependencies(appcfg, project, target, logger)) commands.extend(prepare(appcfg, project, target, logger)) results = execute(commands, "Building") @@ -39,28 +38,3 @@ def test( commands.append(ExecuteTest(None, None, logger, testfile)) results.extend(execute(commands, "Testing")) return results - -def _prepare_dependencies( - appcfg: Config, - project: Project, - target: Target, - logger: Logger = _logger -) -> list[Command]: - from ..management import find_target_by_name - from ..management import find_project_by_target - - commands: list[Command] = [] - - for required in target.deps: - dependency = find_target_by_name(required.value) - if dependency.type == Output.IMP or \ - dependency.type == Output.INT or \ - dependency._prepared: - continue - - parent = find_project_by_target(required.value) - if project.cid != parent.cid: - if required.value not in parent.exports: - raise Exception(f"Cannot depend on non-exported target: {required.value}") - commands.extend(prepare(appcfg, parent, dependency, logger)) - return commands