Compare commits

..
2 Commits
Author SHA1 Message Date
SoraKatadzuma 6b88f01a05 Deduplication in compiler driver commands 2026-09-15 15:08:59 -05:00
SoraKatadzuma 9c41dc36e3 Rebuilds/relinks properly 2026-09-15 15:04:16 -05:00
9 changed files with 148 additions and 121 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
+57 -50
View File
@@ -21,13 +21,14 @@ class _FlattenContext:
_libs_list: list[Accessor] = field(init=False, default_factory=list) _libs_list: list[Accessor] = field(init=False, default_factory=list)
_srcs_list: list[Accessor] = field(init=False, default_factory=list) _srcs_list: list[Accessor] = field(init=False, default_factory=list)
_incs_set: set[Accessor] = field(init=False, default_factory=set) # Keyed by value so the same entry at different access levels isn't repeated.
_dirs_set: set[Accessor] = field(init=False, default_factory=set) _incs_set: set[str] = field(init=False, default_factory=set)
_opts_set: set[Accessor] = field(init=False, default_factory=set) _dirs_set: set[str] = field(init=False, default_factory=set)
_defs_set: set[Accessor] = field(init=False, default_factory=set) _opts_set: set[str] = field(init=False, default_factory=set)
_deps_set: set[Accessor] = field(init=False, default_factory=set) _defs_set: set[str] = field(init=False, default_factory=set)
_libs_set: set[Accessor] = field(init=False, default_factory=set) _deps_set: set[str] = field(init=False, default_factory=set)
_srcs_set: set[Accessor] = field(init=False, default_factory=set) _libs_set: set[str] = field(init=False, default_factory=set)
_srcs_set: set[str] = field(init=False, default_factory=set)
@property @property
def incs(self) -> list[Accessor]: def incs(self) -> list[Accessor]:
@@ -68,45 +69,45 @@ class _FlattenContext:
for src in target.srcs: self.append_src(src) for src in target.srcs: self.append_src(src)
def append_inc(self, inc: Accessor) -> None: def append_inc(self, inc: Accessor) -> None:
if inc.level == Access.PRIVATE or inc in self._incs_set: if inc.level == Access.PRIVATE or str(inc.value) in self._incs_set:
return return
self._incs_set.add(inc) self._incs_set.add(str(inc.value))
self._incs_list.insert(0, inc) self._incs_list.insert(0, inc)
def append_dir(self, lib: Accessor) -> None: def append_dir(self, lib: Accessor) -> None:
if lib.level == Access.PRIVATE or lib in self._dirs_set: if lib.level == Access.PRIVATE or str(lib.value) in self._dirs_set:
return return
self._dirs_set.add(lib) self._dirs_set.add(str(lib.value))
self._dirs_list.insert(0, lib) self._dirs_list.insert(0, lib)
def append_opt(self, opt: Accessor) -> None: def append_opt(self, opt: Accessor) -> None:
if opt.level == Access.PRIVATE or opt in self._opts_set: if opt.level == Access.PRIVATE or str(opt.value) in self._opts_set:
return return
self._opts_set.add(opt) self._opts_set.add(str(opt.value))
self._opts_list.insert(0, opt) self._opts_list.insert(0, opt)
def append_def(self, dfn: Accessor) -> None: def append_def(self, dfn: Accessor) -> None:
if dfn.level == Access.PRIVATE or dfn in self._defs_set: if dfn.level == Access.PRIVATE or str(dfn.value) in self._defs_set:
return return
self._defs_set.add(dfn) self._defs_set.add(str(dfn.value))
self._defs_list.insert(0, dfn) self._defs_list.insert(0, dfn)
def append_dep(self, dep: Accessor) -> None: def append_dep(self, dep: Accessor) -> None:
if dep.level == Access.PRIVATE or dep in self._deps_set: if dep.level == Access.PRIVATE or str(dep.value) in self._deps_set:
return return
self._deps_set.add(dep) self._deps_set.add(str(dep.value))
self._deps_list.insert(0, dep) self._deps_list.insert(0, dep)
def append_lib(self, lib: Accessor) -> None: def append_lib(self, lib: Accessor) -> None:
if lib.level == Access.PRIVATE or lib in self._libs_set: if lib.level == Access.PRIVATE or str(lib.value) in self._libs_set:
return return
self._libs_set.add(lib) self._libs_set.add(str(lib.value))
self._libs_list.insert(0, lib) self._libs_list.insert(0, lib)
def append_src(self, src: Accessor) -> None: def append_src(self, src: Accessor) -> None:
if src.level == Access.PRIVATE or src in self._srcs_set: if src.level == Access.PRIVATE or str(src.value) in self._srcs_set:
return return
self._srcs_set.add(src) self._srcs_set.add(str(src.value))
self._srcs_list.insert(0, src) self._srcs_list.insert(0, src)
@@ -114,18 +115,7 @@ def flatten_targets(project: Project) -> None:
for target in project.targets: for target in project.targets:
if target._flattend: if target._flattend:
continue continue
context = _FlattenContext() _flatten_target(_target_context(project, target), target)
context.append_inc(Accessor(
level = Access.PUBLIC \
if target.name in project.exports \
else Access.PROTECTED,
value = project.origination.src("include")
))
context.append_dir(Accessor(
level = Access.PUBLIC,
value = project.destination.libpath
))
_flatten_target(context, target)
def flatten_tests(project: Project) -> None: def flatten_tests(project: Project) -> None:
for target in project.tests: for target in project.tests:
@@ -142,6 +132,20 @@ def flatten_tests(project: Project) -> None:
)) ))
_flatten_test(context, target) _flatten_test(context, target)
def _target_context(project: Project, target: Target) -> _FlattenContext:
context = _FlattenContext()
context.append_inc(Accessor(
level = Access.PUBLIC \
if target.name in project.exports \
else Access.PROTECTED,
value = project.origination.src("include")
))
context.append_dir(Accessor(
level = Access.PUBLIC,
value = project.destination.libpath
))
return context
def _flatten_target(context: _FlattenContext, target: Target) -> None: def _flatten_target(context: _FlattenContext, target: Target) -> None:
if target._flattend: if target._flattend:
return return
@@ -150,14 +154,7 @@ def _flatten_target(context: _FlattenContext, target: Target) -> None:
if (parent:=targets_by_name_mapping.get(dependency.value)) == None: if (parent:=targets_by_name_mapping.get(dependency.value)) == None:
raise ValueError(f"No such dependable target: {dependency.value}") raise ValueError(f"No such dependable target: {dependency.value}")
_target_inherit_target(context, parent) _target_inherit_target(context, parent)
target.incs.includes.extend(context.incs) _extend_target(context, target)
target.incs.libraries.extend(context.dirs)
target.opts.extend(context.opts)
target.defs.extend(context.defs)
target.deps.extend(context.deps)
target.libs.extend(context.libs)
target.srcs.extend(context.srcs)
target._flattend = True
def _flatten_test(context: _FlattenContext, target: Target) -> None: def _flatten_test(context: _FlattenContext, target: Target) -> None:
if target._flattend: if target._flattend:
@@ -167,15 +164,25 @@ def _flatten_test(context: _FlattenContext, target: Target) -> None:
if (parent:=targets_by_name_mapping.get(dependency.value)) == None: if (parent:=targets_by_name_mapping.get(dependency.value)) == None:
raise ValueError(f"No such dependable target: {dependency.value}") raise ValueError(f"No such dependable target: {dependency.value}")
_test_inherit_target(context, parent) _test_inherit_target(context, parent)
target.incs.includes.extend(context.incs) _extend_target(context, target)
target.incs.libraries.extend(context.dirs)
target.opts.extend(context.opts) def _extend_target(context: _FlattenContext, target: Target) -> None:
target.defs.extend(context.defs) _extend_unique(target.incs.includes, context.incs)
target.deps.extend(context.deps) _extend_unique(target.incs.libraries, context.dirs)
target.libs.extend(context.libs) _extend_unique(target.opts, context.opts)
target.srcs.extend(context.srcs) _extend_unique(target.defs, context.defs)
_extend_unique(target.deps, context.deps)
_extend_unique(target.libs, context.libs)
_extend_unique(target.srcs, context.srcs)
target._flattend = True target._flattend = True
def _extend_unique(dst: list[Accessor], src: list[Accessor]) -> None:
values = {str(v.value) for v in dst}
for accessor in src:
if str(accessor.value) not in values:
values.add(str(accessor.value))
dst.append(accessor)
def _target_inherit_target(context: _FlattenContext, parent: Target) -> None: def _target_inherit_target(context: _FlattenContext, parent: Target) -> None:
if parent.name in tests_by_name_mapping: if parent.name in tests_by_name_mapping:
raise ValueError("Cannot inherit tests") raise ValueError("Cannot inherit tests")
@@ -188,7 +195,8 @@ def _inherit_raw(context: _FlattenContext, parent: Target) -> None:
parent_project = project_by_targets_mapping.get(parent.name) parent_project = project_by_targets_mapping.get(parent.name)
assert parent_project is not None, "Parent project not mapped!" assert parent_project is not None, "Parent project not mapped!"
_flatten_target(context, parent) # Parents flatten in their own context so the child's entries don't leak into them.
_flatten_target(_target_context(parent_project, parent), parent)
context.inherit_target(parent) context.inherit_target(parent)
context.append_inc(Accessor( context.append_inc(Accessor(
level = Access.PROTECTED, level = Access.PROTECTED,
@@ -202,4 +210,3 @@ def _inherit_raw(context: _FlattenContext, parent: Target) -> None:
level = Access.PROTECTED, level = Access.PROTECTED,
value = parent.name value = parent.name
)) ))
+31 -4
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:
if not Path.exists(dst): return not stale(dst, [src])
return False
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 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"(?<!\\)\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
+48 -10
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,22 @@ 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:
defopts.libdirs.append(libprj.destination.libpath) libfile = libprj.destination.lib(library.value)
if libprj.destination.libpath not in defopts.libdirs:
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 +95,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