chaud_hot/cargo/
builder.rs1use 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 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 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 "-Clinker=true",
89 "-Csave-temps",
92 ]);
93
94 let mut builder = Builder { cmd, linker, initial, latest: vec![] };
95
96 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 Ok(memmem::find(&buf, b".bss.__rust_no_alloc_shim_is_unstable").is_some())
387}