|
| 1 | +"""Build a Dylint driver from the git revision used by the lint crate. |
| 2 | +
|
| 3 | +The published `dylint_driver` 5.0.0 crate does not build against the |
| 4 | +nightly toolchain pinned by CI. The lint crate already pins Dylint's git |
| 5 | +revision for `dylint_linting` and `dylint_testing`; this script builds the |
| 6 | +matching driver from that checkout and exports DYLINT_DRIVER_PATH. |
| 7 | +""" |
| 8 | + |
| 9 | +from __future__ import annotations |
| 10 | + |
| 11 | +import os |
| 12 | +import shutil |
| 13 | +import subprocess |
| 14 | +import sys |
| 15 | +import tempfile |
| 16 | +from pathlib import Path |
| 17 | + |
| 18 | + |
| 19 | +DYLINT_REPO = "https://github.com/trailofbits/dylint" |
| 20 | +DYLINT_REV = "4bd91ce7729b74c7ee5664bbb588f7baf30b4a09" |
| 21 | +TOOLCHAIN_CHANNEL = "nightly-2026-03-26" |
| 22 | + |
| 23 | + |
| 24 | +def run(args: list[str], **kwargs) -> subprocess.CompletedProcess[str]: # noqa: ANN003 |
| 25 | + print("+", " ".join(args), flush=True) |
| 26 | + return subprocess.run(args, check=True, text=True, **kwargs) |
| 27 | + |
| 28 | + |
| 29 | +def rustc_host() -> str: |
| 30 | + # Invoke rustc through `rustup run` so the call works even when PATH |
| 31 | + # is fronted by shims (e.g. soldr) that do not understand the |
| 32 | + # `+<toolchain>` directive that only the rustup `cargo`/`rustc` |
| 33 | + # wrappers parse. |
| 34 | + output = subprocess.check_output( |
| 35 | + ["rustup", "run", TOOLCHAIN_CHANNEL, "rustc", "-vV"], |
| 36 | + text=True, |
| 37 | + ) |
| 38 | + for line in output.splitlines(): |
| 39 | + if line.startswith("host: "): |
| 40 | + return line.split("host: ", 1)[1] |
| 41 | + raise RuntimeError("could not determine rustc host triple") |
| 42 | + |
| 43 | + |
| 44 | +def rustc_toolchain_root(full_toolchain: str) -> Path: |
| 45 | + rustc = subprocess.check_output( |
| 46 | + ["rustup", "which", "--toolchain", full_toolchain, "rustc"], |
| 47 | + text=True, |
| 48 | + ).strip() |
| 49 | + return Path(rustc).resolve().parent.parent |
| 50 | + |
| 51 | + |
| 52 | +def write_driver_package(package: Path, dylint_checkout: Path, full_toolchain: str) -> None: |
| 53 | + src = package / "src" |
| 54 | + src.mkdir(parents=True) |
| 55 | + |
| 56 | + driver_path = str((dylint_checkout / "driver").resolve()).replace("\\", "\\\\") |
| 57 | + (package / "Cargo.toml").write_text( |
| 58 | + f""" |
| 59 | +[package] |
| 60 | +name = "dylint_driver-{full_toolchain}" |
| 61 | +version = "0.1.0" |
| 62 | +edition = "2018" |
| 63 | +
|
| 64 | +[dependencies] |
| 65 | +anyhow = "1.0" |
| 66 | +env_logger = "0.11" |
| 67 | +dylint_driver = {{ path = "{driver_path}" }} |
| 68 | +""".lstrip(), |
| 69 | + encoding="utf-8", |
| 70 | + ) |
| 71 | + # Use `.toml` extension so rustup unambiguously parses as TOML. |
| 72 | + # The extensionless `rust-toolchain` form is ambiguous (single-line |
| 73 | + # vs TOML) and on Windows hosts has been observed to silently fall |
| 74 | + # through to the default toolchain, leaving the build script's |
| 75 | + # `#![feature(...)]` rejected as "stable channel". |
| 76 | + (package / "rust-toolchain.toml").write_text( |
| 77 | + f""" |
| 78 | +[toolchain] |
| 79 | +channel = "{full_toolchain}" |
| 80 | +components = ["llvm-tools-preview", "rustc-dev"] |
| 81 | +""".lstrip(), |
| 82 | + encoding="utf-8", |
| 83 | + ) |
| 84 | + (src / "main.rs").write_text( |
| 85 | + """ |
| 86 | +#![feature(rustc_private)] |
| 87 | +
|
| 88 | +use anyhow::Result; |
| 89 | +use std::env; |
| 90 | +
|
| 91 | +pub fn main() -> Result<()> { |
| 92 | + env_logger::init(); |
| 93 | +
|
| 94 | + let args: Vec<_> = env::args_os().collect(); |
| 95 | +
|
| 96 | + dylint_driver::dylint_driver(&args) |
| 97 | +} |
| 98 | +""".lstrip(), |
| 99 | + encoding="utf-8", |
| 100 | + ) |
| 101 | + |
| 102 | + |
| 103 | +def append_github_env(name: str, value: Path) -> None: |
| 104 | + github_env = os.environ.get("GITHUB_ENV") |
| 105 | + if github_env: |
| 106 | + with open(github_env, "a", encoding="utf-8") as file: |
| 107 | + file.write(f"{name}={value}\n") |
| 108 | + |
| 109 | + |
| 110 | +def main() -> int: |
| 111 | + full_toolchain = f"{TOOLCHAIN_CHANNEL}-{rustc_host()}" |
| 112 | + runner_temp = Path(os.environ.get("RUNNER_TEMP", tempfile.gettempdir())).resolve() |
| 113 | + driver_root = runner_temp / "dylint-drivers" |
| 114 | + driver_dir = driver_root / full_toolchain |
| 115 | + driver_dir.mkdir(parents=True, exist_ok=True) |
| 116 | + |
| 117 | + with tempfile.TemporaryDirectory(prefix="fbuild-dylint-") as temp: |
| 118 | + temp_path = Path(temp) |
| 119 | + checkout = temp_path / "dylint" |
| 120 | + package = temp_path / "driver-package" |
| 121 | + |
| 122 | + run(["git", "clone", "--filter=blob:none", DYLINT_REPO, str(checkout)]) |
| 123 | + run(["git", "-C", str(checkout), "checkout", DYLINT_REV]) |
| 124 | + |
| 125 | + package.mkdir() |
| 126 | + write_driver_package(package, checkout, full_toolchain) |
| 127 | + |
| 128 | + env = os.environ.copy() |
| 129 | + # Force the rustup toolchain in the env so it propagates into |
| 130 | + # nested cargo/rustc invocations (e.g. build-script compilation |
| 131 | + # of dylint_driver which uses `#![feature(...)]` and requires |
| 132 | + # nightly). Setting via env is more reliable than relying solely |
| 133 | + # on the `rust-toolchain.toml` lookup, especially on Windows. |
| 134 | + env["RUSTUP_TOOLCHAIN"] = full_toolchain |
| 135 | + # Anchor RUSTC and CARGO to the specific nightly binaries to |
| 136 | + # defeat shadowing by any stable `rustc`/`cargo` that may appear |
| 137 | + # earlier in PATH (e.g. a Chocolatey-installed stable on |
| 138 | + # Windows). Without this, cargo's build-script rustc invocation |
| 139 | + # may resolve to a stable rustc and fail on `#![feature(...)]` |
| 140 | + # with E0554. |
| 141 | + nightly_bin = rustc_toolchain_root(full_toolchain) / "bin" |
| 142 | + rustc_exe = nightly_bin / ("rustc.exe" if os.name == "nt" else "rustc") |
| 143 | + cargo_exe = nightly_bin / ("cargo.exe" if os.name == "nt" else "cargo") |
| 144 | + if rustc_exe.exists(): |
| 145 | + env["RUSTC"] = str(rustc_exe) |
| 146 | + if cargo_exe.exists(): |
| 147 | + env["CARGO"] = str(cargo_exe) |
| 148 | + if os.name != "nt": |
| 149 | + toolchain_root = rustc_toolchain_root(full_toolchain) |
| 150 | + rpath = f"-C link-args=-Wl,-rpath,{toolchain_root / 'lib'}" |
| 151 | + env["RUSTFLAGS"] = f"{env.get('RUSTFLAGS', '')} {rpath}".strip() |
| 152 | + |
| 153 | + # Use `rustup run` instead of `cargo +<toolchain>` because the |
| 154 | + # cargo on PATH may be a shim (e.g. soldr's) that does not parse |
| 155 | + # the `+<toolchain>` directive — that directive is only honored |
| 156 | + # by the rustup-managed cargo wrapper. `rustup run` selects the |
| 157 | + # toolchain explicitly and works regardless of which `cargo` |
| 158 | + # comes first on PATH. |
| 159 | + run( |
| 160 | + ["rustup", "run", TOOLCHAIN_CHANNEL, "cargo", "build"], |
| 161 | + cwd=package, |
| 162 | + env=env, |
| 163 | + ) |
| 164 | + |
| 165 | + exe_suffix = ".exe" if os.name == "nt" else "" |
| 166 | + built_driver = package / "target" / "debug" / f"dylint_driver-{full_toolchain}{exe_suffix}" |
| 167 | + installed_driver = driver_dir / f"dylint-driver{exe_suffix}" |
| 168 | + shutil.copy2(built_driver, installed_driver) |
| 169 | + |
| 170 | + append_github_env("DYLINT_DRIVER_PATH", driver_root) |
| 171 | + print(f"DYLINT_DRIVER_PATH={driver_root}") |
| 172 | + return 0 |
| 173 | + |
| 174 | + |
| 175 | +if __name__ == "__main__": |
| 176 | + sys.exit(main()) |
0 commit comments