Skip to main content

epicenter_cli/
fastlink.rs

1//! Zero-install fast linker.
2//!
3//! rustup ships LLVM's linkers inside every toolchain, next to the target's
4//! std: `<sysroot>/lib/rustlib/<host>/bin/gcc-ld/` contains `ld.lld`
5//! (ELF), `lld-link` (COFF/MSVC) and the Apple/WebAssembly variants. No
6//! `mold`, no `lld` package, no `clang` needed.
7//!
8//! `epicenter setup --fast` wires that directory into Cargo:
9//!
10//! - GNU/ELF hosts keep the system C compiler as the linker *driver* and pass
11//!   `-C link-arg=-fuse-ld=lld -C link-arg=-B<gcc-ld>` — a flag GCC and Clang
12//!   both understand, with `-B` making the bundled `ld.lld` discoverable.
13//! - MSVC hosts cannot use `-fuse-ld` (the driver is `link.exe`): rustc is
14//!   pointed directly at the bundled `lld-link.exe` with `-C linker=…`.
15//!
16//! Apple's `ld64.lld` is not a drop-in for the system linker, so `--fast`
17//! refuses macOS instead of producing fragile builds.
18
19use std::path::{Path, PathBuf};
20
21use anyhow::{Context, Result, bail};
22
23/// How the bundled LLD is invoked on this host.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum Flavor {
26    /// ELF/GNU-style: the C compiler drives `ld.lld` found via `-B`.
27    Gnu,
28    /// COFF/MSVC: rustc drives the bundled `lld-link.exe` directly.
29    Msvc,
30}
31
32/// The bundled LLD for the active rustup toolchain.
33#[derive(Debug, Clone)]
34pub struct FastLinker {
35    /// Host triple the flags apply to (`x86_64-unknown-linux-gnu`).
36    pub host: String,
37    /// `…/lib/rustlib/<host>/bin/gcc-ld`.
38    pub lld_dir: PathBuf,
39    /// `ld.lld` / `lld-link.exe` inside `lld_dir`.
40    pub linker: PathBuf,
41    pub flavor: Flavor,
42}
43
44impl FastLinker {
45    /// rustc flags for `[target.<host>] rustflags`.
46    pub fn rustflags(&self) -> Vec<String> {
47        match self.flavor {
48            Flavor::Gnu => vec![
49                "-C".to_string(),
50                "link-arg=-fuse-ld=lld".to_string(),
51                "-C".to_string(),
52                format!("link-arg=-B{}", forward(&self.lld_dir)),
53            ],
54            Flavor::Msvc => vec![
55                "-C".to_string(),
56                format!("linker={}", forward(&self.linker)),
57            ],
58        }
59    }
60
61    /// The `rustflags = [...]` value, ready for the config line.
62    pub fn rustflags_toml(&self) -> String {
63        toml_array(&self.rustflags())
64    }
65}
66
67/// TOML paths use forward slashes on every platform.
68fn forward(path: &Path) -> String {
69    path.to_string_lossy().replace('\\', "/")
70}
71
72fn toml_array(items: &[String]) -> String {
73    let mut out = String::from("[");
74    for (index, item) in items.iter().enumerate() {
75        if index > 0 {
76            out.push_str(", ");
77        }
78        out.push('"');
79        for ch in item.chars() {
80            match ch {
81                '\\' => out.push_str("\\\\"),
82                '"' => out.push_str("\\\""),
83                '\n' => out.push_str("\\n"),
84                '\r' => out.push_str("\\r"),
85                _ => out.push(ch),
86            }
87        }
88        out.push('"');
89    }
90    out.push(']');
91    out
92}
93
94/// Detect the bundled LLD for the active toolchain's host.
95pub fn detect() -> Result<FastLinker> {
96    let mut rustc = crate::util::command_for("rustc")?;
97    let output = rustc
98        .arg("-vV")
99        .output()
100        .context("running `rustc -vV` to find the host triple")?;
101    if !output.status.success() {
102        bail!("`rustc -vV` failed — is a toolchain installed?");
103    }
104    let text = String::from_utf8_lossy(&output.stdout);
105    let host = text
106        .lines()
107        .find_map(|line| line.strip_prefix("host: "))
108        .map(str::trim)
109        .filter(|host| !host.is_empty())
110        .ok_or_else(|| anyhow::anyhow!("`rustc -vV` did not report a host triple"))?
111        .to_string();
112
113    let mut rustc = crate::util::command_for("rustc")?;
114    let output = rustc
115        .args(["--print", "sysroot"])
116        .output()
117        .context("running `rustc --print sysroot`")?;
118    if !output.status.success() {
119        bail!("`rustc --print sysroot` failed — is a toolchain installed?");
120    }
121    let sysroot = PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
122    locate(&sysroot, &host)
123}
124
125/// Pure detection against a known sysroot/host (unit-testable).
126pub fn locate(sysroot: &Path, host: &str) -> Result<FastLinker> {
127    if host.contains("apple-darwin") {
128        bail!(
129            "--fast supports ELF and COFF hosts; lld is not a drop-in for Apple's ld64 — \
130             leave the default Apple linker in place"
131        );
132    }
133    let lld_dir = sysroot
134        .join("lib")
135        .join("rustlib")
136        .join(host)
137        .join("bin")
138        .join("gcc-ld");
139    let (flavor, names): (Flavor, &[&str]) = if host.contains("windows-msvc") {
140        (Flavor::Msvc, &["lld-link.exe", "lld-link"])
141    } else if host.contains("windows-gnu")
142        || host.contains("linux")
143        || host.contains("freebsd")
144        || host.contains("netbsd")
145        || host.contains("openbsd")
146        || host.contains("dragonfly")
147        || host.contains("solaris")
148        || host.contains("illumos")
149    {
150        (Flavor::Gnu, &["ld.lld", "ld.lld.exe"])
151    } else {
152        bail!("--fast is not supported on host {host} (supported: Linux/BSD ELF and Windows)");
153    };
154    let linker = names
155        .iter()
156        .map(|name| lld_dir.join(name))
157        .find(|path| path.is_file())
158        .ok_or_else(|| {
159            anyhow::anyhow!(
160                "rustup's bundled LLD was not found in {} — is the `{host}` target installed?",
161                lld_dir.display()
162            )
163        })?;
164    Ok(FastLinker {
165        host: host.to_string(),
166        lld_dir,
167        linker,
168        flavor,
169    })
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    fn fixture(root: &Path, host: &str, names: &[&str]) -> PathBuf {
177        let dir = root
178            .join("lib")
179            .join("rustlib")
180            .join(host)
181            .join("bin")
182            .join("gcc-ld");
183        std::fs::create_dir_all(&dir).expect("create fixture");
184        for name in names {
185            std::fs::write(dir.join(name), b"").expect("write fixture");
186        }
187        dir
188    }
189
190    #[test]
191    fn gnu_hosts_use_fuse_ld_with_the_bundled_dir() {
192        let root = tempfile::tempdir().unwrap();
193        let dir = fixture(root.path(), "x86_64-unknown-linux-gnu", &["ld.lld"]);
194        let lld = locate(root.path(), "x86_64-unknown-linux-gnu").unwrap();
195        assert_eq!(lld.flavor, Flavor::Gnu);
196        assert_eq!(lld.lld_dir, dir);
197        let flags = lld.rustflags();
198        assert_eq!(flags.len(), 4);
199        assert_eq!(flags[0], "-C");
200        assert_eq!(flags[1], "link-arg=-fuse-ld=lld");
201        assert_eq!(flags[2], "-C");
202        assert_eq!(flags[3], format!("link-arg=-B{}", dir.display()));
203        assert!(lld.rustflags_toml().starts_with('['));
204    }
205
206    #[test]
207    fn msvc_hosts_link_with_the_bundled_lld_link() {
208        let root = tempfile::tempdir().unwrap();
209        fixture(root.path(), "x86_64-pc-windows-msvc", &["lld-link.exe"]);
210        let lld = locate(root.path(), "x86_64-pc-windows-msvc").unwrap();
211        assert_eq!(lld.flavor, Flavor::Msvc);
212        let flags = lld.rustflags();
213        assert_eq!(flags[0], "-C");
214        assert!(flags[1].starts_with("linker="));
215        assert!(flags[1].ends_with("/lld-link.exe"));
216        assert!(flags[1].contains("x86_64-pc-windows-msvc"));
217        assert!(!flags[1].contains('\\'), "paths are forward-slash");
218    }
219
220    #[test]
221    fn windows_gnu_uses_the_gcc_driver() {
222        let root = tempfile::tempdir().unwrap();
223        fixture(root.path(), "x86_64-pc-windows-gnu", &["ld.lld.exe"]);
224        let lld = locate(root.path(), "x86_64-pc-windows-gnu").unwrap();
225        assert_eq!(lld.flavor, Flavor::Gnu);
226        assert!(lld.rustflags()[1].contains("fuse-ld=lld"));
227    }
228
229    #[test]
230    fn apple_and_missing_lld_are_refused() {
231        let root = tempfile::tempdir().unwrap();
232        let err = locate(root.path(), "aarch64-apple-darwin").unwrap_err();
233        assert!(err.to_string().contains("ld64"), "{err}");
234        let err = locate(root.path(), "x86_64-unknown-linux-gnu").unwrap_err();
235        assert!(err.to_string().contains("bundled LLD"), "{err}");
236    }
237}