use std::{
ffi::OsString,
io::{self, Write as _},
path::{Path, PathBuf},
process::Stdio,
};
use eyre::{Context as _, bail};
use futures_util::StreamExt as _;
use smol::{io::AsyncReadExt as _, process::Command, unblock};
use target_lexicon::{Environment, OperatingSystem, Triple};
use crate::project::Project;
use crate::utils::{run_command, std_output_enabled};
#[must_use]
pub const fn lib_extension_for_triple(triple: &Triple) -> &'static str {
match triple.operating_system {
OperatingSystem::Darwin(_)
| OperatingSystem::MacOSX { .. }
| OperatingSystem::IOS(_)
| OperatingSystem::TvOS(_)
| OperatingSystem::WatchOS(_)
| OperatingSystem::VisionOS(_) => "dylib",
OperatingSystem::Windows => "dll",
_ => "so",
}
}
pub async fn project_toolchain(project: &Project) -> eyre::Result<String> {
Ok(crate::toolchain::rust::project_rustup_toolchain(project.root()).await?)
}
pub async fn rust_target_libdir(triple: &Triple, toolchain: &str) -> eyre::Result<PathBuf> {
let target = triple.to_string();
let host = crate::toolchain::Host::current().with_env("RUSTUP_TOOLCHAIN", toolchain);
let output = host
.run(
"rustc",
["--print", "target-libdir", "--target", target.as_str()],
)
.await?;
let libdir = output.trim();
if libdir.is_empty() {
bail!("`rustc --print target-libdir --target {target}` returned an empty path");
}
let path = PathBuf::from(libdir);
if !path.is_dir() {
bail!(
"Rust target libdir does not exist for dynamic linking: {}",
path.display()
);
}
Ok(path)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CargoTarget<'a> {
Lib,
Binary(&'a str),
}
impl<'a> CargoTarget<'a> {
fn cargo_args(self) -> Vec<&'a str> {
match self {
Self::Lib => vec!["--lib"],
Self::Binary(name) => vec!["--bin", name],
}
}
const fn accepts_crate_type_override(self) -> bool {
matches!(self, Self::Lib)
}
fn matches(&self, target: &cargo_metadata::Target) -> bool {
use cargo_metadata::TargetKind;
match self {
Self::Binary(name) => {
target.name.as_str() == *name && target.kind.contains(&TargetKind::Bin)
}
Self::Lib => target.kind.iter().any(|kind| {
matches!(
kind,
TargetKind::Lib
| TargetKind::RLib
| TargetKind::DyLib
| TargetKind::CDyLib
| TargetKind::StaticLib
| TargetKind::ProcMacro
)
}),
}
}
}
#[derive(Debug)]
pub struct BuiltTarget {
pub profile_dir: PathBuf,
pub artifact: PathBuf,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RustLinkage {
Static,
SharedRuntime,
}
pub fn configure_generated_crate_compilation(command: &mut Command) {
command.env("CARGO_INCREMENTAL", "0");
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RustDynamicLibraries {
waterui: PathBuf,
standard_library: PathBuf,
triple: Triple,
}
impl RustDynamicLibraries {
pub async fn resolve(lib_dir: &Path, triple: &Triple, project: &Project) -> eyre::Result<Self> {
let file_name = dynamic_library_file_name("waterui_dylib", triple);
let waterui = [
lib_dir.join("deps").join(&file_name),
lib_dir.join(&file_name),
]
.into_iter()
.find(|path| path.is_file())
.ok_or_else(|| {
eyre::eyre!(
"Shared WaterUI runtime was not built at {}",
lib_dir.join("deps").join(&file_name).display()
)
})?;
let resolution_triple = triple.clone();
let deps_dir = lib_dir.join("deps");
let staged =
unblock(move || resolve_rust_standard_library_in(&deps_dir, &resolution_triple)).await;
let standard_library = match staged {
Ok(path) => path,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
let toolchain = project_toolchain(project).await?;
let target_libdir = rust_target_libdir(triple, &toolchain).await?;
let resolution_triple = triple.clone();
unblock(move || {
resolve_rust_standard_library_in(&target_libdir, &resolution_triple)
})
.await?
}
Err(error) => return Err(error.into()),
};
Ok(Self {
waterui,
standard_library,
triple: triple.clone(),
})
}
#[must_use]
pub fn waterui(&self) -> &Path {
&self.waterui
}
#[must_use]
pub fn standard_library(&self) -> &Path {
&self.standard_library
}
pub fn iter(&self) -> impl Iterator<Item = &Path> {
[self.waterui(), self.standard_library()].into_iter()
}
pub async fn stage(&self, destination: &Path) -> eyre::Result<()> {
smol::fs::create_dir_all(destination).await?;
let sources: Vec<PathBuf> = self.iter().map(|path| (*path).to_path_buf()).collect();
Self::remove_staged_except(destination, &self.triple, &sources).await?;
for source in &sources {
let file_name = source.file_name().ok_or_else(|| {
eyre::eyre!(
"Dynamic library path has no file name: {}",
source.display()
)
})?;
let staged = destination.join(file_name);
if *source == staged {
continue;
}
crate::utils::copy_file(source, &staged)
.await
.wrap_err_with(|| {
format!(
"Failed to stage {} to {}",
source.display(),
staged.display()
)
})?;
}
Ok(())
}
pub async fn remove_staged(destination: &Path, triple: &Triple) -> eyre::Result<()> {
Self::remove_staged_except(destination, triple, &[]).await
}
async fn remove_staged_except(
destination: &Path,
triple: &Triple,
keep: &[PathBuf],
) -> eyre::Result<()> {
if !destination.is_dir() {
return Ok(());
}
let waterui = dynamic_library_file_name("waterui_dylib", triple);
let (standard_library_prefix, extension) =
if triple.operating_system == OperatingSystem::Windows {
("std-", "dll")
} else {
("libstd-", lib_extension_for_triple(triple))
};
let mut entries = smol::fs::read_dir(destination).await?;
while let Some(entry) = entries.next().await {
let entry = entry?;
if keep.contains(&entry.path()) {
continue;
}
let file_name = entry.file_name();
let file_name = file_name.to_string_lossy();
if file_name == waterui
|| (file_name.starts_with(standard_library_prefix)
&& entry.path().extension().and_then(|value| value.to_str()) == Some(extension))
{
smol::fs::remove_file(entry.path()).await?;
}
}
Ok(())
}
}
fn dynamic_library_file_name(crate_name: &str, triple: &Triple) -> String {
if triple.operating_system == OperatingSystem::Windows {
format!("{crate_name}.dll")
} else {
format!("lib{crate_name}.{}", lib_extension_for_triple(triple))
}
}
fn resolve_rust_standard_library_in(libdir: &Path, triple: &Triple) -> std::io::Result<PathBuf> {
let (prefix, extension) = if triple.operating_system == OperatingSystem::Windows {
("std-", "dll")
} else {
("libstd-", lib_extension_for_triple(triple))
};
let entries = match std::fs::read_dir(libdir) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("{} does not exist", libdir.display()),
));
}
Err(error) => return Err(error),
};
let mut matches = entries
.filter_map(|entry| entry.ok().map(|entry| entry.path()))
.filter(|path| {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| {
name.starts_with(prefix)
&& path.extension().and_then(|extension| extension.to_str())
== Some(extension)
})
})
.collect::<Vec<_>>();
matches.sort_unstable();
match matches.as_slice() {
[path] => Ok(path.clone()),
[] => Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!(
"Rust target libdir {} contains no dynamic standard library for {triple}",
libdir.display()
),
)),
_ => Err(std::io::Error::other(format!(
"Rust target libdir {} contains multiple dynamic standard libraries for {triple}: {}",
libdir.display(),
matches
.iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
.join(", ")
))),
}
}
#[derive(Debug, Clone)]
pub struct RustBuild {
path: PathBuf,
triple: Triple,
project: Option<Project>,
target_dir: Option<PathBuf>,
sccache_path: Option<PathBuf>,
features: Vec<String>,
crate_type_override: Option<String>,
rustc_flags: Vec<String>,
final_rustc_args: Vec<String>,
build_std_toolchain: Option<String>,
envs: Vec<(String, OsString)>,
progress: Option<BuildProgress>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BuildProfile {
#[default]
Debug,
Optimized,
Release,
Profiling,
}
impl BuildProfile {
#[must_use]
pub const fn is_release(self) -> bool {
matches!(self, Self::Release | Self::Profiling)
}
#[must_use]
pub const fn is_development(self) -> bool {
!self.is_release()
}
fn development_envs(self) -> Vec<(String, OsString)> {
let entries: &[(&str, &str)] = match self {
Self::Debug => &[],
Self::Optimized => &[
("CARGO_PROFILE_DEV_OPT_LEVEL", "1"),
("CARGO_PROFILE_DEV_DEBUG", "true"),
("CARGO_PROFILE_DEV_DEBUG_ASSERTIONS", "false"),
("CARGO_PROFILE_DEV_OVERFLOW_CHECKS", "false"),
],
Self::Release => &[
("CARGO_PROFILE_RELEASE_OPT_LEVEL", "3"),
("CARGO_PROFILE_RELEASE_PANIC", "unwind"),
("CARGO_PROFILE_RELEASE_LTO", "off"),
],
Self::Profiling => &[
("CARGO_PROFILE_RELEASE_OPT_LEVEL", "3"),
("CARGO_PROFILE_RELEASE_PANIC", "unwind"),
("CARGO_PROFILE_RELEASE_LTO", "off"),
("CARGO_PROFILE_RELEASE_DEBUG", "true"),
("CARGO_PROFILE_RELEASE_STRIP", "none"),
],
};
entries
.iter()
.map(|(key, value)| ((*key).to_string(), OsString::from(*value)))
.collect()
}
}
#[derive(Debug, Clone)]
pub struct BuildOptions {
profile: BuildProfile,
output_dir: Option<std::path::PathBuf>,
sccache_path: Option<std::path::PathBuf>,
target_triple: Option<Triple>,
linkage: RustLinkage,
dynamic_module_loading: bool,
dev_server: bool,
cargo_envs: Vec<(String, OsString)>,
progress: Option<BuildProgress>,
}
impl BuildOptions {
#[must_use]
pub fn development(profile: BuildProfile) -> Self {
Self {
profile,
output_dir: None,
sccache_path: None,
target_triple: None,
linkage: RustLinkage::SharedRuntime,
dynamic_module_loading: false,
dev_server: false,
cargo_envs: profile.development_envs(),
progress: None,
}
}
#[must_use]
pub fn with_static_runtime(mut self) -> Self {
self.linkage = RustLinkage::Static;
self.cargo_envs.retain(|(key, _)| {
key != "CARGO_PROFILE_RELEASE_PANIC" && key != "CARGO_PROFILE_RELEASE_LTO"
});
self
}
#[must_use]
pub const fn packaging(profile: BuildProfile) -> Self {
Self {
profile,
output_dir: None,
sccache_path: None,
target_triple: None,
linkage: RustLinkage::Static,
dynamic_module_loading: false,
dev_server: false,
cargo_envs: Vec::new(),
progress: None,
}
}
#[must_use]
pub const fn is_release(&self) -> bool {
self.profile.is_release()
}
#[must_use]
pub const fn profile(&self) -> BuildProfile {
self.profile
}
#[must_use]
pub fn cargo_envs(&self) -> &[(String, OsString)] {
&self.cargo_envs
}
#[must_use]
pub const fn with_dev_server(mut self, dev_server: bool) -> Self {
self.dev_server = dev_server;
self
}
#[must_use]
pub const fn uses_dev_server(&self) -> bool {
self.dev_server
}
#[must_use]
pub fn output_dir(&self) -> Option<&std::path::Path> {
self.output_dir.as_deref()
}
#[must_use]
pub fn with_output_dir(mut self, output_dir: impl Into<std::path::PathBuf>) -> Self {
self.output_dir = Some(output_dir.into());
self
}
#[must_use]
pub fn sccache_path(&self) -> Option<&std::path::Path> {
self.sccache_path.as_deref()
}
#[must_use]
pub fn with_sccache(mut self, sccache_path: impl Into<std::path::PathBuf>) -> Self {
self.sccache_path = Some(sccache_path.into());
self
}
#[must_use]
pub const fn target_triple(&self) -> Option<&Triple> {
self.target_triple.as_ref()
}
#[must_use]
pub fn with_target_triple(mut self, target_triple: Triple) -> Self {
self.target_triple = Some(target_triple);
self
}
#[must_use]
pub const fn linkage(&self) -> RustLinkage {
self.linkage
}
#[must_use]
pub const fn with_dynamic_module_loading(mut self) -> Self {
self.dynamic_module_loading = true;
self
}
#[must_use]
pub const fn loads_dynamic_modules(&self) -> bool {
self.dynamic_module_loading
}
#[must_use]
pub fn with_progress(mut self, progress: BuildProgress) -> Self {
self.progress = Some(progress);
self
}
#[must_use]
pub const fn progress(&self) -> Option<&BuildProgress> {
self.progress.as_ref()
}
}
#[derive(Debug, thiserror::Error)]
pub enum RustBuildError {
#[error("Failed to execute cargo build: {0}")]
FailToExecuteCargoBuild(std::io::Error),
#[error("Failed to build Rust library: {0}")]
FailToBuildRustLibrary(std::io::Error),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CompileEvent {
Unit {
phase: &'static str,
name: String,
version: Option<String>,
},
Finished(String),
Line(String),
}
#[derive(Clone)]
pub struct BuildProgress {
report: std::sync::Arc<dyn Fn(CompileEvent) + Send + Sync>,
shows_all_lines: bool,
}
impl std::fmt::Debug for BuildProgress {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("BuildProgress(..)")
}
}
impl BuildProgress {
#[must_use]
pub fn new(report: impl Fn(CompileEvent) + Send + Sync + 'static) -> Self {
Self {
report: std::sync::Arc::new(report),
shows_all_lines: false,
}
}
#[must_use]
pub const fn showing_all_lines(mut self) -> Self {
self.shows_all_lines = true;
self
}
#[must_use]
pub const fn shows_all_lines(&self) -> bool {
self.shows_all_lines
}
fn report(&self, event: CompileEvent) {
(self.report)(event);
}
}
const CARGO_UNIT_PHASES: &[&str] = &[
"Compiling",
"Checking",
"Fresh",
"Downloading",
"Downloaded",
"Doc-tests",
];
fn classify_compile_line(line: &str) -> CompileEvent {
let raw = line.trim();
let stripped = console::strip_ansi_codes(raw);
let text = stripped.trim();
for phase in CARGO_UNIT_PHASES {
let Some(rest) = text
.strip_prefix(phase)
.and_then(|rest| rest.strip_prefix(' '))
else {
continue;
};
let Some((name, version)) = rest.split_once(" v") else {
return CompileEvent::Line(raw.to_owned());
};
let version = version.split([' ', '(']).next().unwrap_or_default();
return CompileEvent::Unit {
phase,
name: name.to_owned(),
version: (!version.is_empty()).then(|| version.to_owned()),
};
}
if text.starts_with("Finished ") {
return CompileEvent::Finished(raw.to_owned());
}
CompileEvent::Line(raw.to_owned())
}
pub(crate) async fn command_output_with_progress(
command: &mut Command,
progress: Option<BuildProgress>,
) -> io::Result<std::process::Output> {
let mut child = command
.kill_on_drop(true)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let stdout_pipe = child.stdout.take().expect("stdout is piped");
let stderr_pipe = child.stderr.take().expect("stderr is piped");
let echo = progress.is_none() && std_output_enabled();
let stdout_task = smol::spawn(drain_pipe(stdout_pipe));
let stderr_task = smol::spawn(drain_cargo_stderr(stderr_pipe, progress, echo));
let status = child.status().await?;
let stdout = stdout_task.await?;
let stderr = stderr_task.await?;
Ok(std::process::Output {
status,
stdout,
stderr,
})
}
async fn drain_pipe(mut reader: impl smol::io::AsyncRead + Unpin) -> io::Result<Vec<u8>> {
let mut collected = Vec::new();
let mut chunk = [0u8; 8192];
loop {
let read = reader.read(&mut chunk).await?;
if read == 0 {
break;
}
collected.extend_from_slice(&chunk[..read]);
}
Ok(collected)
}
async fn drain_cargo_stderr(
mut reader: impl smol::io::AsyncRead + Unpin,
progress: Option<BuildProgress>,
echo: bool,
) -> io::Result<Vec<u8>> {
let mut collected = Vec::new();
let mut pending: Vec<u8> = Vec::new();
let mut chunk = [0u8; 8192];
loop {
let read = reader.read(&mut chunk).await?;
if read == 0 {
break;
}
collected.extend_from_slice(&chunk[..read]);
if echo {
let _ = io::stderr().write_all(&chunk[..read]);
let _ = io::stderr().flush();
}
if let Some(sink) = &progress {
pending.extend_from_slice(&chunk[..read]);
while let Some(newline) = pending.iter().position(|byte| *byte == b'\n') {
let line: Vec<u8> = pending.drain(..=newline).collect();
let line = String::from_utf8_lossy(&line);
let line = line.trim_end();
if !line.trim().is_empty() {
sink.report(classify_compile_line(line));
}
}
}
}
if let Some(sink) = &progress {
let tail = String::from_utf8_lossy(&pending);
let tail = tail.trim_end();
if !tail.trim().is_empty() {
sink.report(classify_compile_line(tail));
}
}
Ok(collected)
}
impl RustBuild {
pub fn new(path: impl AsRef<Path>, triple: Triple) -> Self {
Self {
path: path.as_ref().to_path_buf(),
triple,
project: None,
target_dir: None,
sccache_path: None,
features: Vec::new(),
crate_type_override: None,
rustc_flags: Vec::new(),
final_rustc_args: Vec::new(),
build_std_toolchain: None,
envs: Vec::new(),
progress: None,
}
}
pub(crate) fn with_project(mut self, project: &Project) -> Self {
self.project = Some(project.clone());
self
}
#[must_use]
pub fn with_target_dir(mut self, target_dir: impl Into<PathBuf>) -> Self {
self.target_dir = Some(target_dir.into());
self
}
#[must_use]
pub fn with_sccache(mut self, sccache_path: PathBuf) -> Self {
self.sccache_path = Some(sccache_path);
self
}
#[must_use]
pub fn with_feature(mut self, feature: impl Into<String>) -> Self {
self.features.push(feature.into());
self
}
#[must_use]
pub fn with_features(mut self, features: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.features.extend(features.into_iter().map(Into::into));
self
}
#[must_use]
pub fn features(&self) -> &[String] {
&self.features
}
#[must_use]
pub fn with_rustc_flag(mut self, flag: impl Into<String>) -> Self {
self.rustc_flags.push(flag.into());
self
}
#[must_use]
pub fn with_final_rustc_arg(mut self, flag: impl Into<String>) -> Self {
self.final_rustc_args.push(flag.into());
self
}
#[must_use]
pub fn with_build_std(mut self, toolchain: impl Into<String>) -> Self {
self.build_std_toolchain = Some(toolchain.into());
self
}
#[must_use]
pub fn with_preferred_dynamic_linking(self) -> Self {
self.with_rustc_flag("-Cprefer-dynamic")
.with_rustc_flag("-Crpath")
}
#[must_use]
pub fn with_linkage(
self,
linkage: RustLinkage,
development_feature: &str,
loader_search_path: Option<&str>,
) -> Self {
if linkage == RustLinkage::Static {
return self;
}
let build = self
.with_feature(development_feature)
.with_preferred_dynamic_linking();
match loader_search_path {
Some(path) => build.with_final_rustc_arg(format!("-Clink-arg=-Wl,-rpath,{path}")),
None => build,
}
}
#[must_use]
pub fn with_crate_type_override(mut self, crate_type: impl Into<String>) -> Self {
self.crate_type_override = Some(crate_type.into());
self
}
#[must_use]
pub fn with_env(mut self, key: impl Into<String>, value: impl Into<OsString>) -> Self {
self.envs.push((key.into(), value.into()));
self
}
#[must_use]
pub fn with_envs(mut self, envs: impl IntoIterator<Item = (String, OsString)>) -> Self {
self.envs.extend(envs);
self
}
#[must_use]
pub fn with_progress(mut self, progress: BuildProgress) -> Self {
self.progress = Some(progress);
self
}
#[must_use]
pub const fn triple(&self) -> &Triple {
&self.triple
}
pub async fn dev_build(&self) -> Result<BuiltTarget, RustBuildError> {
self.build_lib(false).await
}
pub async fn release_build(&self) -> Result<BuiltTarget, RustBuildError> {
self.build_lib(true).await
}
pub async fn build_lib(&self, release: bool) -> Result<BuiltTarget, RustBuildError> {
self.build_inner(release, CargoTarget::Lib, self.lib_artifact_extension())
.await
}
pub async fn build_dylib(&self, release: bool) -> Result<PathBuf, RustBuildError> {
let built = self
.build_inner(
release,
CargoTarget::Lib,
Some(lib_extension_for_triple(&self.triple)),
)
.await?;
Ok(built.artifact)
}
pub async fn build_binary(
&self,
binary_name: &str,
release: bool,
) -> Result<PathBuf, RustBuildError> {
let built = self
.build_inner(release, CargoTarget::Binary(binary_name), None)
.await?;
Ok(built.artifact)
}
pub async fn dylib_path(
&self,
crate_name: &str,
release: bool,
) -> Result<PathBuf, RustBuildError> {
let lib_dir = self.lib_output_dir(release).await?;
let lib_name = crate_name.replace('-', "_");
let ext = lib_extension_for_triple(&self.triple);
Ok(lib_dir.join(format!("lib{lib_name}.{ext}")))
}
async fn build_inner(
&self,
release: bool,
cargo_target: CargoTarget<'_>,
artifact_extension: Option<&'static str>,
) -> Result<BuiltTarget, RustBuildError> {
let mut output = self.cargo_build_output(release, cargo_target).await?;
if !output.status.success() {
let mut combined = combined_build_output(&output);
if should_retry_after_cmake_generator_mismatch(&combined)
&& self.clean_stale_cmake_build_dirs().await?
{
output = self.cargo_build_output(release, cargo_target).await?;
combined = combined_build_output(&output);
}
if !output.status.success() && should_auto_install_meson(&combined) {
match ensure_meson_installed_for_build().await {
Ok(()) => {
output = self.cargo_build_output(release, cargo_target).await?;
}
Err(install_err) => {
return Err(RustBuildError::FailToBuildRustLibrary(
std::io::Error::other(format!(
"Cargo build failed and meson appears missing.\n\
Automatic meson installation failed: {install_err}\n\n{}",
self.failure_report(&combined)
)),
));
}
}
}
}
if !output.status.success() {
let combined = combined_build_output(&output);
return Err(RustBuildError::FailToBuildRustLibrary(
std::io::Error::other(format!(
"Cargo build failed:\n{}",
self.failure_report(&combined)
)),
));
}
let stale = stale_shared_dylib_packages(&output.stdout).await?;
if !stale.is_empty() {
let target_dir = self.target_directory().await?;
for package in &stale {
clean_cargo_package(&self.path, package, &target_dir).await?;
}
output = self.cargo_build_output(release, cargo_target).await?;
if !output.status.success() {
let combined = combined_build_output(&output);
return Err(RustBuildError::FailToBuildRustLibrary(
std::io::Error::other(format!(
"Cargo build failed:\n{}",
self.failure_report(&combined)
)),
));
}
}
let artifact =
reported_artifact(&output.stdout, &self.path, cargo_target, artifact_extension)?;
let profile_dir = self.lib_output_dir(release).await?;
Ok(BuiltTarget {
profile_dir,
artifact,
})
}
fn lib_artifact_extension(&self) -> Option<&'static str> {
self.crate_type_override
.as_deref()
.and_then(|crate_type| crate_type_artifact_extension(crate_type, &self.triple))
}
fn failure_report(&self, combined: &str) -> String {
if self
.progress
.as_ref()
.is_some_and(BuildProgress::shows_all_lines)
{
output_tail(combined)
} else {
combined.to_owned()
}
}
async fn clean_stale_cmake_build_dirs(&self) -> Result<bool, RustBuildError> {
let target_dir = self.target_directory().await?;
let triple = self.triple.to_string();
let removed = unblock(move || {
let mut removed = 0usize;
removed +=
remove_cmake_build_dirs_in(&target_dir.join(&triple).join("debug").join("build"))?;
removed += remove_cmake_build_dirs_in(
&target_dir.join(&triple).join("release").join("build"),
)?;
Ok::<usize, std::io::Error>(removed)
})
.await
.map_err(|error| {
RustBuildError::FailToBuildRustLibrary(std::io::Error::other(format!(
"Failed to clean stale CMake cache: {error}"
)))
})?;
Ok(removed > 0)
}
async fn cargo_build_output(
&self,
release: bool,
cargo_target: CargoTarget<'_>,
) -> Result<std::process::Output, RustBuildError> {
let framework = self.project.as_ref().and_then(|project| {
project
.manifest()
.framework
.as_ref()
.map(|framework| (project, framework))
});
if let Some((project, framework)) = framework {
framework
.prepare_build(project, &self.path, &self.features)
.await
.map_err(|error| {
RustBuildError::FailToBuildRustLibrary(std::io::Error::other(error.to_string()))
})?;
}
let crate_type_override = if cargo_target.accepts_crate_type_override() {
self.crate_type_override.as_deref()
} else {
None
};
let mut cmd = Command::new("cargo");
let cargo_subcommand = if crate_type_override.is_some() || !self.final_rustc_args.is_empty()
{
"rustc"
} else {
"build"
};
let mut cmd = cmd.arg(cargo_subcommand);
if self.build_std_toolchain.is_some() {
cmd = cmd.arg("-Zbuild-std=std,panic_abort");
cmd =
cmd.arg("-Zbuild-std-features=panic-unwind,backtrace,default,compiler-builtins-c");
}
let mut cmd = cmd
.arg("--message-format=json-render-diagnostics")
.args(cargo_target.cargo_args())
.args(["--target", self.triple.to_string().as_str()])
.current_dir(&self.path);
if framework.is_some() {
cmd = cmd.arg("--locked");
}
if let Some(target_dir) = &self.target_dir {
cmd = cmd.arg("--target-dir").arg(target_dir);
}
for (key, value) in &self.envs {
cmd.env(key, value);
}
let mut cmd = self.with_project_toolchain_env(cmd).await?;
if !self.rustc_flags.is_empty() {
let mut rustflags = std::env::var_os("RUSTFLAGS").unwrap_or_default();
if !rustflags.is_empty() {
rustflags.push(" ");
}
rustflags.push(self.rustc_flags.join(" "));
cmd = cmd.env("RUSTFLAGS", rustflags);
}
configure_generated_crate_compilation(cmd);
if let Some(sccache_path) = &self.sccache_path {
crate::toolchain::sccache::configure_compilation_cache(cmd, sccache_path).map_err(
|error| {
RustBuildError::FailToBuildRustLibrary(std::io::Error::other(error.to_string()))
},
)?;
}
if self.build_std_toolchain.is_some() {
cmd = self.with_build_std_envs(cmd, release).await?;
}
if self.triple.environment == Environment::Sim
&& let Some(clang_args) = self.bindgen_clang_args_for_simulator().await
{
let bindgen_target_key = format!(
"BINDGEN_EXTRA_CLANG_ARGS_{}",
self.triple.to_string().replace('-', "_")
);
cmd = cmd.env(bindgen_target_key, clang_args);
}
if release {
cmd = cmd.arg("--release");
}
if !self.features.is_empty() {
cmd = cmd.args(["--features", &self.features.join(",")]);
}
if crate_type_override.is_some() || !self.final_rustc_args.is_empty() {
cmd = cmd.arg("--");
if let Some(crate_type) = crate_type_override {
cmd = cmd.arg("--crate-type").arg(crate_type);
}
cmd = cmd.args(&self.final_rustc_args);
}
if std_output_enabled()
&& std::env::var_os("CARGO_TERM_COLOR").is_none()
&& !self.envs.iter().any(|(key, _)| key == "CARGO_TERM_COLOR")
{
cmd.env("CARGO_TERM_COLOR", "always");
}
command_output_with_progress(cmd, self.progress.clone())
.await
.map_err(RustBuildError::FailToExecuteCargoBuild)
}
async fn with_project_toolchain_env<'a>(
&self,
cmd: &'a mut Command,
) -> Result<&'a mut Command, RustBuildError> {
if self.build_std_toolchain.is_some() {
return Ok(cmd);
}
let Some(project) = &self.project else {
return Ok(cmd);
};
let toolchain = project_toolchain(project).await.map_err(|error| {
RustBuildError::FailToBuildRustLibrary(std::io::Error::other(error.to_string()))
})?;
Ok(cmd.env("RUSTUP_TOOLCHAIN", toolchain))
}
async fn with_build_std_envs<'a>(
&self,
cmd: &'a mut Command,
release: bool,
) -> Result<&'a mut Command, RustBuildError> {
let Some(toolchain) = &self.build_std_toolchain else {
return Ok(cmd);
};
let publish_dir = self.lib_output_dir(release).await?.join("deps");
let cmd = cmd
.env("RUSTUP_TOOLCHAIN", toolchain)
.env(
"RUSTC_WRAPPER",
crate::toolchain::Host::current_exe()
.map_err(RustBuildError::FailToExecuteCargoBuild)?,
)
.env(crate::workflows::rustc_wrapper::WRAPPER_MODE_ENV, "1")
.env(
crate::workflows::rustc_wrapper::BUILD_STD_TARGET_ENV,
self.triple.to_string(),
)
.env(
crate::workflows::rustc_wrapper::BUILD_STD_DYLIB_DIR_ENV,
publish_dir,
);
if let Some(sccache_path) = &self.sccache_path {
cmd.env(
crate::workflows::rustc_wrapper::WRAPPER_CHAIN_ENV,
sccache_path,
);
}
cmd.env_remove("RUSTC_WORKSPACE_WRAPPER");
cmd.env_remove("CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER");
Ok(cmd)
}
pub async fn lib_output_dir(&self, release: bool) -> Result<PathBuf, RustBuildError> {
let target_directory = self.target_directory().await?;
Ok(target_directory
.join(self.triple.to_string())
.join(if release { "release" } else { "debug" }))
}
async fn target_directory(&self) -> Result<PathBuf, RustBuildError> {
if let Some(target_dir) = &self.target_dir {
return Ok(target_dir.clone());
}
let build_path = self.path.clone();
let metadata = unblock(move || {
cargo_metadata::MetadataCommand::new()
.no_deps()
.current_dir(build_path)
.exec()
.map_err(|e| {
RustBuildError::FailToBuildRustLibrary(std::io::Error::new(
std::io::ErrorKind::InvalidData,
e,
))
})
})
.await?;
Ok(metadata.target_directory.as_std_path().to_path_buf())
}
async fn bindgen_clang_args_for_simulator(&self) -> Option<String> {
let (sdk_name, target_os) = match self.triple.operating_system {
OperatingSystem::IOS(_) => ("iphonesimulator", "ios"),
OperatingSystem::TvOS(_) => ("appletvsimulator", "tvos"),
OperatingSystem::WatchOS(_) => ("watchsimulator", "watchos"),
OperatingSystem::VisionOS(_) => ("xrsimulator", "xros"),
_ => return None,
};
let arch = match self.triple.architecture {
target_lexicon::Architecture::Aarch64(_) => "arm64",
target_lexicon::Architecture::X86_64 => "x86_64",
_ => return None,
};
let sdk_path = run_command("xcrun", ["--sdk", sdk_name, "--show-sdk-path"])
.await
.ok()
.map(|s| s.trim().to_string())?;
let min_version = if matches!(target_os, "ios" | "tvos") {
"17.0"
} else if target_os == "watchos" {
"10.0"
} else {
debug_assert_eq!(
target_os, "xros",
"bindgen simulator target_os must be one of ios/tvos/watchos/xros"
);
"1.0"
};
Some(format!(
"--target={arch}-apple-{target_os}{min_version}-simulator -isysroot {sdk_path}"
))
}
}
fn crate_type_artifact_extension(crate_type: &str, triple: &Triple) -> Option<&'static str> {
match crate_type {
"lib" | "rlib" => Some("rlib"),
"staticlib" => Some(if matches!(triple.environment, Environment::Msvc) {
"lib"
} else {
"a"
}),
"cdylib" | "dylib" | "proc-macro" => Some(lib_extension_for_triple(triple)),
_ => None,
}
}
pub(crate) fn reported_artifact(
stdout: &[u8],
crate_dir: &Path,
cargo_target: CargoTarget<'_>,
artifact_extension: Option<&'static str>,
) -> Result<PathBuf, RustBuildError> {
let manifest_path = dunce::canonicalize(crate_dir.join("Cargo.toml")).map_err(|error| {
RustBuildError::FailToBuildRustLibrary(io::Error::other(format!(
"failed to canonicalize {}: {error}",
crate_dir.join("Cargo.toml").display()
)))
})?;
let mut artifacts = Vec::new();
for artifact in compiler_artifacts(stdout)? {
if cargo_target.matches(&artifact.target)
&& same_manifest_path(artifact.manifest_path.as_std_path(), &manifest_path)
{
artifacts.push(artifact);
}
}
reported_artifact_file(&artifacts, cargo_target, artifact_extension, &manifest_path)
}
pub(crate) fn compiler_artifacts(
stdout: &[u8],
) -> Result<Vec<cargo_metadata::Artifact>, RustBuildError> {
#[derive(serde::Deserialize)]
struct Reason {
reason: String,
}
let mut artifacts = Vec::new();
for (index, line) in stdout.split(|byte| *byte == b'\n').enumerate() {
let Ok(line) = str::from_utf8(line) else {
continue;
};
let line = line.trim_end();
if line.is_empty() {
continue;
}
let malformed = |error: serde_json::Error| {
RustBuildError::FailToBuildRustLibrary(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"cargo emitted a malformed `compiler-artifact` message on line {}: {error}\n{line}",
index + 1
),
))
};
match serde_json::from_str::<Reason>(line) {
Ok(Reason { reason }) if reason == "compiler-artifact" => {
let artifact =
serde_json::from_str::<cargo_metadata::Artifact>(line).map_err(malformed)?;
artifacts.push(artifact);
}
Err(error) if line.contains("\"reason\":\"compiler-artifact\"") => {
return Err(malformed(error));
}
Ok(_) | Err(_) => {}
}
}
Ok(artifacts)
}
pub(crate) fn same_manifest_path(reported: &Path, expected: &Path) -> bool {
reported == expected
|| dunce::canonicalize(reported).is_ok_and(|canonical| canonical == expected)
}
fn reported_artifact_file(
artifacts: &[cargo_metadata::Artifact],
cargo_target: CargoTarget<'_>,
artifact_extension: Option<&'static str>,
manifest_path: &Path,
) -> Result<PathBuf, RustBuildError> {
let what = || -> String {
match cargo_target {
CargoTarget::Lib => format!("the library target of {}", manifest_path.display()),
CargoTarget::Binary(name) => {
format!("binary `{name}` of {}", manifest_path.display())
}
}
};
let not_found = |detail: String| {
RustBuildError::FailToBuildRustLibrary(io::Error::new(io::ErrorKind::NotFound, detail))
};
let files: Vec<PathBuf> = artifacts
.iter()
.flat_map(|artifact| {
artifact
.filenames
.iter()
.map(|file| file.as_std_path().to_path_buf())
})
.collect();
let artifact = match cargo_target {
CargoTarget::Binary(_) => artifacts
.iter()
.find_map(|artifact| artifact.executable.as_ref())
.map(|path| path.as_std_path().to_path_buf())
.ok_or_else(|| {
not_found(format!(
"Cargo reported no artifact for {} (reported files: {files:?})",
what()
))
})?,
CargoTarget::Lib => {
let matching: Vec<&PathBuf> = artifact_extension.map_or_else(
|| files.iter().collect(),
|extension| {
files
.iter()
.filter(|file| file.extension().is_some_and(|e| *e == *extension))
.collect()
},
);
match matching.as_slice() {
[only] => (*only).clone(),
_ => {
return Err(not_found(artifact_extension.map_or_else(
|| {
format!(
"Cargo reported {} artifacts for {} — select one with a crate-type override (reported files: {files:?})",
matching.len(),
what()
)
},
|extension| {
format!(
"Cargo reported no `.{extension}` artifact for {} (reported files: {files:?})",
what()
)
},
)));
}
}
}
};
if !artifact.is_file() {
return Err(not_found(format!(
"Cargo reported {} for {} but the file does not exist",
artifact.display(),
what()
)));
}
Ok(artifact)
}
async fn stale_shared_dylib_packages(stdout: &[u8]) -> Result<Vec<String>, RustBuildError> {
let mut stale = Vec::new();
for artifact in compiler_artifacts(stdout)? {
if !artifact.fresh {
continue;
}
let Some(manifest_dir) = artifact.manifest_path.as_std_path().parent() else {
continue;
};
if !uplifts_dynamic_library(&artifact.target) {
continue;
}
let manifest_root = dunce::simplified(manifest_dir);
let mut package_stale = false;
for filename in &artifact.filenames {
let file = filename.as_std_path();
if !is_dynamic_library(file) {
continue;
}
let Some(dep_info) = dep_info_path(file, &artifact.filenames) else {
return Err(RustBuildError::FailToBuildRustLibrary(io::Error::new(
io::ErrorKind::NotFound,
format!(
"Cargo reported {} fresh but no dep-info was found beside it or in its unit directory (reported files: {:?})",
file.display(),
artifact.filenames
),
)));
};
let contents = smol::fs::read_to_string(&dep_info).await.map_err(|error| {
RustBuildError::FailToBuildRustLibrary(io::Error::other(format!(
"Cargo reported {} fresh but its dep-info {} is unreadable: {error}",
file.display(),
dep_info.display()
)))
})?;
if !dep_info_prerequisites(&contents).iter().any(|source| {
let source = if source.is_absolute() {
source.clone()
} else {
manifest_dir.join(source)
};
dunce::simplified(&source).starts_with(manifest_root)
}) {
package_stale = true;
}
}
if package_stale {
stale.push(artifact_package_name(&artifact.package_id).to_owned());
}
}
stale.sort_unstable();
stale.dedup();
Ok(stale)
}
fn is_dynamic_library(file: &Path) -> bool {
file.extension()
.is_some_and(|extension| matches!(extension.to_str(), Some("so" | "dylib" | "dll")))
}
fn uplifts_dynamic_library(target: &cargo_metadata::Target) -> bool {
target.crate_types.iter().any(|kind| {
matches!(
kind,
cargo_metadata::CrateType::DyLib | cargo_metadata::CrateType::CDyLib
)
})
}
fn dep_info_path(
artifact_file: &Path,
sibling_files: &[cargo_metadata::camino::Utf8PathBuf],
) -> Option<PathBuf> {
let file_stem = artifact_file.file_stem()?.to_str()?;
let name = file_stem.strip_prefix("lib").unwrap_or(file_stem);
let dir = artifact_file.parent()?;
let mut candidates = vec![
dir.join(format!("{file_stem}.d")),
dir.join("deps").join(format!("{name}.d")),
];
candidates.extend(
sibling_files
.iter()
.filter_map(|sibling| sibling.as_std_path().parent())
.filter(|unit_dir| *unit_dir != dir)
.map(|unit_dir| unit_dir.join(format!("{name}.d"))),
);
candidates.push(dir.join(format!("{name}.d")));
candidates.into_iter().find(|candidate| candidate.is_file())
}
fn dep_info_prerequisites(contents: &str) -> Vec<PathBuf> {
let mut joined = String::with_capacity(contents.len());
for line in contents.lines() {
if let Some(head) = line.strip_suffix('\\') {
joined.push_str(head);
joined.push(' ');
} else {
joined.push_str(line);
joined.push('\n');
}
}
let mut prerequisites = Vec::new();
for line in joined.lines() {
let Some((_, rest)) = line.split_once(": ") else {
continue;
};
let mut token = String::new();
let mut chars = rest.chars().peekable();
while let Some(c) = chars.next() {
match c {
'\\' if chars.peek() == Some(&' ') => {
chars.next();
token.push(' ');
}
c if c.is_whitespace() => {
if !token.is_empty() {
prerequisites.push(PathBuf::from(std::mem::take(&mut token)));
}
}
c => token.push(c),
}
}
if !token.is_empty() {
prerequisites.push(PathBuf::from(token));
}
}
prerequisites
}
fn artifact_package_name(package_id: &cargo_metadata::PackageId) -> &str {
let repr = package_id.repr.as_str();
let (source, fragment) = repr.rsplit_once('#').unwrap_or((repr, ""));
fragment.split_once('@').map_or_else(
|| source.rsplit('/').next().unwrap_or(repr),
|(name, _)| name,
)
}
async fn clean_cargo_package(
crate_dir: &Path,
package: &str,
target_dir: &Path,
) -> Result<(), RustBuildError> {
let mut command = Command::new("cargo");
command
.arg("clean")
.arg("-p")
.arg(package)
.arg("--target-dir")
.arg(target_dir)
.current_dir(crate_dir);
configure_generated_crate_compilation(&mut command);
let output = command
.output()
.await
.map_err(RustBuildError::FailToExecuteCargoBuild)?;
if !output.status.success() {
return Err(RustBuildError::FailToBuildRustLibrary(io::Error::other(
format!(
"cargo clean -p {package} failed:\n{}",
String::from_utf8_lossy(&output.stderr)
),
)));
}
Ok(())
}
fn combined_build_output(output: &std::process::Output) -> String {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
if stderr.is_empty() {
stdout.to_string()
} else {
stderr.to_string()
}
}
const FAILURE_TAIL_LINES: usize = 40;
pub(crate) fn output_tail(text: &str) -> String {
let lines: Vec<&str> = text.lines().collect();
if lines.len() <= FAILURE_TAIL_LINES {
return text.to_owned();
}
format!(
"… {} earlier lines already streamed above …\n{}",
lines.len() - FAILURE_TAIL_LINES,
lines[lines.len() - FAILURE_TAIL_LINES..].join("\n")
)
}
fn should_auto_install_meson(build_output: &str) -> bool {
let lower = build_output.to_ascii_lowercase();
lower.contains("meson")
&& (lower.contains("not found")
|| lower.contains("no such file")
|| lower.contains("failed to execute")
|| lower.contains("is required"))
}
fn should_retry_after_cmake_generator_mismatch(build_output: &str) -> bool {
let lower = build_output.to_ascii_lowercase();
lower.contains("cmake error") && lower.contains("does not match the generator used previously")
}
fn remove_cmake_build_dirs_in(build_root: &Path) -> std::io::Result<usize> {
if !build_root.exists() {
return Ok(0);
}
let mut removed = 0usize;
for entry in std::fs::read_dir(build_root)? {
let entry = entry?;
let path = entry.path();
if !path.is_dir() {
continue;
}
let cmake_build_dir = path.join("out").join("build");
if cmake_build_dir.join("CMakeCache.txt").exists() {
std::fs::remove_dir_all(cmake_build_dir)?;
removed += 1;
}
}
Ok(removed)
}
#[cfg(target_os = "macos")]
async fn ensure_meson_installed_for_build() -> Result<(), String> {
use crate::toolchain::meson::Meson;
use crate::toolchain::{Installation as _, Toolchain as _, ToolchainError};
let host = crate::toolchain::Host::current();
match Meson.check(&host).await {
Ok(()) => Ok(()),
Err(ToolchainError::Fixable(installation)) => {
installation.install(&host).await.map_err(|e| e.to_string())
}
Err(ToolchainError::Unfixable(e)) => Err(e.to_string()),
}
}
#[cfg(not(target_os = "macos"))]
fn ensure_meson_installed_for_build() -> impl std::future::Future<Output = Result<(), String>> {
std::future::ready(Err(
"automatic meson installation is only supported on macOS".to_string(),
))
}
#[cfg(test)]
mod tests {
use target_lexicon::Triple;
use tempfile::tempdir;
use std::ffi::OsString;
use std::path::PathBuf;
use super::{
BuildOptions, BuildProfile, CargoTarget, CompileEvent, RustBuild, RustDynamicLibraries,
RustLinkage, classify_compile_line, dynamic_library_file_name, lib_extension_for_triple,
resolve_rust_standard_library_in,
};
fn triple(value: &str) -> Triple {
value.parse().expect("test target triple must parse")
}
#[test]
fn crate_type_override_applies_only_to_library_targets() {
assert!(CargoTarget::Lib.accepts_crate_type_override());
assert!(!CargoTarget::Binary("waterui-cef-helper").accepts_crate_type_override());
assert_eq!(CargoTarget::Lib.cargo_args(), ["--lib"]);
assert_eq!(
CargoTarget::Binary("waterui-cef-helper").cargo_args(),
["--bin", "waterui-cef-helper"]
);
}
#[test]
fn build_std_envs_wire_the_wrapper_and_clear_workspace_wrappers() {
use std::ffi::OsStr;
let dir = tempdir().expect("target dir");
let toolchain = "nightly-2026-09-09-aarch64-apple-darwin";
let target_dir = dir.path().join("target");
let build = RustBuild::new(dir.path(), triple("aarch64-linux-android"))
.with_build_std(toolchain)
.with_target_dir(target_dir.clone())
.with_sccache(std::path::PathBuf::from("/fake/sccache"));
let mut cmd = smol::process::Command::new("cargo");
smol::block_on(build.with_build_std_envs(&mut cmd, false)).expect("build-std envs apply");
let env = |key: &str| -> Option<Option<OsString>> {
cmd.get_envs()
.find(|(name, _)| *name == OsStr::new(key))
.map(|(_, value)| value.map(ToOwned::to_owned))
};
assert_eq!(
env("RUSTUP_TOOLCHAIN"),
Some(Some(OsString::from(toolchain)))
);
assert_eq!(
env("RUSTC_WRAPPER"),
Some(Some(
crate::toolchain::Host::current_exe()
.expect("the test binary path")
.into_os_string()
)),
"the wrapper must name this binary"
);
assert_eq!(
env(crate::workflows::rustc_wrapper::WRAPPER_MODE_ENV),
Some(Some(OsString::from("1")))
);
assert_eq!(
env(crate::workflows::rustc_wrapper::BUILD_STD_TARGET_ENV),
Some(Some(OsString::from("aarch64-linux-android")))
);
let expected_dylib_dir = target_dir
.join("aarch64-linux-android")
.join("debug")
.join("deps");
assert_eq!(
env(crate::workflows::rustc_wrapper::BUILD_STD_DYLIB_DIR_ENV),
Some(Some(expected_dylib_dir.into_os_string()))
);
assert_eq!(
env(crate::workflows::rustc_wrapper::WRAPPER_CHAIN_ENV),
Some(Some(OsString::from("/fake/sccache"))),
"a configured sccache chains behind the shim"
);
assert_eq!(env("RUSTC_WORKSPACE_WRAPPER"), Some(None));
assert_eq!(env("CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER"), Some(None));
}
#[test]
fn apple_platform_dylibs_use_macho_extension() {
assert_eq!(
lib_extension_for_triple(&triple("aarch64-apple-darwin")),
"dylib"
);
assert_eq!(
lib_extension_for_triple(&triple("aarch64-apple-ios-sim")),
"dylib"
);
assert_eq!(
lib_extension_for_triple(&triple("aarch64-apple-ios")),
"dylib"
);
}
#[test]
fn non_apple_platform_dylibs_keep_platform_extensions() {
assert_eq!(
lib_extension_for_triple(&triple("aarch64-linux-android")),
"so"
);
assert_eq!(
lib_extension_for_triple(&triple("x86_64-unknown-linux-gnu")),
"so"
);
assert_eq!(
lib_extension_for_triple(&triple("x86_64-pc-windows-msvc")),
"dll"
);
}
#[test]
fn development_and_packaging_have_distinct_linkage() {
assert_eq!(
BuildOptions::development(BuildProfile::Debug).linkage(),
RustLinkage::SharedRuntime
);
assert_eq!(
BuildOptions::packaging(BuildProfile::Debug).linkage(),
RustLinkage::Static
);
assert!(BuildOptions::development(BuildProfile::Release).is_release());
assert!(BuildOptions::packaging(BuildProfile::Release).is_release());
}
#[test]
fn build_profile_release_variants_select_the_release_profile() {
assert!(BuildProfile::Release.is_release());
assert!(BuildProfile::Profiling.is_release());
assert!(!BuildProfile::Debug.is_release());
assert!(!BuildProfile::Optimized.is_release());
}
#[test]
fn development_profile_envs_realize_the_selected_trade_off() {
let optimized = BuildOptions::development(BuildProfile::Optimized);
let envs = optimized.cargo_envs();
assert!(
envs.contains(&(
"CARGO_PROFILE_DEV_OPT_LEVEL".to_string(),
OsString::from("1")
)),
"optimized development lifts the dev opt-level: {envs:?}"
);
assert!(
envs.contains(&(
"CARGO_PROFILE_DEV_DEBUG_ASSERTIONS".to_string(),
OsString::from("false")
)),
"optimized development drops dep debug assertions: {envs:?}"
);
assert!(
envs.contains(&(
"CARGO_PROFILE_DEV_DEBUG".to_string(),
OsString::from("true")
)),
"optimized development keeps full debug info: {envs:?}"
);
let shared_runtime_envs = [
(
"CARGO_PROFILE_RELEASE_PANIC".to_string(),
OsString::from("unwind"),
),
(
"CARGO_PROFILE_RELEASE_LTO".to_string(),
OsString::from("off"),
),
];
for env in &shared_runtime_envs {
assert!(
BuildOptions::development(BuildProfile::Release)
.cargo_envs()
.contains(env),
"a release development build links the shared runtime: missing {env:?}"
);
assert!(
!BuildOptions::development(BuildProfile::Release)
.with_static_runtime()
.cargo_envs()
.contains(env),
"a static runtime keeps the manifest's {env:?}"
);
}
let unwind = &shared_runtime_envs[0];
let profiling = BuildOptions::development(BuildProfile::Profiling);
let envs = profiling.cargo_envs();
assert!(
envs.contains(unwind),
"profiling links the shared runtime too"
);
for key in [
"CARGO_PROFILE_RELEASE_OPT_LEVEL",
"CARGO_PROFILE_RELEASE_DEBUG",
"CARGO_PROFILE_RELEASE_STRIP",
] {
assert!(
envs.iter().any(|(env_key, _)| env_key == key),
"profiling keeps debug info and symbols: missing {key} in {envs:?}"
);
}
assert!(
BuildOptions::development(BuildProfile::Debug)
.cargo_envs()
.is_empty(),
"plain debug runs the declared dev profile"
);
}
#[test]
fn packaging_never_overrides_the_declared_profile() {
for profile in [
BuildProfile::Debug,
BuildProfile::Optimized,
BuildProfile::Release,
BuildProfile::Profiling,
] {
assert!(
BuildOptions::packaging(profile).cargo_envs().is_empty(),
"packaging {profile:?} must ship the declared profile"
);
}
}
#[test]
fn resolves_target_standard_library_without_guessing_hash() {
let directory = tempdir().expect("temporary target libdir");
let android_triple = triple("aarch64-linux-android");
let expected = directory.path().join("libstd-1234567890abcdef.so");
std::fs::write(&expected, []).expect("write test std library");
std::fs::write(directory.path().join("libcore.rlib"), []).expect("write unrelated library");
assert_eq!(
resolve_rust_standard_library_in(directory.path(), &android_triple)
.expect("resolve dynamic std"),
expected
);
assert_eq!(
dynamic_library_file_name("waterui_dylib", &android_triple),
"libwaterui_dylib.so"
);
assert_eq!(
dynamic_library_file_name("waterui_dylib", &triple("x86_64-pc-windows-msvc")),
"waterui_dylib.dll"
);
}
#[test]
fn compile_progress_classifies_cargo_unit_lines() {
assert_eq!(
classify_compile_line(" Compiling serde v1.0.228"),
CompileEvent::Unit {
phase: "Compiling",
name: "serde".to_string(),
version: Some("1.0.228".to_string()),
}
);
assert_eq!(
classify_compile_line(" Compiling waterui-app v0.1.0 (/tmp/app)"),
CompileEvent::Unit {
phase: "Compiling",
name: "waterui-app".to_string(),
version: Some("0.1.0".to_string()),
}
);
assert_eq!(
classify_compile_line(" Checking libc v0.2.171"),
CompileEvent::Unit {
phase: "Checking",
name: "libc".to_string(),
version: Some("0.2.171".to_string()),
}
);
}
#[test]
fn compile_progress_keeps_non_unit_lines_verbatim() {
assert_eq!(
classify_compile_line(" Compiling 12 crates"),
CompileEvent::Line("Compiling 12 crates".to_string())
);
assert_eq!(
classify_compile_line(" Downloaded 300 crates (5.2 MB) in 1.23s"),
CompileEvent::Line("Downloaded 300 crates (5.2 MB) in 1.23s".to_string())
);
assert_eq!(
classify_compile_line(
" Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.23s"
),
CompileEvent::Finished(
"Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.23s".to_string()
)
);
assert_eq!(
classify_compile_line("warning: unused import"),
CompileEvent::Line("warning: unused import".to_string())
);
}
#[test]
fn compile_progress_classifies_through_ansi_color() {
let colored = "\u{1b}[0m\u{1b}[1m\u{1b}[32m Compiling\u{1b}[0m serde v1.0.228";
assert_eq!(
classify_compile_line(colored),
CompileEvent::Unit {
phase: "Compiling",
name: "serde".to_string(),
version: Some("1.0.228".to_string()),
}
);
let colored_finished =
"\u{1b}[0m\u{1b}[1m\u{1b}[32m Finished\u{1b}[0m `dev` profile in 1.23s";
assert_eq!(
classify_compile_line(colored_finished),
CompileEvent::Finished(colored_finished.trim().to_string())
);
}
#[test]
fn same_named_projects_resolve_their_own_artifacts_in_one_shared_target() {
use crate::project_model::project_types::{CrateName, generated_crate_name};
smol::block_on(async {
let temporary = tempdir().expect("tempdir");
let shared_target = temporary.path().join("shared-target");
let demo = CrateName::try_from("demo").expect("crate name");
let mut artifacts = Vec::new();
for (directory, marker) in [("first", "first"), ("second", "second")] {
let project_root = temporary.path().join(directory);
let crate_dir = project_root.join("hydrolysis");
std::fs::create_dir_all(crate_dir.join("src")).expect("crate dir");
let package = generated_crate_name(&demo, "hydrolysis", &project_root);
std::fs::write(
crate_dir.join("Cargo.toml"),
format!(
"[package]\nname = \"{package}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n"
),
)
.expect("manifest");
std::fs::write(
crate_dir.join("src/main.rs"),
format!("fn main() {{ println!(\"{marker}\"); }}\n"),
)
.expect("main.rs");
let artifact = super::RustBuild::new(&crate_dir, Triple::host())
.with_target_dir(&shared_target)
.build_binary(package.as_str(), false)
.await
.expect("the generated crate builds");
assert!(artifact.is_file(), "the reported artifact exists");
artifacts.push(artifact);
}
assert_ne!(
artifacts[0], artifacts[1],
"each same-named project resolves its own artifact"
);
for (artifact, marker) in artifacts.iter().zip(["first", "second"]) {
let ran = std::process::Command::new(artifact)
.output()
.expect("the resolved artifact executes");
assert_eq!(
String::from_utf8_lossy(&ran.stdout).trim(),
marker,
"the artifact is this project's binary, not the sibling's"
);
}
});
}
#[test]
fn reported_artifact_selects_the_matching_manifests_file() {
let temporary = tempdir().expect("tempdir");
let crate_dir = temporary.path().join("demo-hydrolysis-deadbeef");
std::fs::create_dir_all(&crate_dir).expect("crate dir");
std::fs::write(crate_dir.join("Cargo.toml"), "[package]\n").expect("manifest");
let manifest =
dunce::canonicalize(crate_dir.join("Cargo.toml")).expect("canonical manifest");
let reported = crate_dir.join("target/debug/deps/demo_hydrolysis_deadbeef-abc123.rlib");
std::fs::create_dir_all(reported.parent().expect("deps dir")).expect("deps dir");
std::fs::write(&reported, []).expect("reported artifact");
let artifact_json = |manifest: &std::path::Path, file: &std::path::Path, name: &str| {
serde_json::json!({
"reason": "compiler-artifact",
"package_id": format!("path+file:///x#{name}@0.1.0"),
"manifest_path": manifest,
"target": {
"kind": ["lib"],
"crate_types": ["lib"],
"name": name,
"src_path": manifest.parent().expect("manifest dir").join("src/lib.rs"),
"edition": "2021",
"doc": true,
"doctest": true,
"test": true,
},
"profile": {
"opt_level": "0",
"debuginfo": 0,
"debug_assertions": true,
"overflow_checks": true,
"test": false,
},
"features": [],
"filenames": [file],
"executable": null,
"fresh": true,
})
.to_string()
};
let other_manifest = temporary.path().join("other").join("Cargo.toml");
let other_file = temporary.path().join("other.rlib");
let stdout = format!(
"{}\n{}\n",
artifact_json(&other_manifest, &other_file, "other"),
artifact_json(&manifest, &reported, "demo_hydrolysis_deadbeef"),
);
let resolved = super::reported_artifact(
stdout.as_bytes(),
&crate_dir,
CargoTarget::Lib,
Some("rlib"),
)
.expect("the matching manifest's artifact resolves");
assert_eq!(resolved, reported);
let foreign_only = artifact_json(&other_manifest, &other_file, "other");
assert!(
super::reported_artifact(
foreign_only.as_bytes(),
&crate_dir,
CargoTarget::Lib,
Some("rlib"),
)
.is_err(),
"an artifact for another manifest is never selected"
);
}
#[test]
fn stale_shared_dylib_packages_flags_a_foreign_written_artifact() {
smol::block_on(async {
let temporary = tempdir().expect("tempdir");
let deps = temporary.path().join("debug/deps");
std::fs::create_dir_all(&deps).expect("deps dir");
let dylib = deps.join("libwaterui_dylib.so");
std::fs::write(&dylib, []).expect("dylib");
let ours = temporary.path().join("our project");
std::fs::create_dir_all(ours.join("src")).expect("our manifest dir");
let manifest = ours.join("Cargo.toml");
std::fs::write(&manifest, "").expect("manifest");
let own_source = ours.join("src/lib.rs");
std::fs::write(&own_source, "").expect("own source");
let artifact = |fresh: bool| {
serde_json::json!({
"reason": "compiler-artifact",
"package_id": "path+file:///x#waterui-dylib@0.1.0",
"manifest_path": manifest,
"target": {
"kind": ["lib"],
"crate_types": ["dylib"],
"name": "waterui_dylib",
"src_path": own_source,
"edition": "2021",
"doc": true,
"doctest": true,
"test": true,
},
"profile": {
"opt_level": "0",
"debuginfo": 0,
"debug_assertions": true,
"overflow_checks": true,
"test": false,
},
"features": [],
"filenames": [dylib],
"executable": null,
"fresh": fresh,
})
.to_string()
};
let dep_info = deps.join("waterui_dylib.d");
let foreign = temporary.path().join("foreign");
std::fs::create_dir_all(foreign.join("src")).expect("foreign source dir");
let foreign_source = foreign.join("src/lib.rs");
std::fs::write(&foreign_source, "").expect("foreign source");
let dep_escape =
|path: &std::path::Path| path.display().to_string().replace(' ', "\\ ");
let write_dep_info = |source: &std::path::Path| {
std::fs::write(
&dep_info,
format!("{}: {}\n", dep_escape(&dylib), dep_escape(source)),
)
.expect("dep-info");
};
write_dep_info(&foreign_source);
let stale = super::stale_shared_dylib_packages(artifact(true).as_bytes())
.await
.expect("scan");
assert_eq!(stale, ["waterui-dylib"]);
write_dep_info(&own_source);
let stale = super::stale_shared_dylib_packages(artifact(true).as_bytes())
.await
.expect("scan");
assert!(stale.is_empty(), "our own artifact is never stale");
write_dep_info(&foreign_source);
let stale = super::stale_shared_dylib_packages(artifact(false).as_bytes())
.await
.expect("scan");
assert!(stale.is_empty(), "a non-fresh unit wrote the file itself");
});
}
#[test]
fn stale_check_reads_build_dir_dep_info_and_skips_proc_macros() {
smol::block_on(async {
let temporary = tempdir().expect("tempdir");
let profile = temporary.path().join("debug");
let unit_dir = profile.join("build/waterui-dylib/0123456789abcdef/out");
std::fs::create_dir_all(&unit_dir).expect("unit dir");
let dylib = profile.join("libwaterui_dylib.so");
std::fs::write(&dylib, []).expect("dylib");
let rmeta = unit_dir.join("libwaterui_dylib.rmeta");
std::fs::write(&rmeta, []).expect("rmeta");
let ours = temporary.path().join("ours");
std::fs::create_dir_all(ours.join("src")).expect("our manifest dir");
let manifest = ours.join("Cargo.toml");
std::fs::write(&manifest, "").expect("manifest");
let foreign = temporary.path().join("foreign/src/lib.rs");
std::fs::create_dir_all(foreign.parent().expect("parent")).expect("foreign dir");
std::fs::write(&foreign, []).expect("foreign source");
std::fs::write(
unit_dir.join("waterui_dylib.d"),
format!("{}: {}\n", dylib.display(), foreign.display()),
)
.expect("dep-info");
let unit = |name: &str, crate_type: &str, filenames: Vec<&std::path::Path>| {
serde_json::json!({
"reason": "compiler-artifact",
"package_id": format!("path+file:///x#{name}@0.1.0"),
"manifest_path": manifest,
"target": {
"kind": [if crate_type == "proc-macro" { "proc-macro" } else { "lib" }],
"crate_types": [crate_type],
"name": name.replace('-', "_"),
"src_path": ours.join("src/lib.rs"),
"edition": "2021",
"doc": true,
"doctest": true,
"test": true,
},
"profile": {
"opt_level": "0",
"debuginfo": 0,
"debug_assertions": true,
"overflow_checks": true,
"test": false,
},
"features": [],
"filenames": filenames,
"executable": null,
"fresh": true,
})
.to_string()
};
let macro_dylib = unit_dir.join("libthiserror_impl-0123456789abcdef.so");
let stdout = format!(
"{}\n{}\n",
unit("thiserror-impl", "proc-macro", vec![¯o_dylib]),
unit("waterui-dylib", "dylib", vec![&dylib, &rmeta]),
);
let stale = super::stale_shared_dylib_packages(stdout.as_bytes())
.await
.expect("scan");
assert_eq!(stale, ["waterui-dylib"]);
std::fs::remove_file(unit_dir.join("waterui_dylib.d")).expect("remove dep-info");
let error = super::stale_shared_dylib_packages(stdout.as_bytes())
.await
.expect_err("a fresh dylib without dep-info is an error");
assert!(
error.to_string().contains("no dep-info was found"),
"{error}"
);
});
}
#[test]
fn dep_info_prerequisites_unescape_spaces_and_join_continued_rules() {
let contents = concat!(
"C:\\out\\app.dll: C:\\work\\my\\ app\\src\\lib.rs \\\n",
" C:\\work\\my\\ app\\build.rs C:\\work\\cost$$.rs\n",
"\n",
"C:\\work\\my\\ app\\src\\lib.rs:\n",
);
assert_eq!(
super::dep_info_prerequisites(contents),
vec![
PathBuf::from("C:\\work\\my app\\src\\lib.rs"),
PathBuf::from("C:\\work\\my app\\build.rs"),
PathBuf::from("C:\\work\\cost$$.rs"),
]
);
}
#[test]
fn static_packaging_removes_only_staged_android_runtime_libraries() {
smol::block_on(async {
let directory = tempdir().expect("temporary Android runtime directory");
let android_triple = triple("aarch64-linux-android");
for file_name in [
"libwaterui_dylib.so",
"libstd-old.so",
"libwaterui_app.so",
"libc++_shared.so",
] {
std::fs::write(directory.path().join(file_name), [])
.expect("write staged runtime test file");
}
RustDynamicLibraries::remove_staged(directory.path(), &android_triple)
.await
.expect("remove shared Rust runtime libraries");
assert!(!directory.path().join("libwaterui_dylib.so").exists());
assert!(!directory.path().join("libstd-old.so").exists());
assert!(directory.path().join("libwaterui_app.so").exists());
assert!(directory.path().join("libc++_shared.so").exists());
});
}
}