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 = [ args = [
f"{compiler}", f"{compiler}",
f"-c {str(srcpath)}", f"-c {str(srcpath)}",
f"-o {str(dstpath)}" f"-o {str(dstpath)}",
f"-MMD -MP -MF {str(dstpath.with_suffix('.d'))}"
] ]
if options.pic: if options.pic:
args.append("-fPIC") args.append("-fPIC")
+2
View File
@@ -4,6 +4,8 @@ from ..models import Project
from ..models import Target from ..models import Target
from ._except import NoProjectFoundError from ._except import NoProjectFoundError
from ._latest import latest from ._latest import latest
from ._latest import read_depfile
from ._latest import stale
from ._load import load from ._load import load
+30 -3
View File
@@ -1,10 +1,37 @@
import re
from pathlib import Path from pathlib import Path
from typing import Iterable
def latest(src: Path, dst: Path) -> bool: 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): 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 return False
srcmtime = src.stat().st_mtime def read_depfile(path: Path) -> list[Path]:
dstmtime = dst.stat().st_mtime if not Path.exists(path):
return srcmtime < dstmtime 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: def obj(self, file: Path) -> Path:
return self.objpath / f"{file}.o" 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. # Internal to the tool.
_flattend: bool = field(init=False,default=False) _flattend: bool = field(init=False,default=False)
_prepared: 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: def __deepcopy__(self, memo) -> Self:
return Target( return Target(
+2 -28
View File
@@ -1,15 +1,14 @@
from logging import getLogger from logging import getLogger
from logging import Logger from logging import Logger
from ..models import Output
from ..models import Project from ..models import Project
from ..models import Result from ..models import Result
from ..models import Target
from .._config import Config from .._config import Config
from ._commands import Command from ._commands import Command
from ._except import NoTargetsFoundError from ._except import NoTargetsFoundError
from ._execute import execute from ._execute import execute
from ._prepare import prepare from ._prepare import prepare
from ._prepare import prepare_dependencies
_logger = getLogger("chinook") _logger = getLogger("chinook")
@@ -24,31 +23,6 @@ def build(
commands: list[Command] = [] commands: list[Command] = []
for target in project.targets: 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)) commands.extend(prepare(appcfg, project, target, logger))
return execute(commands, "Building") 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 Compiler
from ..compiler import CompilerOptions from ..compiler import CompilerOptions
from ..management import latest
from ..management import find_target_by_name from ..management import find_target_by_name
from ..management import find_project_by_target from ..management import find_project_by_target
from ..management import read_depfile
from ..management import stale
from ..models import Output from ..models import Output
from ..models import Project from ..models import Project
from ..models import Target from ..models import Target
@@ -28,8 +29,10 @@ def prepare(
) -> list[Command]: ) -> list[Command]:
commands: list[Command] = [] commands: list[Command] = []
if target.type == Output.IMP or \ if target.type == Output.IMP or \
target.type == Output.INT: target.type == Output.INT or \
target._prepared:
return commands return commands
target._prepared = True
match target.type: match target.type:
case Output.EXE: outfile = project.destination.bin(target.name) case Output.EXE: outfile = project.destination.bin(target.name)
@@ -49,13 +52,14 @@ def prepare(
for file in target.srcs: for file in target.srcs:
srcfile = project.origination.src(file.value) srcfile = project.origination.src(file.value)
dstfile = project.destination.obj(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) dstfile.parent.mkdir(parents=True, exist_ok=True)
commands.append(CompileObject(compiler, defopts, logger, srcfile, dstfile)) 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) defopts.objfiles.extend(objects)
del objects del objects
@@ -66,12 +70,21 @@ def prepare(
libtgt = find_target_by_name(library.value) libtgt = find_target_by_name(library.value)
libprj = find_project_by_target(libtgt.name) libprj = find_project_by_target(libtgt.name)
if libtgt.type == Output.LIB: if libtgt.type == Output.LIB:
libfile = libprj.destination.lib(library.value)
defopts.libdirs.append(libprj.destination.libpath) defopts.libdirs.append(libprj.destination.libpath)
defopts.archives.append(library.value) defopts.archives.append(library.value)
elif libtgt.type == Output.DLL: 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 return commands
command: Command command: Command
@@ -81,6 +94,30 @@ def prepare(
case Output.DLL: command = CompileDynlib(compiler, defopts, logger, outfile) case Output.DLL: command = CompileDynlib(compiler, defopts, logger, outfile)
case Output.OBJ: assert False, "Not Implemented" case Output.OBJ: assert False, "Not Implemented"
target._prepared = True outfile.parent.mkdir(parents=True, exist_ok=True)
target._rebuilt = True
commands.append(command) commands.append(command)
return commands 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 getLogger
from logging import Logger from logging import Logger
from ..models import Output
from ..models import Project from ..models import Project
from ..models import Result from ..models import Result
from ..models import Target
from .._config import Config from .._config import Config
from ._commands import Command from ._commands import Command
from ._commands import ExecuteTest from ._commands import ExecuteTest
from ._except import NoTestsFoundError from ._except import NoTestsFoundError
from ._execute import execute from ._execute import execute
from ._prepare import prepare from ._prepare import prepare
from ._prepare import prepare_dependencies
_logger = getLogger("chinook") _logger = getLogger("chinook")
@@ -25,7 +24,7 @@ def test(
commands: list[Command] = [] commands: list[Command] = []
for target in project.tests: 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)) commands.extend(prepare(appcfg, project, target, logger))
results = execute(commands, "Building") results = execute(commands, "Building")
@@ -39,28 +38,3 @@ def test(
commands.append(ExecuteTest(None, None, logger, testfile)) commands.append(ExecuteTest(None, None, logger, testfile))
results.extend(execute(commands, "Testing")) results.extend(execute(commands, "Testing"))
return results 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