Skip to main content

chaud_hot/cargo/
builder.rs

1use super::StdioMode;
2use crate::util::CommandExt as _;
3use crate::util::assert::err_unreachable;
4use crate::workspace::graph::BuildEnv;
5use anyhow::{Context as _, Result, bail, ensure};
6use camino::{Utf8Path, Utf8PathBuf};
7use core::iter::Peekable;
8use hashbrown::HashMap;
9use memchr::memmem;
10use nanoserde::DeJson;
11use std::process::Command;
12use std::time::{Instant, SystemTime};
13use std::{fs, io};
14
15pub struct Builder {
16    cmd: Command,
17    linker: Linker,
18    initial: HashMap<Utf8PathBuf, SystemTime>,
19    latest: Vec<Utf8PathBuf>,
20}
21
22struct Linker {
23    env_clear: Box<[String]>,
24    env_set: Box<[(String, String)]>,
25    bin: String,
26    arg_pre: Box<[String]>,
27    arg_post: Box<[String]>,
28}
29
30impl Builder {
31    pub fn init(env: &BuildEnv) -> Result<Self> {
32        init_inner(env).context("Failed to init Builder")
33    }
34
35    pub fn link_latest(&self, dst: &Utf8Path) -> Result<()> {
36        link(dst, &self.linker, &self.latest)
37    }
38
39    pub fn build(&mut self) -> Result<()> {
40        let parts = extract_link_args(&mut self.cmd).context("Build failed")?;
41
42        // We need to check this, because the linker args won't be re-printed for a
43        // fully fresh build, and we need to avoid clearing `latest_libs` in that
44        // case.
45        if parts.is_empty() {
46            log::trace!("Empty output, rlibs seem fresh");
47            return Ok(());
48        }
49
50        self.latest.clear();
51        extract_libs(parts, &self.initial, |p, _| {
52            if is_alloc_shim(&p) {
53                // Linking the alloc shim causes problems on Linux.
54                log::trace!("Ignoring alloc shim: {p:?}");
55                return;
56            }
57
58            log::trace!("Found new rlib: {p:?}");
59            self.latest.push(p);
60        });
61
62        log::debug!("Found {} new rlibs", self.latest.len());
63        Ok(())
64    }
65}
66
67fn init_inner(env: &BuildEnv) -> Result<Builder> {
68    verify_fresh(env).context("Failed to check freshness")?;
69
70    let mut cmd = cargo_cmd(env);
71    cmd.args(["--", "--print=link-args"]);
72    cmd.arg(format!(
73        r#"--cfg=chaud_force_dirty="{}""#,
74        current_time_nanos()?
75    ));
76
77    let (linker, initial) = extract_linker(cmd).context("Failed to extract linker")?;
78
79    let mut cmd = cargo_cmd(env);
80    cmd.env("__CHAUD_RELOAD", "1");
81    cmd.args([
82        "--",
83        "--print=link-args",
84        // With `__CHAUD_RELOAD` set, Chaud will generate references to symbols
85        // that only exist in the running binary (not in the newly compiled
86        // code). Using `true` as the linker ensures that the compilation still
87        // succeeds (and has the nice side-effect of avoiding unnecessary work).
88        "-Clinker=true",
89        // Ensure that object files for the root crate are kept around (because
90        // we don't have an rlib for them).
91        "-Csave-temps",
92    ]);
93
94    let mut builder = Builder { cmd, linker, initial, latest: vec![] };
95
96    // Perform an initial build.
97    builder.build()?;
98
99    Ok(builder)
100}
101
102fn verify_fresh(env: &BuildEnv) -> Result<()> {
103    #[derive(DeJson)]
104    struct Message {
105        fresh: bool,
106    }
107
108    let mut cmd = cargo_cmd(env);
109
110    log::info!("Verifying freshness: {cmd:?}");
111
112    cmd.arg("--message-format=json");
113
114    let output = cmd.stdout_str()?;
115
116    let Some(line) = output.lines().rev().nth(1) else {
117        bail!("Not enough output lines");
118    };
119
120    let msg = Message::deserialize_json(line)?;
121
122    if !msg.fresh {
123        log::warn!(
124            "FRESHNESS CHECK FAILED: The build flags are likely incorrect: {:?}",
125            env.flags()
126        );
127    }
128
129    Ok(())
130}
131
132fn extract_libs(
133    parts: Vec<String>,
134    initial: &HashMap<Utf8PathBuf, SystemTime>,
135    mut found: impl FnMut(Utf8PathBuf, SystemTime),
136) {
137    for part in parts {
138        let is_rlib = part.ends_with(".rlib");
139        let is_obj = part.ends_with(".o");
140
141        if !is_rlib && !is_obj {
142            continue;
143        }
144        let part = Utf8PathBuf::from(part);
145
146        match part.metadata() {
147            Ok(m) if m.is_file() => {
148                let mtime = match m.modified() {
149                    Ok(t) => t,
150                    Err(e) => {
151                        log::warn!("Failed to get mtime of existing file {part:?}: {e}");
152                        continue;
153                    }
154                };
155
156                if initial.get(&part).is_none_or(|v| *v != mtime) {
157                    found(part, mtime);
158                }
159            }
160            Err(e) if is_obj && e.kind() == io::ErrorKind::NotFound => {
161                log::trace!("Ignoring missing obj {part:?}");
162            }
163            _ => {
164                log::warn!("Ignoring invalid rlib {part:?}");
165            }
166        }
167    }
168}
169
170fn extract_link_args(cmd: &mut Command) -> Result<Vec<String>> {
171    if log::log_enabled!(log::Level::Trace) {
172        log::trace!("Running {cmd:?}");
173    } else {
174        log::info!("Cargo build in progress...");
175    }
176
177    let start = Instant::now();
178    let output = cmd.stdout_str();
179
180    log::info!(
181        "Cargo build {} in {:.1}s",
182        if output.is_ok() {
183            "succeeded"
184        } else {
185            "failed"
186        },
187        start.elapsed().as_secs_f32()
188    );
189
190    let output = output?;
191    let output = output.trim();
192    ensure!(!output.contains('\n'), "Too many output lines");
193
194    shlex::split(output).context("shlex failed")
195}
196
197fn cargo_cmd(env: &BuildEnv) -> Command {
198    let loud = log::log_enabled!(log::Level::Trace);
199    let mode = match loud {
200        true => StdioMode::LoudCapture,
201        false => StdioMode::QuietCapture,
202    };
203
204    let mut cmd = env.cargo_rustc(mode);
205    cmd.arg("--offline");
206
207    if !loud {
208        cmd.arg("-q");
209    }
210
211    cmd
212}
213
214fn extract_linker(mut cmd: Command) -> Result<(Linker, HashMap<Utf8PathBuf, SystemTime>)> {
215    fn skip_out(parts: &mut Peekable<impl Iterator<Item = String>>) -> Result<()> {
216        if parts.peek().is_some_and(|p| p == "-o") {
217            parts.next();
218            let out = parts.next().context("Too short: -o")?;
219            log::trace!("linker: out: {out:?}");
220        }
221        Ok(())
222    }
223
224    let mut has_strip = false;
225    let mut has_no_whole = false;
226    let mut has_whole = false;
227    let mut check_arg = |arg: &str| {
228        has_strip |= arg.contains("--gc-sections") || arg.contains("-dead_strip");
229        has_no_whole |= arg.contains("--no-whole-archive");
230        has_whole |= arg.contains("--whole-archive") || arg.contains("-all_load");
231    };
232
233    let mut parts = extract_link_args(&mut cmd)?.into_iter().peekable();
234
235    let mut env_clear = vec![];
236    if parts.peek().is_some_and(|p| p == "env") {
237        parts.next();
238        while parts.peek().is_some_and(|p| p == "-u") {
239            parts.next();
240            let name = parts.next().context("Too short: -u")?;
241            log::trace!("linker: env_clear: {name:?}");
242            env_clear.push(name);
243        }
244    }
245
246    let mut env_set = vec![];
247    while let Some((k, v)) = parts.peek().and_then(|s| s.split_once('=')) {
248        if !k.chars().all(|c| c.is_ascii_alphabetic() || c == '_') {
249            break;
250        }
251        log::trace!("linker: env_set: {k:?} = {v:?}");
252        env_set.push((k.to_owned(), v.to_owned()));
253        parts.next();
254    }
255
256    let linker = parts.next().context("Too short: linker")?;
257    log::trace!("linker: linker: {linker:?}");
258
259    let mut arg_pre = vec![];
260    while parts.peek().is_some_and(|p| !p.ends_with(".o")) {
261        skip_out(&mut parts)?;
262
263        let arg = parts.next().context("unreachable: peeked")?;
264        check_arg(&arg);
265        log::trace!("linker: arg_pre: {arg:?}");
266        arg_pre.push(arg);
267    }
268
269    let mut files = vec![];
270    while parts.peek().is_some_and(|p| p.ends_with(".o")) {
271        let arg = parts.next().context("unreachable: peeked")?;
272        log::trace!("linker: object: {arg:?}");
273        files.push(arg);
274    }
275
276    while parts.peek().is_some_and(|p| !p.ends_with(".rlib")) {
277        skip_out(&mut parts)?;
278
279        let arg = parts.next().context("unreachable: peeked")?;
280        check_arg(&arg);
281        log::trace!("linker: custom: {arg:?}");
282    }
283
284    while parts.peek().is_some_and(|p| p.ends_with(".rlib")) {
285        let arg = parts.next().context("unreachable: peeked")?;
286        log::trace!("linker: rlib: {arg:?}");
287        files.push(arg);
288    }
289
290    let mut arg_post = vec![];
291    while parts.peek().is_some() {
292        skip_out(&mut parts)?;
293
294        let arg = parts.next().context("unreachable: peeked")?;
295        check_arg(&arg);
296        log::trace!("linker: arg_post: {arg:?}");
297        arg_post.push(arg);
298    }
299
300    if has_strip {
301        log::warn!("DEAD CODE CHECK FAILED: `-Clink-dead-code` likely not set");
302    }
303    if has_no_whole {
304        log::warn!(
305            "CUSTOM LINK CHECK FAILED: `--no-whole-archive` detected, likely from custom native linking"
306        );
307    }
308    if !has_whole {
309        log::warn!("LINK ALL CHECK FAILED: `-Zpre-link-args` likely not set properly");
310    }
311
312    let mut initial = HashMap::new();
313    extract_libs(files, &HashMap::new(), |p, m| {
314        if p.as_str().ends_with(".rlib") {
315            initial.insert(p, m);
316        }
317    });
318    log::debug!("Found {} initial rlibs", initial.len());
319
320    Ok((
321        Linker {
322            env_clear: env_clear.into_boxed_slice(),
323            env_set: env_set.into_boxed_slice(),
324            bin: linker,
325            arg_pre: arg_pre.into_boxed_slice(),
326            arg_post: arg_post.into_boxed_slice(),
327        },
328        initial,
329    ))
330}
331
332fn link(dst: &Utf8Path, linker: &Linker, latest: &[Utf8PathBuf]) -> Result<()> {
333    let mut cmd = Command::new(&linker.bin);
334
335    for (k, v) in &linker.env_set {
336        cmd.env(k, v);
337    }
338    for k in &linker.env_clear {
339        cmd.env_remove(k);
340    }
341
342    cmd.args(&linker.arg_pre)
343        .args(latest)
344        .args(&linker.arg_post);
345
346    if cfg!(target_os = "macos") {
347        cmd.args(["-undefined", "dynamic_lookup"]);
348    }
349
350    cmd.args(["-shared", "-o", dst.as_str()]);
351
352    log::trace!("Executing: {cmd:?}");
353
354    let st = cmd.status()?;
355    ensure!(st.success(), "Linking failed: {st}");
356
357    Ok(())
358}
359
360fn current_time_nanos() -> Result<u128> {
361    let now = SystemTime::now();
362    let Ok(now) = now.duration_since(SystemTime::UNIX_EPOCH) else {
363        err_unreachable!();
364    };
365
366    Ok(now.as_nanos())
367}
368
369fn is_alloc_shim(p: &Utf8Path) -> bool {
370    if p.extension() != Some("o") {
371        return false;
372    }
373
374    match is_alloc_shim_inner(p) {
375        Ok(r) => r,
376        Err(e) => {
377            log::warn!("Failed to check for alloc shim: {e}");
378            false
379        }
380    }
381}
382
383fn is_alloc_shim_inner(p: &Utf8Path) -> Result<bool> {
384    let buf = fs::read(p)?;
385    // FIXME: Make this more reliable, and maybe cache the results.
386    Ok(memmem::find(&buf, b".bss.__rust_no_alloc_shim_is_unstable").is_some())
387}