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()