Files
modulus/modulus/build.py
T
khodowany fb93acca1a
publish / publish (push) Successful in 11m4s
Cleanup
2026-10-08 01:31:14 -05:00

258 lines
7.9 KiB
Python

import argparse
import shutil
import modulus.clangd
import modulus.conan
import modulus.container
import modulus.process
import modulus.python
def clear_cmake_cache(target):
cache = target.build_dir / "CMakeCache.txt"
cmake_files = target.build_dir / "CMakeFiles"
if cache.exists():
cache.unlink()
if cmake_files.exists():
shutil.rmtree(cmake_files)
def clear_conan_deps(target):
conan_deps = target.build_dir / "conan"
if conan_deps.exists():
shutil.rmtree(conan_deps)
def cmake_cache_values(path):
result = {}
try:
lines = path.read_text(encoding="utf-8").splitlines()
except FileNotFoundError:
return result
for line in lines:
if not line or line.startswith(("//", "#")) or "=" not in line:
continue
key, value = line.split("=", 1)
name = key.split(":", 1)[0]
result[name] = value
return result
def cmake_bool_value(value):
text = str(value).upper()
if text.endswith("-NOTFOUND"):
return False
if text in {"1", "ON", "TRUE", "YES", "Y"}:
return True
if text in {"0", "OFF", "FALSE", "NO", "N", "IGNORE", "NOTFOUND", ""}:
return False
return None
def cmake_cache_value_matches(configured, expected):
if isinstance(expected, bool):
return cmake_bool_value(configured) is expected
return configured == cmake_value(expected)
def target_cmake_definitions_match(target):
definitions = target.cmake_definitions
if not definitions:
return True
values = cmake_cache_values(target.build_dir / "CMakeCache.txt")
for name, expected in definitions.items():
configured = values.get(name)
if configured is None or not cmake_cache_value_matches(configured, expected):
return False
return True
def target_is_configured(target):
return (
(target.build_dir / "CMakeCache.txt").is_file()
and (target.build_dir / "build.ninja").is_file()
and target_cmake_definitions_match(target)
)
def dependency_build_args(context):
return ["--build=missing"]
def configure_one(context, target, update, no_remote=False):
modulus.python.interpreter(context, install=True, offline=no_remote)
clear_conan_deps(target)
command = [
"conan",
"install",
".",
f"--output-folder={target.build_dir / 'conan'}",
f"--profile:host={target.profile}",
f"--profile:build={context.default_target.profile}",
*dependency_build_args(context),
"-o",
"&:with_tests=True" if target.can_run else "&:with_tests=False",
]
if update:
command.append("--update")
if no_remote:
command.append("--no-remote")
status = modulus.process.run(context, command)
if status != 0:
return status
clear_cmake_cache(target)
return modulus.process.run(
context,
[
"cmake",
"-S",
".",
"-B",
target.build_dir,
"-G",
"Ninja",
f"-DCMAKE_TOOLCHAIN_FILE={target.build_dir / 'conan' / 'conan_toolchain.cmake'}",
f"-DCMAKE_BUILD_TYPE={target.build_type}",
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
f"-DBUILD_TESTING={'ON' if target.can_run else 'OFF'}",
f"-DCMAKE_PREFIX_PATH={target.build_dir / 'conan'}",
*cmake_definition_args(modulus.python.cmake_definitions(context)),
*cmake_definition_args(target.cmake_definitions),
],
)
def configure(context, argv):
if not modulus.process.in_docker(context):
status = modulus.container.docker_run(
context,
[
f"./{context.tools_rel}/configure.py",
*argv,
],
)
return status or modulus.clangd.prepare(context)
parser = argparse.ArgumentParser(description=f"Configure {context.package['name']} for Conan/CMake targets.")
parser.add_argument(
"--target",
action="append",
default=[],
choices=sorted(context.targets),
help="Target to configure. May be passed more than once. Defaults to the default target.",
)
parser.add_argument("--all", action="store_true", help="Configure all supported targets.")
parser.add_argument("--update", action="store_true", help="Check Conan remotes for newer dependency revisions.")
parser.add_argument("--no-remote", action="store_true", help="Do not query Conan remotes. Requires all dependencies to be available locally or buildable from local recipes.")
args = parser.parse_args(argv)
context.conan_cache_dir.mkdir(parents=True, exist_ok=True)
if not args.no_remote:
status = modulus.conan.configure_conan_remote(context)
if status != 0:
return status
update = args.update
for target in context.selected_targets(args.target, args.all):
target.build_dir.mkdir(parents=True, exist_ok=True)
status = configure_one(context, target, update, no_remote=args.no_remote)
if status != 0:
return status
return 0
def build(context, argv):
if not modulus.process.in_docker(context):
return modulus.container.docker_run(
context,
[
f"./{context.tools_rel}/build.py",
*argv,
],
)
parser = argparse.ArgumentParser(description=f"Build {context.package['name']} for configured targets.")
parser.add_argument(
"--target",
action="append",
default=[],
choices=sorted(context.targets),
help="Target to build. May be passed more than once. Defaults to the default target.",
)
parser.add_argument(
"--cmake-target",
action="append",
default=[],
help="CMake target to build. May be passed more than once.",
)
parser.add_argument("--all", action="store_true", help="Build all supported targets.")
args = parser.parse_args(argv)
selected_targets = context.selected_targets(args.target, args.all)
if any(not target_is_configured(target) for target in selected_targets):
context.conan_cache_dir.mkdir(parents=True, exist_ok=True)
status = modulus.conan.configure_conan_remote(context)
if status != 0:
return status
for target in selected_targets:
if not target_is_configured(target):
target.build_dir.mkdir(parents=True, exist_ok=True)
status = configure_one(context, target, update=False)
if status != 0:
return status
cmake_targets = list(args.cmake_target)
if not cmake_targets:
cmake_targets = [None]
for cmake_target in cmake_targets:
command = [
"cmake",
"--build",
target.build_dir,
]
if cmake_target is not None:
command.extend(["--target", cmake_target])
command.extend(["--parallel", "2"])
status = modulus.process.run(context, command)
if status != 0:
return status
return 0
def test(context, argv):
if not modulus.process.in_docker(context):
return modulus.container.docker_run(
context,
[
f"./{context.tools_rel}/test.py",
*argv,
],
)
parser = argparse.ArgumentParser(description=f"Run {context.package['name']} tests for configured targets.")
parser.add_argument(
"--target",
action="append",
default=[],
choices=sorted(context.targets),
help="Target to test. May be passed more than once. Defaults to the default target.",
)
parser.add_argument("--all", action="store_true", help="Test all supported targets.")
args = parser.parse_args(argv)
test_name = context.config["tests"]["name"]
for target in context.selected_targets(args.target, args.all):
if not target.can_run:
print(f"Skipping tests for {target.name}: target binaries cannot run in this container.")
continue
status = modulus.process.run(
context,
[
"ctest",
"--test-dir",
target.build_dir,
"-R",
f"^{test_name}$",
"--output-on-failure",
],
)
if status != 0:
return status
return 0
def option_value(value):
if isinstance(value, bool):
return "True" if value else "False"
return str(value)
def cmake_value(value):
if isinstance(value, bool):
return "ON" if value else "OFF"
return str(value)
def cmake_definition_args(definitions):
result = []
for name, value in definitions.items():
result.append(f"-D{name}={cmake_value(value)}")
return result