1use std::ffi::{OsStr, OsString};
28use std::path::{Path, PathBuf};
29use std::time::{Duration, Instant};
30
31const ARTIFACT_WAIT_TIMEOUT: Duration = Duration::from_secs(600);
36
37pub const WRAPPER_MODE_ENV: &str = "WATERUI_INTERNAL_RUSTC_WRAPPER";
39pub const WRAPPER_CHAIN_ENV: &str = "WATERUI_RUSTC_WRAPPER_CHAIN";
42pub const BUILD_STD_TARGET_ENV: &str = "WATERUI_BUILD_STD_TARGET";
48pub const BUILD_STD_DYLIB_DIR_ENV: &str = "WATERUI_BUILD_STD_DYLIB_DIR";
51
52#[must_use]
57pub fn wrapper_main() -> Option<i32> {
58 std::env::var_os(WRAPPER_MODE_ENV)?;
59 Some(run_wrapper())
60}
61
62fn run_wrapper() -> i32 {
63 let mut invocation = std::env::args_os();
64 let _self = invocation.next();
65 let Some(rustc) = invocation.next() else {
66 eprintln!("water: rustc wrapper invoked without a rustc path");
67 return 1;
68 };
69 let args: Vec<OsString> = invocation.collect();
70 let target = std::env::var_os(BUILD_STD_TARGET_ENV).unwrap_or_default();
71 let rewritten = rewrite_args(&args, &target, ARTIFACT_WAIT_TIMEOUT);
72 if let Some(error) = rewritten.error {
76 eprintln!("water: {error}");
77 return 1;
78 }
79
80 let status = match std::env::var_os(WRAPPER_CHAIN_ENV) {
81 Some(chain) => std::process::Command::new(chain)
82 .arg(&rustc)
83 .args(&rewritten.args)
84 .status(),
85 None => std::process::Command::new(&rustc)
86 .args(&rewritten.args)
87 .status(),
88 };
89 let status = match status {
90 Ok(status) => status,
91 Err(error) => {
92 eprintln!("water: failed to invoke rustc wrapper target: {error}");
93 return 1;
94 }
95 };
96
97 if status.success()
98 && rewritten.emits_std_dylib
99 && emits_linked_output(&args)
100 && let Some(publish_dir) = std::env::var_os(BUILD_STD_DYLIB_DIR_ENV)
101 {
102 let out_dir = arg_value(&args, "--out-dir");
103 if let Err(error) = publish_std_dylib(Path::new(out_dir), Path::new(&publish_dir)) {
104 eprintln!("water: failed to stage the build-std libstd dylib: {error}");
105 return 1;
106 }
107 }
108
109 status.code().unwrap_or(1)
110}
111
112struct Rewrite {
114 args: Vec<OsString>,
115 emits_std_dylib: bool,
118 error: Option<DylibTimeout>,
121}
122
123fn rewrite_args(args: &[OsString], target: &OsStr, wait_timeout: Duration) -> Rewrite {
124 if target.is_empty() || arg_value(args, "--target") != target {
125 return Rewrite {
126 args: args.to_vec(),
127 emits_std_dylib: false,
128 error: None,
129 };
130 }
131
132 let std_crate_type = arg_value(args, "--crate-type");
135 let is_std_rlib = arg_value(args, "--crate-name") == OsStr::new("std")
136 && (std_crate_type == "rlib" || std_crate_type == "lib");
137 if is_std_rlib && emits_linked_output(args) {
140 return Rewrite {
141 args: rewrite_std_unit(args, wait_timeout),
142 emits_std_dylib: true,
143 error: None,
144 };
145 }
146
147 match add_std_dylib_extern(args, wait_timeout) {
148 Ok(args) => Rewrite {
149 args,
150 emits_std_dylib: false,
151 error: None,
152 },
153 Err(error) => Rewrite {
154 args: args.to_vec(),
155 emits_std_dylib: false,
156 error: Some(error),
157 },
158 }
159}
160
161fn rewrite_std_unit(args: &[OsString], wait_timeout: Duration) -> Vec<OsString> {
167 let is_rlib_type = |value: &OsStr| value == "rlib" || value == "lib";
168 let mut rewritten = Vec::with_capacity(args.len() + 8);
169 let mut index = 0;
170 while index < args.len() {
171 let arg = &args[index];
172 let split =
173 arg == "--crate-type" && args.get(index + 1).is_some_and(|value| is_rlib_type(value));
174 let joined = arg == "--crate-type=rlib" || arg == "--crate-type=lib";
175 if !split && !joined {
176 rewritten.push(arg.clone());
177 index += 1;
178 continue;
179 }
180 rewritten.push(arg.clone());
181 if split {
182 rewritten.push(args[index + 1].clone());
183 index += 2;
184 } else {
185 index += 1;
186 }
187 rewritten.extend([OsString::from("--crate-type"), OsString::from("dylib")]);
188 }
189 for spec in extern_values(args) {
190 let spec = spec.to_string_lossy();
191 if !spec.ends_with(".rmeta") {
192 continue;
193 }
194 let rlib = format!("{}.rlib", spec.trim_end_matches(".rmeta"));
195 let Some((_, path)) = rlib.rsplit_once('=') else {
196 continue;
197 };
198 if !wait_for_file(Path::new(path), wait_timeout) {
204 eprintln!(
205 "water: build-std dependency rlib never appeared: {path}; \
206 compiling std without it will fail"
207 );
208 continue;
209 }
210 rewritten.push(OsString::from("--extern"));
211 rewritten.push(OsString::from(rlib));
212 }
213 rewritten
214}
215
216fn wait_until(timeout: Duration, mut ready: impl FnMut() -> bool) -> bool {
219 const POLL: Duration = Duration::from_millis(20);
220 let deadline = Instant::now() + timeout;
221 while !ready() && Instant::now() < deadline {
222 std::thread::sleep(POLL);
223 }
224 ready()
225}
226
227fn wait_for_file(path: &Path, timeout: Duration) -> bool {
230 wait_until(timeout, || path.is_file())
231}
232
233struct DylibTimeout {
236 dylib: PathBuf,
237 dep_info: Option<PathBuf>,
238 timeout: Duration,
239}
240
241impl std::fmt::Display for DylibTimeout {
242 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243 write!(
244 f,
245 "build-std libstd dylib never completed at {}; waited {:?} for ",
246 self.dylib.display(),
247 self.timeout
248 )?;
249 match &self.dep_info {
250 Some(dep) => write!(
251 f,
252 "the post-link dep-info {} or a fully written, parseable ELF",
253 dep.display()
254 ),
255 None => write!(f, "a fully written, parseable ELF"),
256 }
257 }
258}
259
260fn wait_for_std_dylib(dylib: &Path, timeout: Duration) -> Result<(), DylibTimeout> {
267 let dep_info = dylib_dep_info(dylib);
268 let ready = || {
269 dep_info.as_ref().is_some_and(|dep| dep.is_file())
270 || dylib.is_file() && elf_file_is_parseable(dylib)
271 };
272 if wait_until(timeout, ready) {
273 Ok(())
274 } else {
275 Err(DylibTimeout {
276 dylib: dylib.to_path_buf(),
277 dep_info,
278 timeout,
279 })
280 }
281}
282
283fn dylib_dep_info(dylib: &Path) -> Option<PathBuf> {
286 dylib
287 .file_stem()
288 .and_then(OsStr::to_str)
289 .and_then(|stem| stem.strip_prefix("lib"))
290 .map(|stem| dylib.with_file_name(format!("{stem}.d")))
291}
292
293fn elf_file_is_parseable(path: &Path) -> bool {
297 let Ok(bytes) = std::fs::read(path) else {
298 return false;
299 };
300 object::File::parse(&*bytes).is_ok()
301}
302
303fn arg_values<'a>(args: &'a [OsString], flag: &str) -> Vec<&'a OsStr> {
306 let mut values = Vec::new();
307 let mut iter = args.iter();
308 while let Some(arg) = iter.next() {
309 if arg == flag {
310 if let Some(value) = iter.next() {
311 values.push(value.as_os_str());
312 }
313 } else if let Some(value) = arg.to_str().and_then(|arg| {
314 arg.strip_prefix(flag)
315 .and_then(|rest| rest.strip_prefix('='))
316 }) {
317 values.push(OsStr::new(value));
318 }
319 }
320 values
321}
322
323fn links_native_artifact(args: &[OsString]) -> bool {
330 const LINKED: &[&str] = &["bin", "cdylib", "dylib", "staticlib", "proc-macro"];
331 let values = arg_values(args, "--crate-type");
332 values.is_empty()
333 || values.iter().any(|value| {
334 value
335 .to_string_lossy()
336 .split(',')
337 .any(|kind| LINKED.contains(&kind))
338 })
339}
340
341fn add_std_dylib_extern(
351 args: &[OsString],
352 wait_timeout: Duration,
353) -> Result<Vec<OsString>, DylibTimeout> {
354 if !emits_linked_output(args) || !links_native_artifact(args) {
355 return Ok(args.to_vec());
356 }
357 let mut rewritten = args.to_vec();
358 for spec in extern_values(args) {
359 let spec = spec.to_string_lossy();
360 let Some((name, path)) = spec.rsplit_once('=') else {
361 continue;
362 };
363 let is_std = name.rsplit(':').next() == Some("std")
364 && Path::new(path)
365 .file_name()
366 .is_some_and(|file| file.to_string_lossy().starts_with("libstd-"));
367 if !is_std {
368 continue;
369 }
370 let dylib = Path::new(path).with_extension("so");
371 wait_for_std_dylib(&dylib, wait_timeout)?;
378 rewritten.push(OsString::from("--extern"));
379 rewritten.push(OsString::from(format!("{}={}", name, dylib.display())));
380 }
381 Ok(rewritten)
382}
383
384fn extern_values(args: &[OsString]) -> Vec<OsString> {
387 let mut specs = Vec::new();
388 let mut iter = args.iter();
389 while let Some(arg) = iter.next() {
390 if arg == "--extern" {
391 if let Some(spec) = iter.next() {
392 specs.push(spec.clone());
393 }
394 } else if let Some(spec) = arg.to_str().and_then(|arg| arg.strip_prefix("--extern=")) {
395 specs.push(OsString::from(spec));
396 }
397 }
398 specs
399}
400
401fn arg_value<'a>(args: &'a [OsString], flag: &str) -> &'a OsStr {
404 arg_values(args, flag).first().copied().unwrap_or_default()
405}
406
407fn emits_linked_output(args: &[OsString]) -> bool {
410 let emit = arg_value(args, "--emit");
411 emit.is_empty() || emit.to_string_lossy().split(',').any(|kind| kind == "link")
412}
413
414fn publish_std_dylib(out_dir: &Path, publish_dir: &Path) -> std::io::Result<()> {
421 let mut produced: Vec<PathBuf> = std::fs::read_dir(out_dir)?
422 .filter_map(|entry| entry.ok().map(|entry| entry.path()))
423 .filter(|path| is_std_dylib_file_name(path.file_name()))
424 .collect();
425 produced.sort_unstable();
426 let [source] = produced.as_slice() else {
427 return Err(std::io::Error::new(
428 std::io::ErrorKind::NotFound,
429 format!(
430 "expected exactly one libstd-*.so in {}, found {}",
431 out_dir.display(),
432 produced.len()
433 ),
434 ));
435 };
436
437 std::fs::create_dir_all(publish_dir)?;
438 let destination = publish_dir.join(source.file_name().unwrap_or_default());
439 if source == &destination {
440 for entry in std::fs::read_dir(publish_dir)? {
441 let path = entry?.path();
442 if path != *source && is_std_dylib_file_name(path.file_name()) {
443 std::fs::remove_file(&path)?;
444 }
445 }
446 } else {
447 for entry in std::fs::read_dir(publish_dir)? {
448 let path = entry?.path();
449 if is_std_dylib_file_name(path.file_name()) {
450 std::fs::remove_file(&path)?;
451 }
452 }
453 std::fs::copy(source, &destination)?;
454 }
455 Ok(())
456}
457
458fn is_std_dylib_file_name(file_name: Option<&OsStr>) -> bool {
460 file_name.is_some_and(|name| {
461 name.to_string_lossy().starts_with("libstd-")
462 && Path::new(name).extension() == Some(OsStr::new("so"))
463 })
464}
465#[cfg(test)]
466mod tests {
467 use std::ffi::{OsStr, OsString};
468 use std::time::Duration;
469
470 use tempfile::tempdir;
471
472 use super::{arg_value, publish_std_dylib, rewrite_args};
473
474 fn os(strings: &[&str]) -> Vec<OsString> {
475 strings.iter().map(OsString::from).collect()
476 }
477
478 fn rewrite(args: &[OsString]) -> super::Rewrite {
479 rewrite_args(
480 args,
481 OsString::from("aarch64-linux-android").as_os_str(),
482 Duration::ZERO,
483 )
484 }
485
486 #[test]
487 fn leaves_units_for_other_targets_alone() {
488 let args = os(&[
489 "--crate-name",
490 "std",
491 "--crate-type",
492 "rlib",
493 "--target",
494 "x86_64-linux-android",
495 ]);
496 let rewritten = rewrite(&args);
497 assert_eq!(rewritten.args, args);
498 assert!(!rewritten.emits_std_dylib);
499 }
500
501 #[test]
502 fn leaves_host_units_alone() {
503 let args = os(&["--crate-name", "std", "--crate-type", "rlib"]);
504 let rewritten = rewrite(&args);
505 assert_eq!(rewritten.args, args);
506 assert!(!rewritten.emits_std_dylib);
507 }
508
509 #[test]
510 fn std_unit_gains_dylib_and_rlib_externs() {
511 let dir = tempdir().expect("deps dir");
512 let rmeta = dir.path().join("libcore-abc.rmeta");
513 let rlib = dir.path().join("libcore-abc.rlib");
514 std::fs::write(&rmeta, []).expect("rmeta");
515 std::fs::write(&rlib, []).expect("rlib");
516
517 let args = os(&[
518 "--crate-name",
519 "std",
520 "--crate-type",
521 "rlib",
522 "--target",
523 "aarch64-linux-android",
524 "--extern",
525 &format!("noprelude:core={}", rmeta.display()),
526 ]);
527 let rewritten = rewrite(&args);
528 assert!(rewritten.emits_std_dylib);
529 assert!(
530 rewritten
531 .args
532 .windows(2)
533 .any(|w| w == [OsString::from("--crate-type"), OsString::from("dylib")])
534 );
535 let expected = OsString::from(format!(
536 "noprelude:core={}.rlib",
537 rmeta.display().to_string().trim_end_matches(".rmeta")
538 ));
539 assert!(rewritten.args.contains(&expected));
540 }
541
542 #[test]
543 fn dependents_get_the_std_dylib_extern_for_rlib_and_rmeta() {
544 let dir = tempdir().expect("std out dir");
545 let rlib = dir.path().join("libstd-abc123.rlib");
546 let rmeta = dir.path().join("libstd-def456.rmeta");
547 let dylib_rlib = dir.path().join("libstd-abc123.so");
548 let dylib_rmeta = dir.path().join("libstd-def456.so");
549 std::fs::write(&rlib, []).expect("rlib");
550 std::fs::write(&rmeta, []).expect("rmeta");
551 std::fs::write(&dylib_rlib, []).expect("dylib for rlib extern");
552 std::fs::write(&dylib_rmeta, []).expect("dylib for rmeta extern");
553 std::fs::write(dir.path().join("std-abc123.d"), []).expect("dep-info");
556 std::fs::write(dir.path().join("std-def456.d"), []).expect("dep-info");
557
558 let args = os(&[
559 "--crate-name",
560 "waterui_preview",
561 "--crate-type",
562 "cdylib",
563 "--target",
564 "aarch64-linux-android",
565 "--emit=dep-info,metadata,link",
566 "--extern",
567 &format!("noprelude,nounused:std={}", rlib.display()),
568 "--extern",
569 &format!("std={}", rmeta.display()),
570 "--extern",
571 &format!("std_detect={}/libstd_detect-zz.rlib", dir.path().display()),
572 ]);
573 let rewritten = rewrite(&args);
574 for expected in [
575 format!("noprelude,nounused:std={}", dylib_rlib.display()),
576 format!("std={}", dylib_rmeta.display()),
577 ] {
578 assert!(
579 rewritten.args.contains(&OsString::from(&expected)),
580 "missing std dylib extern {expected}: {:?}",
581 rewritten.args
582 );
583 }
584 assert!(
585 !rewritten
586 .args
587 .iter()
588 .any(|arg| arg.to_string_lossy().contains("libstd_detect-zz.so")),
589 "std_detect must not be rewritten"
590 );
591 }
592
593 #[test]
594 fn rlib_units_pass_through_without_waiting_for_the_dylib() {
595 let dir = tempdir().expect("std out dir");
596 let rlib = dir.path().join("libstd-abc123.rlib");
597 std::fs::write(&rlib, []).expect("rlib");
598 let args = os(&[
602 "--crate-name",
603 "waterui_dep",
604 "--crate-type",
605 "rlib",
606 "--target",
607 "aarch64-linux-android",
608 "--emit=dep-info,link",
609 "--extern",
610 &format!("std={}", rlib.display()),
611 ]);
612 let rewritten = rewrite(&args);
613 assert_eq!(rewritten.args, args);
614 assert!(rewritten.error.is_none());
615 }
616
617 #[test]
618 fn a_dylib_that_never_completes_fails_the_unit() {
619 let dir = tempdir().expect("std out dir");
620 let rlib = dir.path().join("libstd-abc123.rlib");
621 std::fs::write(&rlib, []).expect("rlib");
622 let args = os(&[
625 "--crate-name",
626 "waterui_preview",
627 "--crate-type",
628 "cdylib",
629 "--target",
630 "aarch64-linux-android",
631 "--emit=dep-info,metadata,link",
632 "--extern",
633 &format!("std={}", rlib.display()),
634 ]);
635 let rewritten = rewrite(&args);
636 let error = rewritten
637 .error
638 .expect("a missing dylib must fail the unit, not link `std` statically");
639 let message = error.to_string();
640 let dylib = dir.path().join("libstd-abc123.so");
641 assert!(
642 message.contains(&dylib.display().to_string()),
643 "names the dylib it waited for: {message}"
644 );
645 assert!(
646 message.contains("std-abc123.d"),
647 "names the dep-info completion signal: {message}"
648 );
649 assert!(
650 message.contains("0ns"),
651 "names the wait deadline: {message}"
652 );
653 }
654
655 #[test]
656 fn metadata_only_units_are_not_rewritten() {
657 let dir = tempdir().expect("std out dir");
658 let rmeta = dir.path().join("libstd-abc123.rmeta");
659 let dylib = dir.path().join("libstd-abc123.so");
660 std::fs::write(&rmeta, []).expect("rmeta");
661 std::fs::write(&dylib, []).expect("dylib");
662
663 let args = os(&[
664 "--crate-name",
665 "waterui_preview",
666 "--target",
667 "aarch64-linux-android",
668 "--emit=dep-info,metadata",
669 "--extern",
670 &format!("std={}", rmeta.display()),
671 ]);
672 let rewritten = rewrite(&args);
673 assert_eq!(rewritten.args, args);
674 }
675
676 #[test]
677 fn publishes_the_dylib_and_replaces_stale_ones() {
678 let out = tempdir().expect("out dir");
679 let publish = tempdir().expect("publish dir");
680 std::fs::write(out.path().join("libstd-new.so"), b"new").expect("new libstd");
681 std::fs::write(publish.path().join("libstd-old.so"), b"old").expect("stale libstd");
682 std::fs::write(publish.path().join("libwaterui_dylib.so"), b"w").expect("other lib");
683
684 publish_std_dylib(out.path(), publish.path()).expect("publish");
685
686 assert!(!publish.path().join("libstd-old.so").exists());
687 assert_eq!(
688 std::fs::read(publish.path().join("libstd-new.so")).expect("read published"),
689 b"new"
690 );
691 assert!(publish.path().join("libwaterui_dylib.so").exists());
692 }
693
694 #[test]
695 fn arg_value_reads_split_and_joined_forms() {
696 let args = os(&["--out-dir", "/tmp/out", "--emit=dep-info,metadata,link"]);
697 assert_eq!(arg_value(&args, "--out-dir"), OsStr::new("/tmp/out"));
698 assert_eq!(arg_value(&args, "--emit"), "dep-info,metadata,link");
699 assert_eq!(arg_value(&args, "--missing"), "");
700 }
701}