Rebuilds/relinks properly
This commit is contained in:
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
Reference in New Issue
Block a user