epicenter_cli/
fastlink.rs1use std::path::{Path, PathBuf};
20
21use anyhow::{Context, Result, bail};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum Flavor {
26 Gnu,
28 Msvc,
30}
31
32#[derive(Debug, Clone)]
34pub struct FastLinker {
35 pub host: String,
37 pub lld_dir: PathBuf,
39 pub linker: PathBuf,
41 pub flavor: Flavor,
42}
43
44impl FastLinker {
45 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 pub fn rustflags_toml(&self) -> String {
63 toml_array(&self.rustflags())
64 }
65}
66
67fn 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
94pub 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
125pub 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}