musllvm

musl and LLVM cross-toolchain build system
git clone https://git.daat.foo/repos/musllvm.git
Log | Files | Refs | README | LICENSE

test_driver.py (15973B)


      1 import hashlib
      2 import io
      3 import json
      4 import os
      5 from pathlib import Path
      6 import shutil
      7 import subprocess
      8 import tarfile
      9 import tempfile
     10 import unittest
     11 
     12 
     13 ROOT = Path(__file__).resolve().parent.parent
     14 MAKE = shutil.which("make")
     15 TOOLS = "clang clang++ ld.lld llvm-ar llvm-ranlib llvm-nm llvm-objcopy llvm-objdump llvm-readelf llvm-strip llvm-size llvm-strings llvm-addr2line llvm-cxxfilt"
     16 FAKE_TOOL = r'''#!/usr/bin/env python3
     17 import json
     18 import os
     19 from pathlib import Path
     20 import shutil
     21 import sys
     22 
     23 name = Path(sys.argv[0]).name
     24 args = sys.argv[1:]
     25 with open(os.environ["FAKE_LOG"], "a") as log:
     26     log.write(json.dumps({"tool": name, "args": args}) + "\n")
     27 
     28 def write(path, value="fixture"):
     29     path = Path(path)
     30     path.parent.mkdir(parents=True, exist_ok=True)
     31     path.write_text(value)
     32 
     33 if name in ("clang", "clang++"):
     34     if "--version" in args:
     35         print("clang version 21.1.8 (fixture)")
     36     elif "-dumpversion" in args:
     37         print("21.1.8")
     38     elif "-print-resource-dir" in args:
     39         print(os.environ["FAKE_RESOURCE"])
     40     elif "-print-target-triple" in args:
     41         print(next(arg.split("=", 1)[1] for arg in args if arg.startswith("--target=")))
     42     elif "-print-libgcc-file-name" in args:
     43         resource = next(arg.split("=", 1)[1] for arg in args if arg.startswith("-resource-dir="))
     44         print(str(Path(resource) / "lib/linux" / ("libclang_rt.builtins-" + os.environ["ARCH"] + ".a")))
     45     else:
     46         sys.exit("fixture refuses compilation")
     47 elif name == "llvm-config":
     48     print(os.environ["FAKE_LLVM_CMAKE"])
     49 elif name == "curl":
     50     destination = Path(args[args.index("--output") + 1])
     51     archive = args[-1].rsplit("/", 1)[-1]
     52     if os.environ.get("FAKE_CURL_BAD"):
     53         destination.write_text("truncated download")
     54     else:
     55         shutil.copyfile(Path(os.environ["FAKE_DOWNLOADS"]) / archive, destination)
     56     if os.environ.get("FAKE_CURL_FAIL"):
     57         sys.exit(22)
     58 elif name == "make":
     59     if "headers_install" in args:
     60         prefix = next(arg.split("=", 1)[1] for arg in args if arg.startswith("INSTALL_HDR_PATH="))
     61         write(Path(prefix) / "include/linux/version.h")
     62     elif "install-headers" in args:
     63         prefix = next(arg.split("=", 1)[1] for arg in args if arg.startswith("DESTDIR="))
     64         write(Path(prefix) / "usr/include/stdio.h")
     65     elif "install" in args:
     66         prefix = next(arg.split("=", 1)[1] for arg in args if arg.startswith("DESTDIR="))
     67         write(Path(prefix) / "usr/lib/libc.so")
     68         (Path(prefix) / "lib").mkdir(exist_ok=True)
     69 elif name == "cmake":
     70     if "-B" in args:
     71         directory = Path(args[args.index("-B") + 1])
     72         write(directory / "fixture-config.json", json.dumps(args))
     73     elif "--build" in args:
     74         directory = Path(args[args.index("--build") + 1])
     75         if os.environ.get("FAKE_FAIL_STAGE") == directory.name:
     76             sys.exit(1)
     77         if "install-distribution" in args:
     78             configuration = json.loads((directory / "fixture-config.json").read_text())
     79             prefix = next(arg.split("=", 1)[1] for arg in configuration if arg.startswith("-DCMAKE_INSTALL_PREFIX="))
     80             for tool in os.environ["FAKE_TOOLS"].split():
     81                 path = Path(prefix) / "bin" / tool
     82                 path.parent.mkdir(parents=True, exist_ok=True)
     83                 shutil.copyfile(os.environ["FAKE_EXECUTABLE"], path)
     84                 path.chmod(0o755)
     85     elif "--install" in args:
     86         directory = Path(args[args.index("--install") + 1])
     87         configuration = json.loads((directory / "fixture-config.json").read_text())
     88         prefix = next(arg.split("=", 1)[1] for arg in configuration if arg.startswith("-DCMAKE_INSTALL_PREFIX="))
     89         if directory.name == "builtins":
     90             write(Path(prefix) / "lib/linux" / ("libclang_rt.builtins-" + os.environ["ARCH"] + ".a"))
     91         else:
     92             write(Path(os.environ["DESTDIR"]) / "usr/lib/libc++.a")
     93 '''
     94 
     95 CAPTURE = r'''#!/usr/bin/env python3
     96 import json
     97 import os
     98 import sys
     99 print(json.dumps({"args": sys.argv[1:], "env": dict(os.environ)}))
    100 '''
    101 
    102 
    103 class DriverTests(unittest.TestCase):
    104     def setUp(self):
    105         self.temporary = tempfile.TemporaryDirectory(prefix="musllvm-test-")
    106         self.addCleanup(self.temporary.cleanup)
    107         self.root = Path(self.temporary.name) / "project"
    108         self.root.mkdir()
    109         for name in ("scripts", "files/toolchain"):
    110             shutil.copytree(ROOT / name, self.root / name)
    111         for name in ("Makefile", "config.mk"):
    112             shutil.copyfile(ROOT / name, self.root / name)
    113         self.tools = Path(self.temporary.name) / "tools"
    114         self.tools.mkdir()
    115         self.fake = self.tools / "fixture"
    116         self.fake.write_text(FAKE_TOOL)
    117         self.fake.chmod(0o755)
    118         for name in TOOLS.split() + ["llvm-config", "curl", "make", "cmake", "ninja"]:
    119             (self.tools / name).symlink_to(self.fake)
    120         self.resource = Path(self.temporary.name) / "resource/21"
    121         (self.resource / "include").mkdir(parents=True)
    122         (self.resource / "include/unwind.h").write_text("fixture")
    123         self.llvm_cmake = Path(self.temporary.name) / "llvm-cmake"
    124         self.llvm_cmake.mkdir()
    125         (self.llvm_cmake / "LLVMConfig.cmake").write_text("fixture")
    126         self.log = Path(self.temporary.name) / "commands.jsonl"
    127         self.downloads = Path(self.temporary.name) / "downloads"
    128         self.downloads.mkdir()
    129         manifest = []
    130         for component in ("musl", "linux", "llvm", "atomic"):
    131             archive = self.downloads / (component + "-test.tar.gz")
    132             with tarfile.open(archive, "w:gz") as stream:
    133                 relative = "configure" if component == "musl" else "placeholder"
    134                 content = b"#!/bin/sh\nset -eu\n\"$CC\" -print-target-triple\n" if component == "musl" else b"fixture"
    135                 info = tarfile.TarInfo(component + "-test/" + relative)
    136                 info.size = len(content)
    137                 info.mode = 0o755
    138                 stream.addfile(info, io.BytesIO(content))
    139                 if component == "atomic":
    140                     for relative in ("Makefile", "atomic.c"):
    141                         info = tarfile.TarInfo(component + "-test/" + relative)
    142                         info.size = 7
    143                         stream.addfile(info, io.BytesIO(b"fixture"))
    144             digest = hashlib.sha256(archive.read_bytes()).hexdigest()
    145             manifest.append(f"{component} {archive.name} {digest} https://example.invalid/{archive.name}\n")
    146         (self.root / "sources.list").write_text("".join(manifest))
    147         self.env = os.environ.copy()
    148         for key in ("ARCH", "TARGET", "PREFIX", "BUILD_DIR", "SOURCES_DIR", "SOURCES_FILE", "LLVM_MODE", "JOBS", "LINK_JOBS", "HOST_CC", "HOST_CXX", "TARGET_CFLAGS", "HOST_CFLAGS", "HOST_CXXFLAGS", "HOST_LDFLAGS", "RUNNER", "MAKEFLAGS", "MFLAGS", "MUSLLVM_SYSROOT"):
    149             self.env.pop(key, None)
    150         self.env.update({
    151             "PATH": str(self.tools) + os.pathsep + os.environ["PATH"],
    152             "FAKE_LOG": str(self.log), "FAKE_RESOURCE": str(self.resource),
    153             "FAKE_LLVM_CMAKE": str(self.llvm_cmake),
    154             "FAKE_DOWNLOADS": str(self.downloads), "ARCH": "x86_64",
    155             "FAKE_EXECUTABLE": str(self.fake), "FAKE_TOOLS": TOOLS,
    156         })
    157 
    158     def run_make(self, *arguments, success=True, env=None):
    159         result = subprocess.run([MAKE, *arguments], cwd=self.root, env=env or self.env,
    160                                 text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    161         if success:
    162             self.assertEqual(result.returncode, 0, result.stdout)
    163         else:
    164             self.assertNotEqual(result.returncode, 0, result.stdout)
    165         return result
    166 
    167     def commands(self):
    168         return [json.loads(line) for line in self.log.read_text().splitlines()]
    169 
    170     def install_capture(self, path):
    171         path.write_text(CAPTURE)
    172         path.chmod(0o755)
    173 
    174     def install_wrapper(self, name, template, prefix=None):
    175         prefix = prefix or Path(self.temporary.name) / "relocated tools' path"
    176         (prefix / "bin").mkdir(parents=True, exist_ok=True)
    177         (prefix / "libexec").mkdir(exist_ok=True)
    178         script = (ROOT / "files/toolchain" / template).read_text()
    179         path = prefix / "bin" / name
    180         path.write_text(script.replace("@TARGET@", "x86_64-linux-musl").replace("@RESOURCE_VERSION@", "21"))
    181         path.chmod(0o755)
    182         return path
    183 
    184     def test_configuration_and_command_line_overrides(self):
    185         result = self.run_make("show-config", "ARCH=aarch64", "JOBS=1")
    186         self.assertIn("TARGET=aarch64-linux-musl", result.stdout)
    187         self.assertIn("JOBS=1", result.stdout)
    188         self.assertIn(f"PREFIX={self.root}/build/aarch64-linux-musl\n", result.stdout)
    189         self.assertIn(f"BUILD_DIR={self.root}/build/work/aarch64-linux-musl\n", result.stdout)
    190         self.assertFalse((self.root / "build").exists())
    191 
    192     def test_invalid_configuration_fails_before_work(self):
    193         for arguments in (("ARCH=mips",), ("JOBS=00",), ("JOBS=-2",), ("LINK_JOBS=1:2",),
    194                           ("PREFIX=/",), ("BUILD_DIR=/tmp/../home",), ("LLVM_MODE=other",),
    195                           ("TARGET=aarch64-linux-musl",), ("PREFIX=/tmp/tools with spaces",),
    196                           ("PREFIX=/tmp/tools$$variable",)):
    197             with self.subTest(arguments=arguments):
    198                 self.run_make("show-config", *arguments, success=False)
    199         self.assertFalse((self.root / "build").exists())
    200 
    201     def test_fetch_verifies_and_skips_downloaded_archives(self):
    202         self.run_make("pull")
    203         self.assertEqual(sum(item["tool"] == "curl" for item in self.commands()), 4)
    204         self.run_make("pull", "verify-sources")
    205         self.assertEqual(sum(item["tool"] == "curl" for item in self.commands()), 4)
    206 
    207     def test_corrupt_cache_is_repaired(self):
    208         self.run_make("pull")
    209         (self.root / "sources/musl-test.tar.gz").write_text("corrupt")
    210         self.run_make("verify-sources", success=False)
    211         self.run_make("pull", "verify-sources")
    212         self.assertEqual(sum(item["tool"] == "curl" for item in self.commands()), 5)
    213 
    214     def test_bad_or_failed_download_is_never_published(self):
    215         for setting in ("FAKE_CURL_BAD", "FAKE_CURL_FAIL"):
    216             with self.subTest(setting=setting):
    217                 env = {**self.env, setting: "1"}
    218                 self.run_make("pull", success=False, env=env)
    219                 self.assertEqual(list((self.root / "sources").iterdir()), [])
    220 
    221     def test_full_driver_resumes_and_rejects_changed_configuration(self):
    222         self.run_make("runtimes")
    223         first = len([item for item in self.commands() if item["tool"] in ("make", "cmake")])
    224         self.run_make("runtimes", "JOBS=1")
    225         second = len([item for item in self.commands() if item["tool"] in ("make", "cmake")])
    226         self.assertEqual(first, second)
    227         self.run_make("runtimes", "TARGET_CFLAGS=-Os", success=False)
    228         self.run_make("clean")
    229         self.assertFalse((self.root / "build/x86_64-linux-musl").exists())
    230         self.assertTrue((self.root / "sources/musl-test.tar.gz").exists())
    231 
    232     def test_failed_stage_resumes_without_rebuilding_headers(self):
    233         self.run_make("runtimes", success=False, env={**self.env, "FAKE_FAIL_STAGE": "builtins"})
    234         self.assertTrue((self.root / "build/work/x86_64-linux-musl/stamps/headers").exists())
    235         self.assertFalse((self.root / "build/work/x86_64-linux-musl/stamps/builtins").exists())
    236         self.run_make("runtimes")
    237         self.assertEqual(sum("headers_install" in item["args"] for item in self.commands()), 1)
    238 
    239     def test_source_mode_uses_installed_host_tools(self):
    240         self.run_make("runtimes", "LLVM_MODE=source")
    241         prefix = self.root / "build/x86_64-linux-musl"
    242         self.assertEqual(os.readlink(prefix / "libexec/clang"), "../host/bin/clang")
    243         self.assertTrue((prefix / "bin/x86_64-linux-musl-c++").exists())
    244 
    245     def test_clean_preserves_unmarked_directory(self):
    246         prefix = self.root / "unrelated"
    247         prefix.mkdir()
    248         sentinel = prefix / "keep"
    249         sentinel.write_text("user data")
    250         self.run_make("clean", f"PREFIX={prefix}", success=False)
    251         self.assertEqual(sentinel.read_text(), "user data")
    252 
    253     def test_concurrent_build_is_rejected(self):
    254         self.run_make("runtimes")
    255         (self.root / "build/work/x86_64-linux-musl/lock").mkdir()
    256         self.run_make("runtimes", success=False)
    257         self.run_make("clean", success=False)
    258 
    259     def test_cxx_wrapper_relocation_and_host_isolation(self):
    260         wrapper = self.install_wrapper("x86_64-linux-musl-c++", "cc.sh")
    261         self.install_capture(wrapper.parent.parent / "libexec/clang")
    262         env = {**self.env, "CPATH": "/host/headers", "LIBRARY_PATH": "/host/libraries"}
    263         result = subprocess.check_output([str(wrapper), "source with spaces.cpp"], env=env, text=True)
    264         invocation = json.loads(result)
    265         self.assertIn("--driver-mode=g++", invocation["args"])
    266         self.assertIn(f"--config={wrapper.parent.parent}/share/musllvm/clang.cfg", invocation["args"])
    267         self.assertIn("source with spaces.cpp", invocation["args"])
    268         self.assertIn(f"--sysroot={wrapper.parent.parent}/sysroot", invocation["args"])
    269         self.assertNotIn("CPATH", invocation["env"])
    270         self.assertNotIn("LIBRARY_PATH", invocation["env"])
    271 
    272     def test_wrapper_omits_default_libraries_for_compile_only_and_freestanding(self):
    273         wrapper = self.install_wrapper("x86_64-linux-musl-cc", "cc.sh")
    274         self.install_capture(wrapper.parent.parent / "libexec/clang")
    275         for option in ("-c", "-E", "-nostdlib", "-nodefaultlibs"):
    276             with self.subTest(option=option):
    277                 invocation = json.loads(subprocess.check_output([str(wrapper), option, "source.c"], env=self.env, text=True))
    278                 self.assertNotIn("-latomic", invocation["args"])
    279 
    280     def test_sysroot_override_applies_to_compiler_and_pkg_config(self):
    281         wrapper = self.install_wrapper("x86_64-linux-musl-cc", "cc.sh")
    282         self.install_capture(wrapper.parent.parent / "libexec/clang")
    283         env = {**self.env, "MUSLLVM_SYSROOT": "/another/sysroot"}
    284         invocation = json.loads(subprocess.check_output([str(wrapper), "source.c"], env=env, text=True))
    285         self.assertIn("--sysroot=/another/sysroot", invocation["args"])
    286         pkg_wrapper = self.install_wrapper("x86_64-linux-musl-pkg-config", "pkg-config.sh")
    287         self.install_capture(self.tools / "pkg-config")
    288         invocation = json.loads(subprocess.check_output([str(pkg_wrapper), "--libs", "target"], env=env, text=True))
    289         self.assertEqual(invocation["env"]["PKG_CONFIG_SYSROOT_DIR"], "/another/sysroot")
    290 
    291     def test_environment_output_is_safe_to_evaluate(self):
    292         wrapper = self.install_wrapper("x86_64-linux-musl-env", "env.sh")
    293         result = subprocess.check_output(["sh", "-c", 'eval "$("$1")"; printf "%s\\n%s\\n" "$CC" "$CMAKE_TOOLCHAIN_FILE"',
    294                                           "test", str(wrapper)], env=self.env, text=True)
    295         self.assertEqual(result.splitlines(), [str(wrapper.parent / "x86_64-linux-musl-cc"),
    296                                              str(wrapper.parent.parent / "share/musllvm/toolchain.cmake")])
    297 
    298     def test_pkg_config_cannot_see_host_packages(self):
    299         wrapper = self.install_wrapper("x86_64-linux-musl-pkg-config", "pkg-config.sh")
    300         self.install_capture(self.tools / "pkg-config")
    301         env = {**self.env, "PKG_CONFIG_PATH": "/host/packages", "PKG_CONFIG_LIBDIR": "/host/packages"}
    302         invocation = json.loads(subprocess.check_output([str(wrapper), "--libs", "target"], env=env, text=True))
    303         self.assertEqual(invocation["env"]["PKG_CONFIG_PATH"], "")
    304         self.assertEqual(invocation["env"]["PKG_CONFIG_SYSROOT_DIR"], str(wrapper.parent.parent / "sysroot"))
    305         self.assertNotIn("/host/packages", invocation["env"]["PKG_CONFIG_LIBDIR"])
    306 
    307 
    308 if __name__ == "__main__":
    309     unittest.main()