use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
const ARTIFACT_WAIT_TIMEOUT: Duration = Duration::from_secs(600);
pub const WRAPPER_MODE_ENV: &str = "WATERUI_INTERNAL_RUSTC_WRAPPER";
pub const WRAPPER_CHAIN_ENV: &str = "WATERUI_RUSTC_WRAPPER_CHAIN";
pub const BUILD_STD_TARGET_ENV: &str = "WATERUI_BUILD_STD_TARGET";
pub const BUILD_STD_DYLIB_DIR_ENV: &str = "WATERUI_BUILD_STD_DYLIB_DIR";
#[must_use]
pub fn wrapper_main() -> Option<i32> {
std::env::var_os(WRAPPER_MODE_ENV)?;
Some(run_wrapper())
}
fn run_wrapper() -> i32 {
let mut invocation = std::env::args_os();
let _self = invocation.next();
let Some(rustc) = invocation.next() else {
eprintln!("water: rustc wrapper invoked without a rustc path");
return 1;
};
let args: Vec<OsString> = invocation.collect();
let target = std::env::var_os(BUILD_STD_TARGET_ENV).unwrap_or_default();
let rewritten = rewrite_args(&args, &target, ARTIFACT_WAIT_TIMEOUT);
if let Some(error) = rewritten.error {
eprintln!("water: {error}");
return 1;
}
let status = match std::env::var_os(WRAPPER_CHAIN_ENV) {
Some(chain) => std::process::Command::new(chain)
.arg(&rustc)
.args(&rewritten.args)
.status(),
None => std::process::Command::new(&rustc)
.args(&rewritten.args)
.status(),
};
let status = match status {
Ok(status) => status,
Err(error) => {
eprintln!("water: failed to invoke rustc wrapper target: {error}");
return 1;
}
};
if status.success()
&& rewritten.emits_std_dylib
&& emits_linked_output(&args)
&& let Some(publish_dir) = std::env::var_os(BUILD_STD_DYLIB_DIR_ENV)
{
let out_dir = arg_value(&args, "--out-dir");
if let Err(error) = publish_std_dylib(Path::new(out_dir), Path::new(&publish_dir)) {
eprintln!("water: failed to stage the build-std libstd dylib: {error}");
return 1;
}
}
status.code().unwrap_or(1)
}
struct Rewrite {
args: Vec<OsString>,
emits_std_dylib: bool,
error: Option<DylibTimeout>,
}
fn rewrite_args(args: &[OsString], target: &OsStr, wait_timeout: Duration) -> Rewrite {
if target.is_empty() || arg_value(args, "--target") != target {
return Rewrite {
args: args.to_vec(),
emits_std_dylib: false,
error: None,
};
}
let std_crate_type = arg_value(args, "--crate-type");
let is_std_rlib = arg_value(args, "--crate-name") == OsStr::new("std")
&& (std_crate_type == "rlib" || std_crate_type == "lib");
if is_std_rlib && emits_linked_output(args) {
return Rewrite {
args: rewrite_std_unit(args, wait_timeout),
emits_std_dylib: true,
error: None,
};
}
match add_std_dylib_extern(args, wait_timeout) {
Ok(args) => Rewrite {
args,
emits_std_dylib: false,
error: None,
},
Err(error) => Rewrite {
args: args.to_vec(),
emits_std_dylib: false,
error: Some(error),
},
}
}
fn rewrite_std_unit(args: &[OsString], wait_timeout: Duration) -> Vec<OsString> {
let is_rlib_type = |value: &OsStr| value == "rlib" || value == "lib";
let mut rewritten = Vec::with_capacity(args.len() + 8);
let mut index = 0;
while index < args.len() {
let arg = &args[index];
let split =
arg == "--crate-type" && args.get(index + 1).is_some_and(|value| is_rlib_type(value));
let joined = arg == "--crate-type=rlib" || arg == "--crate-type=lib";
if !split && !joined {
rewritten.push(arg.clone());
index += 1;
continue;
}
rewritten.push(arg.clone());
if split {
rewritten.push(args[index + 1].clone());
index += 2;
} else {
index += 1;
}
rewritten.extend([OsString::from("--crate-type"), OsString::from("dylib")]);
}
for spec in extern_values(args) {
let spec = spec.to_string_lossy();
if !spec.ends_with(".rmeta") {
continue;
}
let rlib = format!("{}.rlib", spec.trim_end_matches(".rmeta"));
let Some((_, path)) = rlib.rsplit_once('=') else {
continue;
};
if !wait_for_file(Path::new(path), wait_timeout) {
eprintln!(
"water: build-std dependency rlib never appeared: {path}; \
compiling std without it will fail"
);
continue;
}
rewritten.push(OsString::from("--extern"));
rewritten.push(OsString::from(rlib));
}
rewritten
}
fn wait_until(timeout: Duration, mut ready: impl FnMut() -> bool) -> bool {
const POLL: Duration = Duration::from_millis(20);
let deadline = Instant::now() + timeout;
while !ready() && Instant::now() < deadline {
std::thread::sleep(POLL);
}
ready()
}
fn wait_for_file(path: &Path, timeout: Duration) -> bool {
wait_until(timeout, || path.is_file())
}
struct DylibTimeout {
dylib: PathBuf,
dep_info: Option<PathBuf>,
timeout: Duration,
}
impl std::fmt::Display for DylibTimeout {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"build-std libstd dylib never completed at {}; waited {:?} for ",
self.dylib.display(),
self.timeout
)?;
match &self.dep_info {
Some(dep) => write!(
f,
"the post-link dep-info {} or a fully written, parseable ELF",
dep.display()
),
None => write!(f, "a fully written, parseable ELF"),
}
}
}
fn wait_for_std_dylib(dylib: &Path, timeout: Duration) -> Result<(), DylibTimeout> {
let dep_info = dylib_dep_info(dylib);
let ready = || {
dep_info.as_ref().is_some_and(|dep| dep.is_file())
|| dylib.is_file() && elf_file_is_parseable(dylib)
};
if wait_until(timeout, ready) {
Ok(())
} else {
Err(DylibTimeout {
dylib: dylib.to_path_buf(),
dep_info,
timeout,
})
}
}
fn dylib_dep_info(dylib: &Path) -> Option<PathBuf> {
dylib
.file_stem()
.and_then(OsStr::to_str)
.and_then(|stem| stem.strip_prefix("lib"))
.map(|stem| dylib.with_file_name(format!("{stem}.d")))
}
fn elf_file_is_parseable(path: &Path) -> bool {
let Ok(bytes) = std::fs::read(path) else {
return false;
};
object::File::parse(&*bytes).is_ok()
}
fn arg_values<'a>(args: &'a [OsString], flag: &str) -> Vec<&'a OsStr> {
let mut values = Vec::new();
let mut iter = args.iter();
while let Some(arg) = iter.next() {
if arg == flag {
if let Some(value) = iter.next() {
values.push(value.as_os_str());
}
} else if let Some(value) = arg.to_str().and_then(|arg| {
arg.strip_prefix(flag)
.and_then(|rest| rest.strip_prefix('='))
}) {
values.push(OsStr::new(value));
}
}
values
}
fn links_native_artifact(args: &[OsString]) -> bool {
const LINKED: &[&str] = &["bin", "cdylib", "dylib", "staticlib", "proc-macro"];
let values = arg_values(args, "--crate-type");
values.is_empty()
|| values.iter().any(|value| {
value
.to_string_lossy()
.split(',')
.any(|kind| LINKED.contains(&kind))
})
}
fn add_std_dylib_extern(
args: &[OsString],
wait_timeout: Duration,
) -> Result<Vec<OsString>, DylibTimeout> {
if !emits_linked_output(args) || !links_native_artifact(args) {
return Ok(args.to_vec());
}
let mut rewritten = args.to_vec();
for spec in extern_values(args) {
let spec = spec.to_string_lossy();
let Some((name, path)) = spec.rsplit_once('=') else {
continue;
};
let is_std = name.rsplit(':').next() == Some("std")
&& Path::new(path)
.file_name()
.is_some_and(|file| file.to_string_lossy().starts_with("libstd-"));
if !is_std {
continue;
}
let dylib = Path::new(path).with_extension("so");
wait_for_std_dylib(&dylib, wait_timeout)?;
rewritten.push(OsString::from("--extern"));
rewritten.push(OsString::from(format!("{}={}", name, dylib.display())));
}
Ok(rewritten)
}
fn extern_values(args: &[OsString]) -> Vec<OsString> {
let mut specs = Vec::new();
let mut iter = args.iter();
while let Some(arg) = iter.next() {
if arg == "--extern" {
if let Some(spec) = iter.next() {
specs.push(spec.clone());
}
} else if let Some(spec) = arg.to_str().and_then(|arg| arg.strip_prefix("--extern=")) {
specs.push(OsString::from(spec));
}
}
specs
}
fn arg_value<'a>(args: &'a [OsString], flag: &str) -> &'a OsStr {
arg_values(args, flag).first().copied().unwrap_or_default()
}
fn emits_linked_output(args: &[OsString]) -> bool {
let emit = arg_value(args, "--emit");
emit.is_empty() || emit.to_string_lossy().split(',').any(|kind| kind == "link")
}
fn publish_std_dylib(out_dir: &Path, publish_dir: &Path) -> std::io::Result<()> {
let mut produced: Vec<PathBuf> = std::fs::read_dir(out_dir)?
.filter_map(|entry| entry.ok().map(|entry| entry.path()))
.filter(|path| is_std_dylib_file_name(path.file_name()))
.collect();
produced.sort_unstable();
let [source] = produced.as_slice() else {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!(
"expected exactly one libstd-*.so in {}, found {}",
out_dir.display(),
produced.len()
),
));
};
std::fs::create_dir_all(publish_dir)?;
let destination = publish_dir.join(source.file_name().unwrap_or_default());
if source == &destination {
for entry in std::fs::read_dir(publish_dir)? {
let path = entry?.path();
if path != *source && is_std_dylib_file_name(path.file_name()) {
std::fs::remove_file(&path)?;
}
}
} else {
for entry in std::fs::read_dir(publish_dir)? {
let path = entry?.path();
if is_std_dylib_file_name(path.file_name()) {
std::fs::remove_file(&path)?;
}
}
std::fs::copy(source, &destination)?;
}
Ok(())
}
fn is_std_dylib_file_name(file_name: Option<&OsStr>) -> bool {
file_name.is_some_and(|name| {
name.to_string_lossy().starts_with("libstd-")
&& Path::new(name).extension() == Some(OsStr::new("so"))
})
}
#[cfg(test)]
mod tests {
use std::ffi::{OsStr, OsString};
use std::time::Duration;
use tempfile::tempdir;
use super::{arg_value, publish_std_dylib, rewrite_args};
fn os(strings: &[&str]) -> Vec<OsString> {
strings.iter().map(OsString::from).collect()
}
fn rewrite(args: &[OsString]) -> super::Rewrite {
rewrite_args(
args,
OsString::from("aarch64-linux-android").as_os_str(),
Duration::ZERO,
)
}
#[test]
fn leaves_units_for_other_targets_alone() {
let args = os(&[
"--crate-name",
"std",
"--crate-type",
"rlib",
"--target",
"x86_64-linux-android",
]);
let rewritten = rewrite(&args);
assert_eq!(rewritten.args, args);
assert!(!rewritten.emits_std_dylib);
}
#[test]
fn leaves_host_units_alone() {
let args = os(&["--crate-name", "std", "--crate-type", "rlib"]);
let rewritten = rewrite(&args);
assert_eq!(rewritten.args, args);
assert!(!rewritten.emits_std_dylib);
}
#[test]
fn std_unit_gains_dylib_and_rlib_externs() {
let dir = tempdir().expect("deps dir");
let rmeta = dir.path().join("libcore-abc.rmeta");
let rlib = dir.path().join("libcore-abc.rlib");
std::fs::write(&rmeta, []).expect("rmeta");
std::fs::write(&rlib, []).expect("rlib");
let args = os(&[
"--crate-name",
"std",
"--crate-type",
"rlib",
"--target",
"aarch64-linux-android",
"--extern",
&format!("noprelude:core={}", rmeta.display()),
]);
let rewritten = rewrite(&args);
assert!(rewritten.emits_std_dylib);
assert!(
rewritten
.args
.windows(2)
.any(|w| w == [OsString::from("--crate-type"), OsString::from("dylib")])
);
let expected = OsString::from(format!(
"noprelude:core={}.rlib",
rmeta.display().to_string().trim_end_matches(".rmeta")
));
assert!(rewritten.args.contains(&expected));
}
#[test]
fn dependents_get_the_std_dylib_extern_for_rlib_and_rmeta() {
let dir = tempdir().expect("std out dir");
let rlib = dir.path().join("libstd-abc123.rlib");
let rmeta = dir.path().join("libstd-def456.rmeta");
let dylib_rlib = dir.path().join("libstd-abc123.so");
let dylib_rmeta = dir.path().join("libstd-def456.so");
std::fs::write(&rlib, []).expect("rlib");
std::fs::write(&rmeta, []).expect("rmeta");
std::fs::write(&dylib_rlib, []).expect("dylib for rlib extern");
std::fs::write(&dylib_rmeta, []).expect("dylib for rmeta extern");
std::fs::write(dir.path().join("std-abc123.d"), []).expect("dep-info");
std::fs::write(dir.path().join("std-def456.d"), []).expect("dep-info");
let args = os(&[
"--crate-name",
"waterui_preview",
"--crate-type",
"cdylib",
"--target",
"aarch64-linux-android",
"--emit=dep-info,metadata,link",
"--extern",
&format!("noprelude,nounused:std={}", rlib.display()),
"--extern",
&format!("std={}", rmeta.display()),
"--extern",
&format!("std_detect={}/libstd_detect-zz.rlib", dir.path().display()),
]);
let rewritten = rewrite(&args);
for expected in [
format!("noprelude,nounused:std={}", dylib_rlib.display()),
format!("std={}", dylib_rmeta.display()),
] {
assert!(
rewritten.args.contains(&OsString::from(&expected)),
"missing std dylib extern {expected}: {:?}",
rewritten.args
);
}
assert!(
!rewritten
.args
.iter()
.any(|arg| arg.to_string_lossy().contains("libstd_detect-zz.so")),
"std_detect must not be rewritten"
);
}
#[test]
fn rlib_units_pass_through_without_waiting_for_the_dylib() {
let dir = tempdir().expect("std out dir");
let rlib = dir.path().join("libstd-abc123.rlib");
std::fs::write(&rlib, []).expect("rlib");
let args = os(&[
"--crate-name",
"waterui_dep",
"--crate-type",
"rlib",
"--target",
"aarch64-linux-android",
"--emit=dep-info,link",
"--extern",
&format!("std={}", rlib.display()),
]);
let rewritten = rewrite(&args);
assert_eq!(rewritten.args, args);
assert!(rewritten.error.is_none());
}
#[test]
fn a_dylib_that_never_completes_fails_the_unit() {
let dir = tempdir().expect("std out dir");
let rlib = dir.path().join("libstd-abc123.rlib");
std::fs::write(&rlib, []).expect("rlib");
let args = os(&[
"--crate-name",
"waterui_preview",
"--crate-type",
"cdylib",
"--target",
"aarch64-linux-android",
"--emit=dep-info,metadata,link",
"--extern",
&format!("std={}", rlib.display()),
]);
let rewritten = rewrite(&args);
let error = rewritten
.error
.expect("a missing dylib must fail the unit, not link `std` statically");
let message = error.to_string();
let dylib = dir.path().join("libstd-abc123.so");
assert!(
message.contains(&dylib.display().to_string()),
"names the dylib it waited for: {message}"
);
assert!(
message.contains("std-abc123.d"),
"names the dep-info completion signal: {message}"
);
assert!(
message.contains("0ns"),
"names the wait deadline: {message}"
);
}
#[test]
fn metadata_only_units_are_not_rewritten() {
let dir = tempdir().expect("std out dir");
let rmeta = dir.path().join("libstd-abc123.rmeta");
let dylib = dir.path().join("libstd-abc123.so");
std::fs::write(&rmeta, []).expect("rmeta");
std::fs::write(&dylib, []).expect("dylib");
let args = os(&[
"--crate-name",
"waterui_preview",
"--target",
"aarch64-linux-android",
"--emit=dep-info,metadata",
"--extern",
&format!("std={}", rmeta.display()),
]);
let rewritten = rewrite(&args);
assert_eq!(rewritten.args, args);
}
#[test]
fn publishes_the_dylib_and_replaces_stale_ones() {
let out = tempdir().expect("out dir");
let publish = tempdir().expect("publish dir");
std::fs::write(out.path().join("libstd-new.so"), b"new").expect("new libstd");
std::fs::write(publish.path().join("libstd-old.so"), b"old").expect("stale libstd");
std::fs::write(publish.path().join("libwaterui_dylib.so"), b"w").expect("other lib");
publish_std_dylib(out.path(), publish.path()).expect("publish");
assert!(!publish.path().join("libstd-old.so").exists());
assert_eq!(
std::fs::read(publish.path().join("libstd-new.so")).expect("read published"),
b"new"
);
assert!(publish.path().join("libwaterui_dylib.so").exists());
}
#[test]
fn arg_value_reads_split_and_joined_forms() {
let args = os(&["--out-dir", "/tmp/out", "--emit=dep-info,metadata,link"]);
assert_eq!(arg_value(&args, "--out-dir"), OsStr::new("/tmp/out"));
assert_eq!(arg_value(&args, "--emit"), "dep-info,metadata,link");
assert_eq!(arg_value(&args, "--missing"), "");
}
}