Skip to main content

waterui_cli/workflows/
rustc_wrapper.rs

1//! `RUSTC_WRAPPER` shim that recovers the `dylib` half of a `-Zbuild-std`
2//! standard library.
3//!
4//! Cargo deliberately strips the `dylib` crate type from `std` when the
5//! standard library is built from source (`-Zbuild-std`), so the only shared
6//! `libstd` a `-Cprefer-dynamic` link can pick is rustup's prebuilt one — and
7//! rustup ships the Android `libstd` with 4 KB-aligned `LOAD` segments, which
8//! a device with 16 KB pages refuses to map.
9//!
10//! This wrapper supplies the missing half at the exact point Cargo cannot
11//! express it. For the `std` unit of the configured target it appends
12//! `--crate-type dylib` to the same rustc invocation that emits the rlib, so
13//! both artifacts carry one strict version hash and every dependent crate
14//! accepts the dylib as the same `std`. And for every other unit that
15//! receives the `std` rlib through `--extern`, it appends the produced `.so`
16//! as a second `std` extern: a crate named on the command line is never
17//! re-resolved against the library search path, so the sibling dylib has to
18//! be passed explicitly for `prefer-dynamic` to find it.
19//!
20//! The `water` binary itself is the wrapper (`RUSTC_WRAPPER` points at the
21//! running executable) so the shim ships inside the same artifact a
22//! `cargo install` produces. Cargo invokes the wrapper as
23//! `water <rustc> <args…>`; the process enters here only when the cargo
24//! invocation that spawned it also exported [`WRAPPER_MODE_ENV`], so a normal
25//! `water` run never wanders into this path.
26
27use std::ffi::{OsStr, OsString};
28use std::path::{Path, PathBuf};
29use std::time::{Duration, Instant};
30
31/// How long a rewritten unit may wait on a sibling artifact — the produced
32/// `libstd` dylib or a pipelined dep rlib — before the wait is declared
33/// failed. `-Zbuild-std` codegen can run for minutes; the bound only keeps
34/// a genuinely missing artifact from hanging the build forever.
35const ARTIFACT_WAIT_TIMEOUT: Duration = Duration::from_secs(600);
36
37/// Marks the `water` process as a Cargo rustc wrapper rather than a CLI.
38pub const WRAPPER_MODE_ENV: &str = "WATERUI_INTERNAL_RUSTC_WRAPPER";
39/// Optional second wrapper (e.g. `sccache`) invoked between this shim and
40/// rustc; unset means the rewritten arguments go to rustc directly.
41pub const WRAPPER_CHAIN_ENV: &str = "WATERUI_RUSTC_WRAPPER_CHAIN";
42/// The only `--target` triple this shim rewrites.
43///
44/// Host units (build scripts, proc macros) never carry the flag and are
45/// passed through untouched, which keeps their host standard library on the
46/// default rlib path.
47pub const BUILD_STD_TARGET_ENV: &str = "WATERUI_BUILD_STD_TARGET";
48/// Directory the produced `libstd-*.so` is published into — the Cargo profile
49/// `deps/` directory, where the packaging step stages it from.
50pub const BUILD_STD_DYLIB_DIR_ENV: &str = "WATERUI_BUILD_STD_DYLIB_DIR";
51
52/// Run as a Cargo rustc wrapper when the invoking cargo configured us as one.
53///
54/// Returns `Some(exit_code)` in wrapper mode; the caller should exit the
55/// process with it. `None` means this is an ordinary CLI invocation.
56#[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    // A unit that needed the shared `std` dylib and never saw it complete
73    // fails here: invoking rustc would produce a statically linked artifact
74    // that shares no runtime with the app.
75    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
112/// The result of rewriting one rustc invocation.
113struct Rewrite {
114    args: Vec<OsString>,
115    /// This invocation is the configured target's `std` unit with `dylib`
116    /// added — on success a `libstd-*.so` exists under its `--out-dir`.
117    emits_std_dylib: bool,
118    /// A link-emitting unit's `std` dylib never completed; the unit must
119    /// fail rather than fall back to a static `std` link.
120    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    // Cargo spells the rlib crate type either `rlib` or `lib`; both need the
133    // dylib companion.
134    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    // A metadata-only pass emits no dylib; rewriting it would only clobber
138    // the rlib's `libstd-*.rmeta` with a second SVH.
139    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
161/// Compile the `std` unit as `rlib` + `dylib` in one invocation.
162///
163/// The dylib emits the dep `rlib`s' machine code, so each rmeta-only extern
164/// Cargo hands the unit gains its rlib sibling as a second location for the
165/// same crate — one candidate per artifact kind, one hash.
166fn 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        // Cargo pipelines the `std` unit behind its dependencies' metadata
199        // alone: the `dylib` crate type added above codegens against the dep
200        // rlibs, which the still-running dep units have not written yet. The
201        // rlib appears when that unit finishes; waiting for the file — never
202        // for a fixed delay — is the only ordering the shim can impose.
203        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
216/// Poll until `ready` holds, up to `timeout`. Returns `false` on timeout
217/// so the caller can fail the unit instead of silently degrading.
218fn 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
227/// Poll until `path` exists — safe only for artifacts rustc renames into
228/// place (rlibs, rmeta), where presence already means complete.
229fn wait_for_file(path: &Path, timeout: Duration) -> bool {
230    wait_until(timeout, || path.is_file())
231}
232
233/// A link-emitting unit's wait for the shared `libstd` dylib expired — the
234/// unit fails with this diagnostic instead of linking `std` statically.
235struct 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
260/// Wait for the produced `libstd-*.so` to be *complete*, not merely present:
261/// the native linker streams the dylib to its final path, so `is_file` can
262/// observe a partially written file a dependent's rustc would then fail to
263/// read. The real completion signal is the unit's dep-info `std-<hash>.d`,
264/// which rustc writes only after linking returns; when no dep-info is
265/// emitted, the `.so` must instead parse as a complete ELF before use.
266fn 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
283/// The dep-info path rustc writes next to the `libstd-*.so` once its link
284/// returns — `libstd-<hash>.so` ⇒ `std-<hash>.d`.
285fn 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
293/// Whether `path` currently holds a completely written, parseable ELF — the
294/// readiness fallback for the `libstd` dylib when the `std` unit emits no
295/// dep-info. A file the linker is still streaming fails to parse.
296fn 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
303/// Every value paired with `flag` (split and joined forms); a flag may be
304/// repeated.
305fn 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
323/// Whether the invocation's `--crate-type` list names a kind the linker
324/// writes — `bin`/`cdylib`/`dylib`/`staticlib`/`proc-macro`. rlib and rmeta
325/// units only archive metadata and never resolve `std` at link time, so
326/// rewriting them would only serialize pipelined dependents behind the
327/// dylib wait. An absent `--crate-type` keeps the conservative answer:
328/// rustc's default crate type is a linked kind.
329fn 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
341/// Hand every link-emitting unit that depends on `std` the freshly built
342/// dylib as a second `std` extern, so `-Cprefer-dynamic` resolves to it.
343///
344/// Cargo may express the dependency as either the `libstd-*.rlib` or the
345/// `libstd-*.rmeta` produced in the `std` unit's own out dir; the dylib sits
346/// next to both. A unit that only reads metadata never links `std`, so only
347/// `--emit` sets containing `link` *and* `--crate-type`s that link natively
348/// are rewritten — and only those wait for the dylib, since Cargo starts
349/// them no earlier than the unit producing it.
350fn 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        // The `std` unit emits the dylib at the end of the same invocation
372        // that wrote the rmeta/rlib this extern points at; if pipelining let
373        // this unit start early, the dylib is on its way — wait for the
374        // post-link completion signal. A dylib that never completes fails
375        // the unit: continuing would link `std` statically and silently
376        // produce a module that shares no runtime with the app.
377        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
384/// Every `--extern` spec in the invocation, covering both the split
385/// (`--extern <spec>`) and joined (`--extern=<spec>`) forms.
386fn 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
401/// The first value paired with `flag` in split or joined (`--flag=value`)
402/// form.
403fn arg_value<'a>(args: &'a [OsString], flag: &str) -> &'a OsStr {
404    arg_values(args, flag).first().copied().unwrap_or_default()
405}
406
407/// Whether this invocation links an artifact (as opposed to a metadata-only
408/// check emit), which is when the added dylib actually lands on disk.
409fn 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
414/// Move the produced `libstd-*.so` from the unit's out dir into the profile's
415/// `deps/` directory, replacing whatever an earlier toolchain left there.
416///
417/// Cargo compiles the `std` unit straight into `deps/`, so the usual case is
418/// `out_dir == publish_dir`: the produced file must survive the stale sweep,
419/// and a copy onto itself would truncate it.
420fn 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
458/// Whether a directory entry is a `libstd-*.so` shared library.
459fn 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        // rustc writes dep-info after the link finishes — its presence, not
554        // the `.so` appearing, is what marks the dylib complete.
555        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        // No `std-*.d` dep-info and no `.so` at all — a linked-kind unit
599        // would block on the dylib wait here; an rlib unit archives metadata
600        // only, so it must pass through untouched and unblocked.
601        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        // Neither the `.so` nor its post-link `std-*.d` dep-info appears —
623        // the unit must fail rather than link `std` statically.
624        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}