Rebuilds/relinks properly

This commit is contained in:
2026-09-15 15:04:16 -05:00
parent 3c3ec7c354
commit 9c41dc36e3
8 changed files with 89 additions and 70 deletions
+2 -1
View File
@@ -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")
+2
View File
@@ -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
+30 -3
View File
@@ -1,10 +1,37 @@
import re
from pathlib import Path
from typing import Iterable
def latest(src: Path, dst: Path) -> bool:
return not stale(dst, [src])
def stale(dst: Path, inputs: Iterable[Path]) -> bool:
if not Path.exists(dst):
return True
dstmtime = dst.stat().st_mtime
for src in inputs:
if not Path.exists(src) or src.stat().st_mtime >= dstmtime:
return True
return False
srcmtime = src.stat().st_mtime
dstmtime = dst.stat().st_mtime
return srcmtime < dstmtime
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"(?<!\\)\s+", prereqs.strip()):
if len(token) == 0:
continue
token = re.sub(r"\\([ #])", r"\1", token).replace("$$", "$")
paths.append(Path(token))
return paths
+3
View File
@@ -45,3 +45,6 @@ class Destination:
def obj(self, file: Path) -> Path:
return self.objpath / f"{file}.o"
def dep(self, file: Path) -> Path:
return self.objpath / f"{file}.d"
+1
View File
@@ -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(
+2 -28
View File
@@ -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
+46 -9
View File
@@ -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
+2 -28
View File
@@ -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