1use std::{
4 ffi::OsString,
5 io::{self, Write as _},
6 path::{Path, PathBuf},
7 process::Stdio,
8};
9
10use eyre::{Context as _, bail};
11use futures_util::StreamExt as _;
12use smol::{io::AsyncReadExt as _, process::Command, unblock};
13use target_lexicon::{Environment, OperatingSystem, Triple};
14
15use crate::project::Project;
16use crate::utils::{run_command, std_output_enabled};
17
18#[must_use]
20pub const fn lib_extension_for_triple(triple: &Triple) -> &'static str {
21 match triple.operating_system {
22 OperatingSystem::Darwin(_)
23 | OperatingSystem::MacOSX { .. }
24 | OperatingSystem::IOS(_)
25 | OperatingSystem::TvOS(_)
26 | OperatingSystem::WatchOS(_)
27 | OperatingSystem::VisionOS(_) => "dylib",
28 OperatingSystem::Windows => "dll",
29 _ => "so",
31 }
32}
33
34pub async fn project_toolchain(project: &Project) -> eyre::Result<String> {
40 Ok(crate::toolchain::rust::project_rustup_toolchain(project.root()).await?)
41}
42
43pub async fn rust_target_libdir(triple: &Triple, toolchain: &str) -> eyre::Result<PathBuf> {
49 let target = triple.to_string();
50 let host = crate::toolchain::Host::current().with_env("RUSTUP_TOOLCHAIN", toolchain);
51 let output = host
52 .run(
53 "rustc",
54 ["--print", "target-libdir", "--target", target.as_str()],
55 )
56 .await?;
57 let libdir = output.trim();
58 if libdir.is_empty() {
59 bail!("`rustc --print target-libdir --target {target}` returned an empty path");
60 }
61 let path = PathBuf::from(libdir);
62 if !path.is_dir() {
63 bail!(
64 "Rust target libdir does not exist for dynamic linking: {}",
65 path.display()
66 );
67 }
68 Ok(path)
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub(crate) enum CargoTarget<'a> {
78 Lib,
80 Binary(&'a str),
82}
83
84impl<'a> CargoTarget<'a> {
85 fn cargo_args(self) -> Vec<&'a str> {
86 match self {
87 Self::Lib => vec!["--lib"],
88 Self::Binary(name) => vec!["--bin", name],
89 }
90 }
91
92 const fn accepts_crate_type_override(self) -> bool {
93 matches!(self, Self::Lib)
94 }
95
96 fn matches(&self, target: &cargo_metadata::Target) -> bool {
99 use cargo_metadata::TargetKind;
100 match self {
101 Self::Binary(name) => {
102 target.name.as_str() == *name && target.kind.contains(&TargetKind::Bin)
103 }
104 Self::Lib => target.kind.iter().any(|kind| {
105 matches!(
106 kind,
107 TargetKind::Lib
108 | TargetKind::RLib
109 | TargetKind::DyLib
110 | TargetKind::CDyLib
111 | TargetKind::StaticLib
112 | TargetKind::ProcMacro
113 )
114 }),
115 }
116 }
117}
118
119#[derive(Debug)]
122pub struct BuiltTarget {
123 pub profile_dir: PathBuf,
126 pub artifact: PathBuf,
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub enum RustLinkage {
135 Static,
137 SharedRuntime,
139}
140
141pub fn configure_generated_crate_compilation(command: &mut Command) {
157 command.env("CARGO_INCREMENTAL", "0");
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct RustDynamicLibraries {
163 waterui: PathBuf,
164 standard_library: PathBuf,
165 triple: Triple,
166}
167
168impl RustDynamicLibraries {
169 pub async fn resolve(lib_dir: &Path, triple: &Triple, project: &Project) -> eyre::Result<Self> {
180 let file_name = dynamic_library_file_name("waterui_dylib", triple);
181 let waterui = [
186 lib_dir.join("deps").join(&file_name),
187 lib_dir.join(&file_name),
188 ]
189 .into_iter()
190 .find(|path| path.is_file())
191 .ok_or_else(|| {
192 eyre::eyre!(
193 "Shared WaterUI runtime was not built at {}",
194 lib_dir.join("deps").join(&file_name).display()
195 )
196 })?;
197
198 let resolution_triple = triple.clone();
204 let deps_dir = lib_dir.join("deps");
205 let staged =
206 unblock(move || resolve_rust_standard_library_in(&deps_dir, &resolution_triple)).await;
207 let standard_library = match staged {
208 Ok(path) => path,
209 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
210 let toolchain = project_toolchain(project).await?;
211 let target_libdir = rust_target_libdir(triple, &toolchain).await?;
212 let resolution_triple = triple.clone();
213 unblock(move || {
214 resolve_rust_standard_library_in(&target_libdir, &resolution_triple)
215 })
216 .await?
217 }
218 Err(error) => return Err(error.into()),
219 };
220
221 Ok(Self {
222 waterui,
223 standard_library,
224 triple: triple.clone(),
225 })
226 }
227
228 #[must_use]
230 pub fn waterui(&self) -> &Path {
231 &self.waterui
232 }
233
234 #[must_use]
236 pub fn standard_library(&self) -> &Path {
237 &self.standard_library
238 }
239
240 pub fn iter(&self) -> impl Iterator<Item = &Path> {
242 [self.waterui(), self.standard_library()].into_iter()
243 }
244
245 pub async fn stage(&self, destination: &Path) -> eyre::Result<()> {
256 smol::fs::create_dir_all(destination).await?;
257 let sources: Vec<PathBuf> = self.iter().map(|path| (*path).to_path_buf()).collect();
262 Self::remove_staged_except(destination, &self.triple, &sources).await?;
263 for source in &sources {
264 let file_name = source.file_name().ok_or_else(|| {
265 eyre::eyre!(
266 "Dynamic library path has no file name: {}",
267 source.display()
268 )
269 })?;
270 let staged = destination.join(file_name);
271 if *source == staged {
272 continue;
273 }
274 crate::utils::copy_file(source, &staged)
275 .await
276 .wrap_err_with(|| {
277 format!(
278 "Failed to stage {} to {}",
279 source.display(),
280 staged.display()
281 )
282 })?;
283 }
284 Ok(())
285 }
286
287 pub async fn remove_staged(destination: &Path, triple: &Triple) -> eyre::Result<()> {
292 Self::remove_staged_except(destination, triple, &[]).await
293 }
294
295 async fn remove_staged_except(
299 destination: &Path,
300 triple: &Triple,
301 keep: &[PathBuf],
302 ) -> eyre::Result<()> {
303 if !destination.is_dir() {
304 return Ok(());
305 }
306
307 let waterui = dynamic_library_file_name("waterui_dylib", triple);
308 let (standard_library_prefix, extension) =
309 if triple.operating_system == OperatingSystem::Windows {
310 ("std-", "dll")
311 } else {
312 ("libstd-", lib_extension_for_triple(triple))
313 };
314 let mut entries = smol::fs::read_dir(destination).await?;
315 while let Some(entry) = entries.next().await {
316 let entry = entry?;
317 if keep.contains(&entry.path()) {
318 continue;
319 }
320 let file_name = entry.file_name();
321 let file_name = file_name.to_string_lossy();
322 if file_name == waterui
323 || (file_name.starts_with(standard_library_prefix)
324 && entry.path().extension().and_then(|value| value.to_str()) == Some(extension))
325 {
326 smol::fs::remove_file(entry.path()).await?;
327 }
328 }
329 Ok(())
330 }
331}
332
333fn dynamic_library_file_name(crate_name: &str, triple: &Triple) -> String {
334 if triple.operating_system == OperatingSystem::Windows {
335 format!("{crate_name}.dll")
336 } else {
337 format!("lib{crate_name}.{}", lib_extension_for_triple(triple))
338 }
339}
340
341fn resolve_rust_standard_library_in(libdir: &Path, triple: &Triple) -> std::io::Result<PathBuf> {
347 let (prefix, extension) = if triple.operating_system == OperatingSystem::Windows {
348 ("std-", "dll")
349 } else {
350 ("libstd-", lib_extension_for_triple(triple))
351 };
352 let entries = match std::fs::read_dir(libdir) {
353 Ok(entries) => entries,
354 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
355 return Err(std::io::Error::new(
356 std::io::ErrorKind::NotFound,
357 format!("{} does not exist", libdir.display()),
358 ));
359 }
360 Err(error) => return Err(error),
361 };
362 let mut matches = entries
363 .filter_map(|entry| entry.ok().map(|entry| entry.path()))
364 .filter(|path| {
365 path.file_name()
366 .and_then(|name| name.to_str())
367 .is_some_and(|name| {
368 name.starts_with(prefix)
369 && path.extension().and_then(|extension| extension.to_str())
370 == Some(extension)
371 })
372 })
373 .collect::<Vec<_>>();
374 matches.sort_unstable();
375 match matches.as_slice() {
376 [path] => Ok(path.clone()),
377 [] => Err(std::io::Error::new(
378 std::io::ErrorKind::NotFound,
379 format!(
380 "Rust target libdir {} contains no dynamic standard library for {triple}",
381 libdir.display()
382 ),
383 )),
384 _ => Err(std::io::Error::other(format!(
385 "Rust target libdir {} contains multiple dynamic standard libraries for {triple}: {}",
386 libdir.display(),
387 matches
388 .iter()
389 .map(|path| path.display().to_string())
390 .collect::<Vec<_>>()
391 .join(", ")
392 ))),
393 }
394}
395
396#[derive(Debug, Clone)]
398pub struct RustBuild {
399 path: PathBuf,
400 triple: Triple,
401 project: Option<Project>,
402 target_dir: Option<PathBuf>,
404 sccache_path: Option<PathBuf>,
406 features: Vec<String>,
408 crate_type_override: Option<String>,
410 rustc_flags: Vec<String>,
412 final_rustc_args: Vec<String>,
421 build_std_toolchain: Option<String>,
430 envs: Vec<(String, OsString)>,
432 progress: Option<BuildProgress>,
434}
435
436#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
442pub enum BuildProfile {
443 #[default]
445 Debug,
446 Optimized,
450 Release,
452 Profiling,
455}
456
457impl BuildProfile {
458 #[must_use]
461 pub const fn is_release(self) -> bool {
462 matches!(self, Self::Release | Self::Profiling)
463 }
464
465 #[must_use]
468 pub const fn is_development(self) -> bool {
469 !self.is_release()
470 }
471
472 fn development_envs(self) -> Vec<(String, OsString)> {
491 let entries: &[(&str, &str)] = match self {
492 Self::Debug => &[],
493 Self::Optimized => &[
494 ("CARGO_PROFILE_DEV_OPT_LEVEL", "1"),
495 ("CARGO_PROFILE_DEV_DEBUG", "true"),
496 ("CARGO_PROFILE_DEV_DEBUG_ASSERTIONS", "false"),
497 ("CARGO_PROFILE_DEV_OVERFLOW_CHECKS", "false"),
498 ],
499 Self::Release => &[
500 ("CARGO_PROFILE_RELEASE_OPT_LEVEL", "3"),
501 ("CARGO_PROFILE_RELEASE_PANIC", "unwind"),
502 ("CARGO_PROFILE_RELEASE_LTO", "off"),
503 ],
504 Self::Profiling => &[
505 ("CARGO_PROFILE_RELEASE_OPT_LEVEL", "3"),
506 ("CARGO_PROFILE_RELEASE_PANIC", "unwind"),
507 ("CARGO_PROFILE_RELEASE_LTO", "off"),
508 ("CARGO_PROFILE_RELEASE_DEBUG", "true"),
509 ("CARGO_PROFILE_RELEASE_STRIP", "none"),
510 ],
511 };
512 entries
513 .iter()
514 .map(|(key, value)| ((*key).to_string(), OsString::from(*value)))
515 .collect()
516 }
517}
518
519#[derive(Debug, Clone)]
521pub struct BuildOptions {
522 profile: BuildProfile,
523 output_dir: Option<std::path::PathBuf>,
524 sccache_path: Option<std::path::PathBuf>,
526 target_triple: Option<Triple>,
528 linkage: RustLinkage,
530 dynamic_module_loading: bool,
535 dev_server: bool,
538 cargo_envs: Vec<(String, OsString)>,
540 progress: Option<BuildProgress>,
542}
543
544impl BuildOptions {
545 #[must_use]
552 pub fn development(profile: BuildProfile) -> Self {
553 Self {
554 profile,
555 output_dir: None,
556 sccache_path: None,
557 target_triple: None,
558 linkage: RustLinkage::SharedRuntime,
559 dynamic_module_loading: false,
560 dev_server: false,
561 cargo_envs: profile.development_envs(),
562 progress: None,
563 }
564 }
565
566 #[must_use]
572 pub fn with_static_runtime(mut self) -> Self {
573 self.linkage = RustLinkage::Static;
574 self.cargo_envs.retain(|(key, _)| {
577 key != "CARGO_PROFILE_RELEASE_PANIC" && key != "CARGO_PROFILE_RELEASE_LTO"
578 });
579 self
580 }
581
582 #[must_use]
588 pub const fn packaging(profile: BuildProfile) -> Self {
589 Self {
590 profile,
591 output_dir: None,
592 sccache_path: None,
593 target_triple: None,
594 linkage: RustLinkage::Static,
595 dynamic_module_loading: false,
596 dev_server: false,
597 cargo_envs: Vec::new(),
598 progress: None,
599 }
600 }
601
602 #[must_use]
604 pub const fn is_release(&self) -> bool {
605 self.profile.is_release()
606 }
607
608 #[must_use]
610 pub const fn profile(&self) -> BuildProfile {
611 self.profile
612 }
613
614 #[must_use]
616 pub fn cargo_envs(&self) -> &[(String, OsString)] {
617 &self.cargo_envs
618 }
619
620 #[must_use]
622 pub const fn with_dev_server(mut self, dev_server: bool) -> Self {
623 self.dev_server = dev_server;
624 self
625 }
626
627 #[must_use]
629 pub const fn uses_dev_server(&self) -> bool {
630 self.dev_server
631 }
632
633 #[must_use]
635 pub fn output_dir(&self) -> Option<&std::path::Path> {
636 self.output_dir.as_deref()
637 }
638
639 #[must_use]
641 pub fn with_output_dir(mut self, output_dir: impl Into<std::path::PathBuf>) -> Self {
642 self.output_dir = Some(output_dir.into());
643 self
644 }
645
646 #[must_use]
648 pub fn sccache_path(&self) -> Option<&std::path::Path> {
649 self.sccache_path.as_deref()
650 }
651
652 #[must_use]
657 pub fn with_sccache(mut self, sccache_path: impl Into<std::path::PathBuf>) -> Self {
658 self.sccache_path = Some(sccache_path.into());
659 self
660 }
661
662 #[must_use]
664 pub const fn target_triple(&self) -> Option<&Triple> {
665 self.target_triple.as_ref()
666 }
667
668 #[must_use]
670 pub fn with_target_triple(mut self, target_triple: Triple) -> Self {
671 self.target_triple = Some(target_triple);
672 self
673 }
674
675 #[must_use]
677 pub const fn linkage(&self) -> RustLinkage {
678 self.linkage
679 }
680
681 #[must_use]
687 pub const fn with_dynamic_module_loading(mut self) -> Self {
688 self.dynamic_module_loading = true;
689 self
690 }
691
692 #[must_use]
694 pub const fn loads_dynamic_modules(&self) -> bool {
695 self.dynamic_module_loading
696 }
697
698 #[must_use]
701 pub fn with_progress(mut self, progress: BuildProgress) -> Self {
702 self.progress = Some(progress);
703 self
704 }
705
706 #[must_use]
708 pub const fn progress(&self) -> Option<&BuildProgress> {
709 self.progress.as_ref()
710 }
711}
712
713#[derive(Debug, thiserror::Error)]
715pub enum RustBuildError {
716 #[error("Failed to execute cargo build: {0}")]
718 FailToExecuteCargoBuild(std::io::Error),
719
720 #[error("Failed to build Rust library: {0}")]
722 FailToBuildRustLibrary(std::io::Error),
723}
724
725#[derive(Debug, Clone, PartialEq, Eq)]
733pub enum CompileEvent {
734 Unit {
738 phase: &'static str,
740 name: String,
742 version: Option<String>,
744 },
745 Finished(String),
747 Line(String),
750}
751
752#[derive(Clone)]
757pub struct BuildProgress {
758 report: std::sync::Arc<dyn Fn(CompileEvent) + Send + Sync>,
759 shows_all_lines: bool,
763}
764
765impl std::fmt::Debug for BuildProgress {
766 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
767 formatter.write_str("BuildProgress(..)")
768 }
769}
770
771impl BuildProgress {
772 #[must_use]
774 pub fn new(report: impl Fn(CompileEvent) + Send + Sync + 'static) -> Self {
775 Self {
776 report: std::sync::Arc::new(report),
777 shows_all_lines: false,
778 }
779 }
780
781 #[must_use]
784 pub const fn showing_all_lines(mut self) -> Self {
785 self.shows_all_lines = true;
786 self
787 }
788
789 #[must_use]
791 pub const fn shows_all_lines(&self) -> bool {
792 self.shows_all_lines
793 }
794
795 fn report(&self, event: CompileEvent) {
796 (self.report)(event);
797 }
798}
799
800const CARGO_UNIT_PHASES: &[&str] = &[
802 "Compiling",
803 "Checking",
804 "Fresh",
805 "Downloading",
806 "Downloaded",
807 "Doc-tests",
808];
809
810fn classify_compile_line(line: &str) -> CompileEvent {
818 let raw = line.trim();
819 let stripped = console::strip_ansi_codes(raw);
820 let text = stripped.trim();
821 for phase in CARGO_UNIT_PHASES {
822 let Some(rest) = text
823 .strip_prefix(phase)
824 .and_then(|rest| rest.strip_prefix(' '))
825 else {
826 continue;
827 };
828 let Some((name, version)) = rest.split_once(" v") else {
831 return CompileEvent::Line(raw.to_owned());
832 };
833 let version = version.split([' ', '(']).next().unwrap_or_default();
834 return CompileEvent::Unit {
835 phase,
836 name: name.to_owned(),
837 version: (!version.is_empty()).then(|| version.to_owned()),
838 };
839 }
840 if text.starts_with("Finished ") {
841 return CompileEvent::Finished(raw.to_owned());
842 }
843 CompileEvent::Line(raw.to_owned())
844}
845
846pub(crate) async fn command_output_with_progress(
858 command: &mut Command,
859 progress: Option<BuildProgress>,
860) -> io::Result<std::process::Output> {
861 let mut child = command
862 .kill_on_drop(true)
863 .stdin(Stdio::null())
864 .stdout(Stdio::piped())
865 .stderr(Stdio::piped())
866 .spawn()?;
867 let stdout_pipe = child.stdout.take().expect("stdout is piped");
868 let stderr_pipe = child.stderr.take().expect("stderr is piped");
869
870 let echo = progress.is_none() && std_output_enabled();
873 let stdout_task = smol::spawn(drain_pipe(stdout_pipe));
876 let stderr_task = smol::spawn(drain_cargo_stderr(stderr_pipe, progress, echo));
877 let status = child.status().await?;
878 let stdout = stdout_task.await?;
879 let stderr = stderr_task.await?;
880 Ok(std::process::Output {
881 status,
882 stdout,
883 stderr,
884 })
885}
886
887async fn drain_pipe(mut reader: impl smol::io::AsyncRead + Unpin) -> io::Result<Vec<u8>> {
889 let mut collected = Vec::new();
890 let mut chunk = [0u8; 8192];
891 loop {
892 let read = reader.read(&mut chunk).await?;
893 if read == 0 {
894 break;
895 }
896 collected.extend_from_slice(&chunk[..read]);
897 }
898 Ok(collected)
899}
900
901async fn drain_cargo_stderr(
905 mut reader: impl smol::io::AsyncRead + Unpin,
906 progress: Option<BuildProgress>,
907 echo: bool,
908) -> io::Result<Vec<u8>> {
909 let mut collected = Vec::new();
910 let mut pending: Vec<u8> = Vec::new();
911 let mut chunk = [0u8; 8192];
912 loop {
913 let read = reader.read(&mut chunk).await?;
914 if read == 0 {
915 break;
916 }
917 collected.extend_from_slice(&chunk[..read]);
918 if echo {
919 let _ = io::stderr().write_all(&chunk[..read]);
920 let _ = io::stderr().flush();
921 }
922 if let Some(sink) = &progress {
923 pending.extend_from_slice(&chunk[..read]);
924 while let Some(newline) = pending.iter().position(|byte| *byte == b'\n') {
928 let line: Vec<u8> = pending.drain(..=newline).collect();
929 let line = String::from_utf8_lossy(&line);
930 let line = line.trim_end();
931 if !line.trim().is_empty() {
932 sink.report(classify_compile_line(line));
933 }
934 }
935 }
936 }
937 if let Some(sink) = &progress {
938 let tail = String::from_utf8_lossy(&pending);
939 let tail = tail.trim_end();
940 if !tail.trim().is_empty() {
941 sink.report(classify_compile_line(tail));
942 }
943 }
944 Ok(collected)
945}
946
947impl RustBuild {
948 pub fn new(path: impl AsRef<Path>, triple: Triple) -> Self {
950 Self {
951 path: path.as_ref().to_path_buf(),
952 triple,
953 project: None,
954 target_dir: None,
955 sccache_path: None,
956 features: Vec::new(),
957 crate_type_override: None,
958 rustc_flags: Vec::new(),
959 final_rustc_args: Vec::new(),
960 build_std_toolchain: None,
961 envs: Vec::new(),
962 progress: None,
963 }
964 }
965
966 pub(crate) fn with_project(mut self, project: &Project) -> Self {
973 self.project = Some(project.clone());
974 self
975 }
976
977 #[must_use]
979 pub fn with_target_dir(mut self, target_dir: impl Into<PathBuf>) -> Self {
980 self.target_dir = Some(target_dir.into());
981 self
982 }
983
984 #[must_use]
989 pub fn with_sccache(mut self, sccache_path: PathBuf) -> Self {
990 self.sccache_path = Some(sccache_path);
991 self
992 }
993
994 #[must_use]
998 pub fn with_feature(mut self, feature: impl Into<String>) -> Self {
999 self.features.push(feature.into());
1000 self
1001 }
1002
1003 #[must_use]
1005 pub fn with_features(mut self, features: impl IntoIterator<Item = impl Into<String>>) -> Self {
1006 self.features.extend(features.into_iter().map(Into::into));
1007 self
1008 }
1009
1010 #[must_use]
1012 pub fn features(&self) -> &[String] {
1013 &self.features
1014 }
1015
1016 #[must_use]
1018 pub fn with_rustc_flag(mut self, flag: impl Into<String>) -> Self {
1019 self.rustc_flags.push(flag.into());
1020 self
1021 }
1022
1023 #[must_use]
1030 pub fn with_final_rustc_arg(mut self, flag: impl Into<String>) -> Self {
1031 self.final_rustc_args.push(flag.into());
1032 self
1033 }
1034
1035 #[must_use]
1048 pub fn with_build_std(mut self, toolchain: impl Into<String>) -> Self {
1049 self.build_std_toolchain = Some(toolchain.into());
1050 self
1051 }
1052
1053 #[must_use]
1055 pub fn with_preferred_dynamic_linking(self) -> Self {
1056 self.with_rustc_flag("-Cprefer-dynamic")
1057 .with_rustc_flag("-Crpath")
1058 }
1059
1060 #[must_use]
1067 pub fn with_linkage(
1068 self,
1069 linkage: RustLinkage,
1070 development_feature: &str,
1071 loader_search_path: Option<&str>,
1072 ) -> Self {
1073 if linkage == RustLinkage::Static {
1074 return self;
1075 }
1076 let build = self
1077 .with_feature(development_feature)
1078 .with_preferred_dynamic_linking();
1079 match loader_search_path {
1080 Some(path) => build.with_final_rustc_arg(format!("-Clink-arg=-Wl,-rpath,{path}")),
1081 None => build,
1082 }
1083 }
1084
1085 #[must_use]
1087 pub fn with_crate_type_override(mut self, crate_type: impl Into<String>) -> Self {
1088 self.crate_type_override = Some(crate_type.into());
1089 self
1090 }
1091
1092 #[must_use]
1094 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<OsString>) -> Self {
1095 self.envs.push((key.into(), value.into()));
1096 self
1097 }
1098
1099 #[must_use]
1101 pub fn with_envs(mut self, envs: impl IntoIterator<Item = (String, OsString)>) -> Self {
1102 self.envs.extend(envs);
1103 self
1104 }
1105
1106 #[must_use]
1111 pub fn with_progress(mut self, progress: BuildProgress) -> Self {
1112 self.progress = Some(progress);
1113 self
1114 }
1115
1116 #[must_use]
1118 pub const fn triple(&self) -> &Triple {
1119 &self.triple
1120 }
1121
1122 pub async fn dev_build(&self) -> Result<BuiltTarget, RustBuildError> {
1130 self.build_lib(false).await
1131 }
1132
1133 pub async fn release_build(&self) -> Result<BuiltTarget, RustBuildError> {
1139 self.build_lib(true).await
1140 }
1141
1142 pub async fn build_lib(&self, release: bool) -> Result<BuiltTarget, RustBuildError> {
1153 self.build_inner(release, CargoTarget::Lib, self.lib_artifact_extension())
1154 .await
1155 }
1156
1157 pub async fn build_dylib(&self, release: bool) -> Result<PathBuf, RustBuildError> {
1167 let built = self
1168 .build_inner(
1169 release,
1170 CargoTarget::Lib,
1171 Some(lib_extension_for_triple(&self.triple)),
1172 )
1173 .await?;
1174 Ok(built.artifact)
1175 }
1176
1177 pub async fn build_binary(
1187 &self,
1188 binary_name: &str,
1189 release: bool,
1190 ) -> Result<PathBuf, RustBuildError> {
1191 let built = self
1192 .build_inner(release, CargoTarget::Binary(binary_name), None)
1193 .await?;
1194 Ok(built.artifact)
1195 }
1196
1197 pub async fn dylib_path(
1205 &self,
1206 crate_name: &str,
1207 release: bool,
1208 ) -> Result<PathBuf, RustBuildError> {
1209 let lib_dir = self.lib_output_dir(release).await?;
1210 let lib_name = crate_name.replace('-', "_");
1211 let ext = lib_extension_for_triple(&self.triple);
1212 Ok(lib_dir.join(format!("lib{lib_name}.{ext}")))
1213 }
1214
1215 async fn build_inner(
1217 &self,
1218 release: bool,
1219 cargo_target: CargoTarget<'_>,
1220 artifact_extension: Option<&'static str>,
1221 ) -> Result<BuiltTarget, RustBuildError> {
1222 let mut output = self.cargo_build_output(release, cargo_target).await?;
1223
1224 if !output.status.success() {
1225 let mut combined = combined_build_output(&output);
1226
1227 if should_retry_after_cmake_generator_mismatch(&combined)
1230 && self.clean_stale_cmake_build_dirs().await?
1231 {
1232 output = self.cargo_build_output(release, cargo_target).await?;
1233 combined = combined_build_output(&output);
1234 }
1235
1236 if !output.status.success() && should_auto_install_meson(&combined) {
1237 match ensure_meson_installed_for_build().await {
1238 Ok(()) => {
1239 output = self.cargo_build_output(release, cargo_target).await?;
1240 }
1241 Err(install_err) => {
1242 return Err(RustBuildError::FailToBuildRustLibrary(
1243 std::io::Error::other(format!(
1244 "Cargo build failed and meson appears missing.\n\
1245Automatic meson installation failed: {install_err}\n\n{}",
1246 self.failure_report(&combined)
1247 )),
1248 ));
1249 }
1250 }
1251 }
1252 }
1253
1254 if !output.status.success() {
1255 let combined = combined_build_output(&output);
1256 return Err(RustBuildError::FailToBuildRustLibrary(
1257 std::io::Error::other(format!(
1258 "Cargo build failed:\n{}",
1259 self.failure_report(&combined)
1260 )),
1261 ));
1262 }
1263
1264 let stale = stale_shared_dylib_packages(&output.stdout).await?;
1273 if !stale.is_empty() {
1274 let target_dir = self.target_directory().await?;
1275 for package in &stale {
1276 clean_cargo_package(&self.path, package, &target_dir).await?;
1277 }
1278 output = self.cargo_build_output(release, cargo_target).await?;
1279 if !output.status.success() {
1280 let combined = combined_build_output(&output);
1281 return Err(RustBuildError::FailToBuildRustLibrary(
1282 std::io::Error::other(format!(
1283 "Cargo build failed:\n{}",
1284 self.failure_report(&combined)
1285 )),
1286 ));
1287 }
1288 }
1289
1290 let artifact =
1291 reported_artifact(&output.stdout, &self.path, cargo_target, artifact_extension)?;
1292 let profile_dir = self.lib_output_dir(release).await?;
1293 Ok(BuiltTarget {
1294 profile_dir,
1295 artifact,
1296 })
1297 }
1298
1299 fn lib_artifact_extension(&self) -> Option<&'static str> {
1302 self.crate_type_override
1303 .as_deref()
1304 .and_then(|crate_type| crate_type_artifact_extension(crate_type, &self.triple))
1305 }
1306
1307 fn failure_report(&self, combined: &str) -> String {
1310 if self
1311 .progress
1312 .as_ref()
1313 .is_some_and(BuildProgress::shows_all_lines)
1314 {
1315 output_tail(combined)
1316 } else {
1317 combined.to_owned()
1318 }
1319 }
1320
1321 async fn clean_stale_cmake_build_dirs(&self) -> Result<bool, RustBuildError> {
1322 let target_dir = self.target_directory().await?;
1323 let triple = self.triple.to_string();
1324
1325 let removed = unblock(move || {
1326 let mut removed = 0usize;
1327 removed +=
1328 remove_cmake_build_dirs_in(&target_dir.join(&triple).join("debug").join("build"))?;
1329 removed += remove_cmake_build_dirs_in(
1330 &target_dir.join(&triple).join("release").join("build"),
1331 )?;
1332 Ok::<usize, std::io::Error>(removed)
1333 })
1334 .await
1335 .map_err(|error| {
1336 RustBuildError::FailToBuildRustLibrary(std::io::Error::other(format!(
1337 "Failed to clean stale CMake cache: {error}"
1338 )))
1339 })?;
1340
1341 Ok(removed > 0)
1342 }
1343
1344 async fn cargo_build_output(
1345 &self,
1346 release: bool,
1347 cargo_target: CargoTarget<'_>,
1348 ) -> Result<std::process::Output, RustBuildError> {
1349 let framework = self.project.as_ref().and_then(|project| {
1350 project
1351 .manifest()
1352 .framework
1353 .as_ref()
1354 .map(|framework| (project, framework))
1355 });
1356 if let Some((project, framework)) = framework {
1357 framework
1358 .prepare_build(project, &self.path, &self.features)
1359 .await
1360 .map_err(|error| {
1361 RustBuildError::FailToBuildRustLibrary(std::io::Error::other(error.to_string()))
1362 })?;
1363 }
1364 let crate_type_override = if cargo_target.accepts_crate_type_override() {
1365 self.crate_type_override.as_deref()
1366 } else {
1367 None
1368 };
1369 let mut cmd = Command::new("cargo");
1370 let cargo_subcommand = if crate_type_override.is_some() || !self.final_rustc_args.is_empty()
1371 {
1372 "rustc"
1373 } else {
1374 "build"
1375 };
1376 let mut cmd = cmd.arg(cargo_subcommand);
1377 if self.build_std_toolchain.is_some() {
1378 cmd = cmd.arg("-Zbuild-std=std,panic_abort");
1388 cmd =
1389 cmd.arg("-Zbuild-std-features=panic-unwind,backtrace,default,compiler-builtins-c");
1390 }
1391 let mut cmd = cmd
1392 .arg("--message-format=json-render-diagnostics")
1393 .args(cargo_target.cargo_args())
1394 .args(["--target", self.triple.to_string().as_str()])
1395 .current_dir(&self.path);
1396 if framework.is_some() {
1397 cmd = cmd.arg("--locked");
1398 }
1399
1400 if let Some(target_dir) = &self.target_dir {
1401 cmd = cmd.arg("--target-dir").arg(target_dir);
1402 }
1403
1404 for (key, value) in &self.envs {
1406 cmd.env(key, value);
1407 }
1408 let mut cmd = self.with_project_toolchain_env(cmd).await?;
1409
1410 if !self.rustc_flags.is_empty() {
1411 let mut rustflags = std::env::var_os("RUSTFLAGS").unwrap_or_default();
1412 if !rustflags.is_empty() {
1413 rustflags.push(" ");
1414 }
1415 rustflags.push(self.rustc_flags.join(" "));
1416 cmd = cmd.env("RUSTFLAGS", rustflags);
1417 }
1418
1419 configure_generated_crate_compilation(cmd);
1420
1421 if let Some(sccache_path) = &self.sccache_path {
1423 crate::toolchain::sccache::configure_compilation_cache(cmd, sccache_path).map_err(
1424 |error| {
1425 RustBuildError::FailToBuildRustLibrary(std::io::Error::other(error.to_string()))
1426 },
1427 )?;
1428 }
1429
1430 if self.build_std_toolchain.is_some() {
1436 cmd = self.with_build_std_envs(cmd, release).await?;
1437 }
1438
1439 if self.triple.environment == Environment::Sim
1446 && let Some(clang_args) = self.bindgen_clang_args_for_simulator().await
1447 {
1448 let bindgen_target_key = format!(
1449 "BINDGEN_EXTRA_CLANG_ARGS_{}",
1450 self.triple.to_string().replace('-', "_")
1451 );
1452 cmd = cmd.env(bindgen_target_key, clang_args);
1453 }
1454
1455 if release {
1456 cmd = cmd.arg("--release");
1457 }
1458
1459 if !self.features.is_empty() {
1461 cmd = cmd.args(["--features", &self.features.join(",")]);
1462 }
1463
1464 if crate_type_override.is_some() || !self.final_rustc_args.is_empty() {
1465 cmd = cmd.arg("--");
1466 if let Some(crate_type) = crate_type_override {
1467 cmd = cmd.arg("--crate-type").arg(crate_type);
1468 }
1469 cmd = cmd.args(&self.final_rustc_args);
1470 }
1471
1472 if std_output_enabled()
1477 && std::env::var_os("CARGO_TERM_COLOR").is_none()
1478 && !self.envs.iter().any(|(key, _)| key == "CARGO_TERM_COLOR")
1479 {
1480 cmd.env("CARGO_TERM_COLOR", "always");
1481 }
1482
1483 command_output_with_progress(cmd, self.progress.clone())
1484 .await
1485 .map_err(RustBuildError::FailToExecuteCargoBuild)
1486 }
1487
1488 async fn with_project_toolchain_env<'a>(
1494 &self,
1495 cmd: &'a mut Command,
1496 ) -> Result<&'a mut Command, RustBuildError> {
1497 if self.build_std_toolchain.is_some() {
1498 return Ok(cmd);
1499 }
1500 let Some(project) = &self.project else {
1501 return Ok(cmd);
1502 };
1503 let toolchain = project_toolchain(project).await.map_err(|error| {
1504 RustBuildError::FailToBuildRustLibrary(std::io::Error::other(error.to_string()))
1505 })?;
1506 Ok(cmd.env("RUSTUP_TOOLCHAIN", toolchain))
1507 }
1508
1509 async fn with_build_std_envs<'a>(
1513 &self,
1514 cmd: &'a mut Command,
1515 release: bool,
1516 ) -> Result<&'a mut Command, RustBuildError> {
1517 let Some(toolchain) = &self.build_std_toolchain else {
1518 return Ok(cmd);
1519 };
1520 let publish_dir = self.lib_output_dir(release).await?.join("deps");
1521 let cmd = cmd
1522 .env("RUSTUP_TOOLCHAIN", toolchain)
1523 .env(
1524 "RUSTC_WRAPPER",
1525 crate::toolchain::Host::current_exe()
1526 .map_err(RustBuildError::FailToExecuteCargoBuild)?,
1527 )
1528 .env(crate::workflows::rustc_wrapper::WRAPPER_MODE_ENV, "1")
1529 .env(
1530 crate::workflows::rustc_wrapper::BUILD_STD_TARGET_ENV,
1531 self.triple.to_string(),
1532 )
1533 .env(
1534 crate::workflows::rustc_wrapper::BUILD_STD_DYLIB_DIR_ENV,
1535 publish_dir,
1536 );
1537 if let Some(sccache_path) = &self.sccache_path {
1538 cmd.env(
1539 crate::workflows::rustc_wrapper::WRAPPER_CHAIN_ENV,
1540 sccache_path,
1541 );
1542 }
1543 cmd.env_remove("RUSTC_WORKSPACE_WRAPPER");
1549 cmd.env_remove("CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER");
1550 Ok(cmd)
1551 }
1552
1553 pub async fn lib_output_dir(&self, release: bool) -> Result<PathBuf, RustBuildError> {
1558 let target_directory = self.target_directory().await?;
1559 Ok(target_directory
1560 .join(self.triple.to_string())
1561 .join(if release { "release" } else { "debug" }))
1562 }
1563
1564 async fn target_directory(&self) -> Result<PathBuf, RustBuildError> {
1565 if let Some(target_dir) = &self.target_dir {
1566 return Ok(target_dir.clone());
1567 }
1568
1569 let build_path = self.path.clone();
1570 let metadata = unblock(move || {
1571 cargo_metadata::MetadataCommand::new()
1572 .no_deps()
1573 .current_dir(build_path)
1574 .exec()
1575 .map_err(|e| {
1576 RustBuildError::FailToBuildRustLibrary(std::io::Error::new(
1577 std::io::ErrorKind::InvalidData,
1578 e,
1579 ))
1580 })
1581 })
1582 .await?;
1583 Ok(metadata.target_directory.as_std_path().to_path_buf())
1584 }
1585
1586 async fn bindgen_clang_args_for_simulator(&self) -> Option<String> {
1591 let (sdk_name, target_os) = match self.triple.operating_system {
1592 OperatingSystem::IOS(_) => ("iphonesimulator", "ios"),
1593 OperatingSystem::TvOS(_) => ("appletvsimulator", "tvos"),
1594 OperatingSystem::WatchOS(_) => ("watchsimulator", "watchos"),
1595 OperatingSystem::VisionOS(_) => ("xrsimulator", "xros"),
1596 _ => return None,
1597 };
1598
1599 let arch = match self.triple.architecture {
1600 target_lexicon::Architecture::Aarch64(_) => "arm64",
1601 target_lexicon::Architecture::X86_64 => "x86_64",
1602 _ => return None,
1603 };
1604
1605 let sdk_path = run_command("xcrun", ["--sdk", sdk_name, "--show-sdk-path"])
1607 .await
1608 .ok()
1609 .map(|s| s.trim().to_string())?;
1610
1611 let min_version = if matches!(target_os, "ios" | "tvos") {
1613 "17.0"
1614 } else if target_os == "watchos" {
1615 "10.0"
1616 } else {
1617 debug_assert_eq!(
1618 target_os, "xros",
1619 "bindgen simulator target_os must be one of ios/tvos/watchos/xros"
1620 );
1621 "1.0"
1622 };
1623
1624 Some(format!(
1625 "--target={arch}-apple-{target_os}{min_version}-simulator -isysroot {sdk_path}"
1626 ))
1627 }
1628}
1629
1630fn crate_type_artifact_extension(crate_type: &str, triple: &Triple) -> Option<&'static str> {
1633 match crate_type {
1634 "lib" | "rlib" => Some("rlib"),
1635 "staticlib" => Some(if matches!(triple.environment, Environment::Msvc) {
1636 "lib"
1637 } else {
1638 "a"
1639 }),
1640 "cdylib" | "dylib" | "proc-macro" => Some(lib_extension_for_triple(triple)),
1641 _ => None,
1642 }
1643}
1644
1645pub(crate) fn reported_artifact(
1660 stdout: &[u8],
1661 crate_dir: &Path,
1662 cargo_target: CargoTarget<'_>,
1663 artifact_extension: Option<&'static str>,
1664) -> Result<PathBuf, RustBuildError> {
1665 let manifest_path = dunce::canonicalize(crate_dir.join("Cargo.toml")).map_err(|error| {
1666 RustBuildError::FailToBuildRustLibrary(io::Error::other(format!(
1667 "failed to canonicalize {}: {error}",
1668 crate_dir.join("Cargo.toml").display()
1669 )))
1670 })?;
1671 let mut artifacts = Vec::new();
1672 for artifact in compiler_artifacts(stdout)? {
1673 if cargo_target.matches(&artifact.target)
1674 && same_manifest_path(artifact.manifest_path.as_std_path(), &manifest_path)
1675 {
1676 artifacts.push(artifact);
1677 }
1678 }
1679 reported_artifact_file(&artifacts, cargo_target, artifact_extension, &manifest_path)
1680}
1681
1682pub(crate) fn compiler_artifacts(
1691 stdout: &[u8],
1692) -> Result<Vec<cargo_metadata::Artifact>, RustBuildError> {
1693 #[derive(serde::Deserialize)]
1695 struct Reason {
1696 reason: String,
1697 }
1698
1699 let mut artifacts = Vec::new();
1700 for (index, line) in stdout.split(|byte| *byte == b'\n').enumerate() {
1701 let Ok(line) = str::from_utf8(line) else {
1702 continue;
1703 };
1704 let line = line.trim_end();
1705 if line.is_empty() {
1706 continue;
1707 }
1708 let malformed = |error: serde_json::Error| {
1709 RustBuildError::FailToBuildRustLibrary(io::Error::new(
1710 io::ErrorKind::InvalidData,
1711 format!(
1712 "cargo emitted a malformed `compiler-artifact` message on line {}: {error}\n{line}",
1713 index + 1
1714 ),
1715 ))
1716 };
1717 match serde_json::from_str::<Reason>(line) {
1718 Ok(Reason { reason }) if reason == "compiler-artifact" => {
1719 let artifact =
1720 serde_json::from_str::<cargo_metadata::Artifact>(line).map_err(malformed)?;
1721 artifacts.push(artifact);
1722 }
1723 Err(error) if line.contains("\"reason\":\"compiler-artifact\"") => {
1727 return Err(malformed(error));
1728 }
1729 Ok(_) | Err(_) => {}
1730 }
1731 }
1732 Ok(artifacts)
1733}
1734
1735pub(crate) fn same_manifest_path(reported: &Path, expected: &Path) -> bool {
1741 reported == expected
1742 || dunce::canonicalize(reported).is_ok_and(|canonical| canonical == expected)
1743}
1744
1745fn reported_artifact_file(
1748 artifacts: &[cargo_metadata::Artifact],
1749 cargo_target: CargoTarget<'_>,
1750 artifact_extension: Option<&'static str>,
1751 manifest_path: &Path,
1752) -> Result<PathBuf, RustBuildError> {
1753 let what = || -> String {
1754 match cargo_target {
1755 CargoTarget::Lib => format!("the library target of {}", manifest_path.display()),
1756 CargoTarget::Binary(name) => {
1757 format!("binary `{name}` of {}", manifest_path.display())
1758 }
1759 }
1760 };
1761 let not_found = |detail: String| {
1762 RustBuildError::FailToBuildRustLibrary(io::Error::new(io::ErrorKind::NotFound, detail))
1763 };
1764
1765 let files: Vec<PathBuf> = artifacts
1766 .iter()
1767 .flat_map(|artifact| {
1768 artifact
1769 .filenames
1770 .iter()
1771 .map(|file| file.as_std_path().to_path_buf())
1772 })
1773 .collect();
1774 let artifact = match cargo_target {
1775 CargoTarget::Binary(_) => artifacts
1776 .iter()
1777 .find_map(|artifact| artifact.executable.as_ref())
1778 .map(|path| path.as_std_path().to_path_buf())
1779 .ok_or_else(|| {
1780 not_found(format!(
1781 "Cargo reported no artifact for {} (reported files: {files:?})",
1782 what()
1783 ))
1784 })?,
1785 CargoTarget::Lib => {
1786 let matching: Vec<&PathBuf> = artifact_extension.map_or_else(
1787 || files.iter().collect(),
1788 |extension| {
1789 files
1790 .iter()
1791 .filter(|file| file.extension().is_some_and(|e| *e == *extension))
1792 .collect()
1793 },
1794 );
1795 match matching.as_slice() {
1796 [only] => (*only).clone(),
1797 _ => {
1798 return Err(not_found(artifact_extension.map_or_else(
1799 || {
1800 format!(
1801 "Cargo reported {} artifacts for {} — select one with a crate-type override (reported files: {files:?})",
1802 matching.len(),
1803 what()
1804 )
1805 },
1806 |extension| {
1807 format!(
1808 "Cargo reported no `.{extension}` artifact for {} (reported files: {files:?})",
1809 what()
1810 )
1811 },
1812 )));
1813 }
1814 }
1815 }
1816 };
1817 if !artifact.is_file() {
1818 return Err(not_found(format!(
1819 "Cargo reported {} for {} but the file does not exist",
1820 artifact.display(),
1821 what()
1822 )));
1823 }
1824 Ok(artifact)
1825}
1826
1827async fn stale_shared_dylib_packages(stdout: &[u8]) -> Result<Vec<String>, RustBuildError> {
1837 let mut stale = Vec::new();
1838 for artifact in compiler_artifacts(stdout)? {
1839 if !artifact.fresh {
1840 continue;
1841 }
1842 let Some(manifest_dir) = artifact.manifest_path.as_std_path().parent() else {
1843 continue;
1844 };
1845 if !uplifts_dynamic_library(&artifact.target) {
1851 continue;
1852 }
1853 let manifest_root = dunce::simplified(manifest_dir);
1854 let mut package_stale = false;
1855 for filename in &artifact.filenames {
1856 let file = filename.as_std_path();
1857 if !is_dynamic_library(file) {
1858 continue;
1859 }
1860 let Some(dep_info) = dep_info_path(file, &artifact.filenames) else {
1861 return Err(RustBuildError::FailToBuildRustLibrary(io::Error::new(
1862 io::ErrorKind::NotFound,
1863 format!(
1864 "Cargo reported {} fresh but no dep-info was found beside it or in its unit directory (reported files: {:?})",
1865 file.display(),
1866 artifact.filenames
1867 ),
1868 )));
1869 };
1870 let contents = smol::fs::read_to_string(&dep_info).await.map_err(|error| {
1871 RustBuildError::FailToBuildRustLibrary(io::Error::other(format!(
1872 "Cargo reported {} fresh but its dep-info {} is unreadable: {error}",
1873 file.display(),
1874 dep_info.display()
1875 )))
1876 })?;
1877 if !dep_info_prerequisites(&contents).iter().any(|source| {
1881 let source = if source.is_absolute() {
1882 source.clone()
1883 } else {
1884 manifest_dir.join(source)
1885 };
1886 dunce::simplified(&source).starts_with(manifest_root)
1887 }) {
1888 package_stale = true;
1889 }
1890 }
1891 if package_stale {
1892 stale.push(artifact_package_name(&artifact.package_id).to_owned());
1893 }
1894 }
1895 stale.sort_unstable();
1896 stale.dedup();
1897 Ok(stale)
1898}
1899
1900fn is_dynamic_library(file: &Path) -> bool {
1903 file.extension()
1904 .is_some_and(|extension| matches!(extension.to_str(), Some("so" | "dylib" | "dll")))
1905}
1906
1907fn uplifts_dynamic_library(target: &cargo_metadata::Target) -> bool {
1911 target.crate_types.iter().any(|kind| {
1912 matches!(
1913 kind,
1914 cargo_metadata::CrateType::DyLib | cargo_metadata::CrateType::CDyLib
1915 )
1916 })
1917}
1918
1919fn dep_info_path(
1935 artifact_file: &Path,
1936 sibling_files: &[cargo_metadata::camino::Utf8PathBuf],
1937) -> Option<PathBuf> {
1938 let file_stem = artifact_file.file_stem()?.to_str()?;
1939 let name = file_stem.strip_prefix("lib").unwrap_or(file_stem);
1940 let dir = artifact_file.parent()?;
1941 let mut candidates = vec![
1946 dir.join(format!("{file_stem}.d")),
1947 dir.join("deps").join(format!("{name}.d")),
1948 ];
1949 candidates.extend(
1950 sibling_files
1951 .iter()
1952 .filter_map(|sibling| sibling.as_std_path().parent())
1953 .filter(|unit_dir| *unit_dir != dir)
1954 .map(|unit_dir| unit_dir.join(format!("{name}.d"))),
1955 );
1956 candidates.push(dir.join(format!("{name}.d")));
1957 candidates.into_iter().find(|candidate| candidate.is_file())
1958}
1959
1960fn dep_info_prerequisites(contents: &str) -> Vec<PathBuf> {
1978 let mut joined = String::with_capacity(contents.len());
1981 for line in contents.lines() {
1982 if let Some(head) = line.strip_suffix('\\') {
1983 joined.push_str(head);
1984 joined.push(' ');
1985 } else {
1986 joined.push_str(line);
1987 joined.push('\n');
1988 }
1989 }
1990 let mut prerequisites = Vec::new();
1991 for line in joined.lines() {
1992 let Some((_, rest)) = line.split_once(": ") else {
1993 continue;
1994 };
1995 let mut token = String::new();
1996 let mut chars = rest.chars().peekable();
1997 while let Some(c) = chars.next() {
1998 match c {
1999 '\\' if chars.peek() == Some(&' ') => {
2000 chars.next();
2001 token.push(' ');
2002 }
2003 c if c.is_whitespace() => {
2004 if !token.is_empty() {
2005 prerequisites.push(PathBuf::from(std::mem::take(&mut token)));
2006 }
2007 }
2008 c => token.push(c),
2009 }
2010 }
2011 if !token.is_empty() {
2012 prerequisites.push(PathBuf::from(token));
2013 }
2014 }
2015 prerequisites
2016}
2017
2018fn artifact_package_name(package_id: &cargo_metadata::PackageId) -> &str {
2021 let repr = package_id.repr.as_str();
2022 let (source, fragment) = repr.rsplit_once('#').unwrap_or((repr, ""));
2023 fragment.split_once('@').map_or_else(
2024 || source.rsplit('/').next().unwrap_or(repr),
2025 |(name, _)| name,
2026 )
2027}
2028
2029async fn clean_cargo_package(
2033 crate_dir: &Path,
2034 package: &str,
2035 target_dir: &Path,
2036) -> Result<(), RustBuildError> {
2037 let mut command = Command::new("cargo");
2038 command
2039 .arg("clean")
2040 .arg("-p")
2041 .arg(package)
2042 .arg("--target-dir")
2043 .arg(target_dir)
2044 .current_dir(crate_dir);
2045 configure_generated_crate_compilation(&mut command);
2046 let output = command
2047 .output()
2048 .await
2049 .map_err(RustBuildError::FailToExecuteCargoBuild)?;
2050 if !output.status.success() {
2051 return Err(RustBuildError::FailToBuildRustLibrary(io::Error::other(
2052 format!(
2053 "cargo clean -p {package} failed:\n{}",
2054 String::from_utf8_lossy(&output.stderr)
2055 ),
2056 )));
2057 }
2058 Ok(())
2059}
2060
2061fn combined_build_output(output: &std::process::Output) -> String {
2062 let stderr = String::from_utf8_lossy(&output.stderr);
2063 let stdout = String::from_utf8_lossy(&output.stdout);
2064 if stderr.is_empty() {
2065 stdout.to_string()
2066 } else {
2067 stderr.to_string()
2068 }
2069}
2070
2071const FAILURE_TAIL_LINES: usize = 40;
2074
2075pub(crate) fn output_tail(text: &str) -> String {
2078 let lines: Vec<&str> = text.lines().collect();
2079 if lines.len() <= FAILURE_TAIL_LINES {
2080 return text.to_owned();
2081 }
2082 format!(
2083 "… {} earlier lines already streamed above …\n{}",
2084 lines.len() - FAILURE_TAIL_LINES,
2085 lines[lines.len() - FAILURE_TAIL_LINES..].join("\n")
2086 )
2087}
2088
2089fn should_auto_install_meson(build_output: &str) -> bool {
2090 let lower = build_output.to_ascii_lowercase();
2091 lower.contains("meson")
2092 && (lower.contains("not found")
2093 || lower.contains("no such file")
2094 || lower.contains("failed to execute")
2095 || lower.contains("is required"))
2096}
2097
2098fn should_retry_after_cmake_generator_mismatch(build_output: &str) -> bool {
2099 let lower = build_output.to_ascii_lowercase();
2100 lower.contains("cmake error") && lower.contains("does not match the generator used previously")
2101}
2102
2103fn remove_cmake_build_dirs_in(build_root: &Path) -> std::io::Result<usize> {
2104 if !build_root.exists() {
2105 return Ok(0);
2106 }
2107
2108 let mut removed = 0usize;
2109 for entry in std::fs::read_dir(build_root)? {
2110 let entry = entry?;
2111 let path = entry.path();
2112 if !path.is_dir() {
2113 continue;
2114 }
2115
2116 let cmake_build_dir = path.join("out").join("build");
2117 if cmake_build_dir.join("CMakeCache.txt").exists() {
2118 std::fs::remove_dir_all(cmake_build_dir)?;
2119 removed += 1;
2120 }
2121 }
2122
2123 Ok(removed)
2124}
2125
2126#[cfg(target_os = "macos")]
2127async fn ensure_meson_installed_for_build() -> Result<(), String> {
2128 use crate::toolchain::meson::Meson;
2129 use crate::toolchain::{Installation as _, Toolchain as _, ToolchainError};
2130
2131 let host = crate::toolchain::Host::current();
2132 match Meson.check(&host).await {
2133 Ok(()) => Ok(()),
2134 Err(ToolchainError::Fixable(installation)) => {
2135 installation.install(&host).await.map_err(|e| e.to_string())
2136 }
2137 Err(ToolchainError::Unfixable(e)) => Err(e.to_string()),
2138 }
2139}
2140
2141#[cfg(not(target_os = "macos"))]
2142fn ensure_meson_installed_for_build() -> impl std::future::Future<Output = Result<(), String>> {
2143 std::future::ready(Err(
2144 "automatic meson installation is only supported on macOS".to_string(),
2145 ))
2146}
2147
2148#[cfg(test)]
2149mod tests {
2150 use target_lexicon::Triple;
2151 use tempfile::tempdir;
2152
2153 use std::ffi::OsString;
2154 use std::path::PathBuf;
2155
2156 use super::{
2157 BuildOptions, BuildProfile, CargoTarget, CompileEvent, RustBuild, RustDynamicLibraries,
2158 RustLinkage, classify_compile_line, dynamic_library_file_name, lib_extension_for_triple,
2159 resolve_rust_standard_library_in,
2160 };
2161
2162 fn triple(value: &str) -> Triple {
2163 value.parse().expect("test target triple must parse")
2164 }
2165
2166 #[test]
2167 fn crate_type_override_applies_only_to_library_targets() {
2168 assert!(CargoTarget::Lib.accepts_crate_type_override());
2169 assert!(!CargoTarget::Binary("waterui-cef-helper").accepts_crate_type_override());
2170 assert_eq!(CargoTarget::Lib.cargo_args(), ["--lib"]);
2171 assert_eq!(
2172 CargoTarget::Binary("waterui-cef-helper").cargo_args(),
2173 ["--bin", "waterui-cef-helper"]
2174 );
2175 }
2176
2177 #[test]
2178 fn build_std_envs_wire_the_wrapper_and_clear_workspace_wrappers() {
2179 use std::ffi::OsStr;
2180
2181 let dir = tempdir().expect("target dir");
2182 let toolchain = "nightly-2026-09-09-aarch64-apple-darwin";
2183 let target_dir = dir.path().join("target");
2184 let build = RustBuild::new(dir.path(), triple("aarch64-linux-android"))
2185 .with_build_std(toolchain)
2186 .with_target_dir(target_dir.clone())
2187 .with_sccache(std::path::PathBuf::from("/fake/sccache"));
2188 let mut cmd = smol::process::Command::new("cargo");
2189 smol::block_on(build.with_build_std_envs(&mut cmd, false)).expect("build-std envs apply");
2190
2191 let env = |key: &str| -> Option<Option<OsString>> {
2192 cmd.get_envs()
2193 .find(|(name, _)| *name == OsStr::new(key))
2194 .map(|(_, value)| value.map(ToOwned::to_owned))
2195 };
2196 assert_eq!(
2197 env("RUSTUP_TOOLCHAIN"),
2198 Some(Some(OsString::from(toolchain)))
2199 );
2200 assert_eq!(
2201 env("RUSTC_WRAPPER"),
2202 Some(Some(
2203 crate::toolchain::Host::current_exe()
2204 .expect("the test binary path")
2205 .into_os_string()
2206 )),
2207 "the wrapper must name this binary"
2208 );
2209 assert_eq!(
2210 env(crate::workflows::rustc_wrapper::WRAPPER_MODE_ENV),
2211 Some(Some(OsString::from("1")))
2212 );
2213 assert_eq!(
2214 env(crate::workflows::rustc_wrapper::BUILD_STD_TARGET_ENV),
2215 Some(Some(OsString::from("aarch64-linux-android")))
2216 );
2217 let expected_dylib_dir = target_dir
2218 .join("aarch64-linux-android")
2219 .join("debug")
2220 .join("deps");
2221 assert_eq!(
2222 env(crate::workflows::rustc_wrapper::BUILD_STD_DYLIB_DIR_ENV),
2223 Some(Some(expected_dylib_dir.into_os_string()))
2224 );
2225 assert_eq!(
2226 env(crate::workflows::rustc_wrapper::WRAPPER_CHAIN_ENV),
2227 Some(Some(OsString::from("/fake/sccache"))),
2228 "a configured sccache chains behind the shim"
2229 );
2230 assert_eq!(env("RUSTC_WORKSPACE_WRAPPER"), Some(None));
2233 assert_eq!(env("CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER"), Some(None));
2234 }
2235
2236 #[test]
2237 fn apple_platform_dylibs_use_macho_extension() {
2238 assert_eq!(
2239 lib_extension_for_triple(&triple("aarch64-apple-darwin")),
2240 "dylib"
2241 );
2242 assert_eq!(
2243 lib_extension_for_triple(&triple("aarch64-apple-ios-sim")),
2244 "dylib"
2245 );
2246 assert_eq!(
2247 lib_extension_for_triple(&triple("aarch64-apple-ios")),
2248 "dylib"
2249 );
2250 }
2251
2252 #[test]
2253 fn non_apple_platform_dylibs_keep_platform_extensions() {
2254 assert_eq!(
2255 lib_extension_for_triple(&triple("aarch64-linux-android")),
2256 "so"
2257 );
2258 assert_eq!(
2259 lib_extension_for_triple(&triple("x86_64-unknown-linux-gnu")),
2260 "so"
2261 );
2262 assert_eq!(
2263 lib_extension_for_triple(&triple("x86_64-pc-windows-msvc")),
2264 "dll"
2265 );
2266 }
2267
2268 #[test]
2269 fn development_and_packaging_have_distinct_linkage() {
2270 assert_eq!(
2271 BuildOptions::development(BuildProfile::Debug).linkage(),
2272 RustLinkage::SharedRuntime
2273 );
2274 assert_eq!(
2275 BuildOptions::packaging(BuildProfile::Debug).linkage(),
2276 RustLinkage::Static
2277 );
2278 assert!(BuildOptions::development(BuildProfile::Release).is_release());
2279 assert!(BuildOptions::packaging(BuildProfile::Release).is_release());
2280 }
2281
2282 #[test]
2283 fn build_profile_release_variants_select_the_release_profile() {
2284 assert!(BuildProfile::Release.is_release());
2285 assert!(BuildProfile::Profiling.is_release());
2286 assert!(!BuildProfile::Debug.is_release());
2287 assert!(!BuildProfile::Optimized.is_release());
2288 }
2289
2290 #[test]
2291 fn development_profile_envs_realize_the_selected_trade_off() {
2292 let optimized = BuildOptions::development(BuildProfile::Optimized);
2293 let envs = optimized.cargo_envs();
2294 assert!(
2295 envs.contains(&(
2296 "CARGO_PROFILE_DEV_OPT_LEVEL".to_string(),
2297 OsString::from("1")
2298 )),
2299 "optimized development lifts the dev opt-level: {envs:?}"
2300 );
2301 assert!(
2302 envs.contains(&(
2303 "CARGO_PROFILE_DEV_DEBUG_ASSERTIONS".to_string(),
2304 OsString::from("false")
2305 )),
2306 "optimized development drops dep debug assertions: {envs:?}"
2307 );
2308 assert!(
2309 envs.contains(&(
2310 "CARGO_PROFILE_DEV_DEBUG".to_string(),
2311 OsString::from("true")
2312 )),
2313 "optimized development keeps full debug info: {envs:?}"
2314 );
2315
2316 let shared_runtime_envs = [
2317 (
2318 "CARGO_PROFILE_RELEASE_PANIC".to_string(),
2319 OsString::from("unwind"),
2320 ),
2321 (
2322 "CARGO_PROFILE_RELEASE_LTO".to_string(),
2323 OsString::from("off"),
2324 ),
2325 ];
2326 for env in &shared_runtime_envs {
2327 assert!(
2328 BuildOptions::development(BuildProfile::Release)
2329 .cargo_envs()
2330 .contains(env),
2331 "a release development build links the shared runtime: missing {env:?}"
2332 );
2333 assert!(
2334 !BuildOptions::development(BuildProfile::Release)
2335 .with_static_runtime()
2336 .cargo_envs()
2337 .contains(env),
2338 "a static runtime keeps the manifest's {env:?}"
2339 );
2340 }
2341 let unwind = &shared_runtime_envs[0];
2342
2343 let profiling = BuildOptions::development(BuildProfile::Profiling);
2344 let envs = profiling.cargo_envs();
2345 assert!(
2346 envs.contains(unwind),
2347 "profiling links the shared runtime too"
2348 );
2349 for key in [
2350 "CARGO_PROFILE_RELEASE_OPT_LEVEL",
2351 "CARGO_PROFILE_RELEASE_DEBUG",
2352 "CARGO_PROFILE_RELEASE_STRIP",
2353 ] {
2354 assert!(
2355 envs.iter().any(|(env_key, _)| env_key == key),
2356 "profiling keeps debug info and symbols: missing {key} in {envs:?}"
2357 );
2358 }
2359
2360 assert!(
2361 BuildOptions::development(BuildProfile::Debug)
2362 .cargo_envs()
2363 .is_empty(),
2364 "plain debug runs the declared dev profile"
2365 );
2366 }
2367
2368 #[test]
2369 fn packaging_never_overrides_the_declared_profile() {
2370 for profile in [
2371 BuildProfile::Debug,
2372 BuildProfile::Optimized,
2373 BuildProfile::Release,
2374 BuildProfile::Profiling,
2375 ] {
2376 assert!(
2377 BuildOptions::packaging(profile).cargo_envs().is_empty(),
2378 "packaging {profile:?} must ship the declared profile"
2379 );
2380 }
2381 }
2382
2383 #[test]
2384 fn resolves_target_standard_library_without_guessing_hash() {
2385 let directory = tempdir().expect("temporary target libdir");
2386 let android_triple = triple("aarch64-linux-android");
2387 let expected = directory.path().join("libstd-1234567890abcdef.so");
2388 std::fs::write(&expected, []).expect("write test std library");
2389 std::fs::write(directory.path().join("libcore.rlib"), []).expect("write unrelated library");
2390
2391 assert_eq!(
2392 resolve_rust_standard_library_in(directory.path(), &android_triple)
2393 .expect("resolve dynamic std"),
2394 expected
2395 );
2396 assert_eq!(
2397 dynamic_library_file_name("waterui_dylib", &android_triple),
2398 "libwaterui_dylib.so"
2399 );
2400 assert_eq!(
2401 dynamic_library_file_name("waterui_dylib", &triple("x86_64-pc-windows-msvc")),
2402 "waterui_dylib.dll"
2403 );
2404 }
2405
2406 #[test]
2407 fn compile_progress_classifies_cargo_unit_lines() {
2408 assert_eq!(
2409 classify_compile_line(" Compiling serde v1.0.228"),
2410 CompileEvent::Unit {
2411 phase: "Compiling",
2412 name: "serde".to_string(),
2413 version: Some("1.0.228".to_string()),
2414 }
2415 );
2416 assert_eq!(
2417 classify_compile_line(" Compiling waterui-app v0.1.0 (/tmp/app)"),
2418 CompileEvent::Unit {
2419 phase: "Compiling",
2420 name: "waterui-app".to_string(),
2421 version: Some("0.1.0".to_string()),
2422 }
2423 );
2424 assert_eq!(
2425 classify_compile_line(" Checking libc v0.2.171"),
2426 CompileEvent::Unit {
2427 phase: "Checking",
2428 name: "libc".to_string(),
2429 version: Some("0.2.171".to_string()),
2430 }
2431 );
2432 }
2433
2434 #[test]
2435 fn compile_progress_keeps_non_unit_lines_verbatim() {
2436 assert_eq!(
2437 classify_compile_line(" Compiling 12 crates"),
2438 CompileEvent::Line("Compiling 12 crates".to_string())
2439 );
2440 assert_eq!(
2441 classify_compile_line(" Downloaded 300 crates (5.2 MB) in 1.23s"),
2442 CompileEvent::Line("Downloaded 300 crates (5.2 MB) in 1.23s".to_string())
2443 );
2444 assert_eq!(
2445 classify_compile_line(
2446 " Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.23s"
2447 ),
2448 CompileEvent::Finished(
2449 "Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.23s".to_string()
2450 )
2451 );
2452 assert_eq!(
2453 classify_compile_line("warning: unused import"),
2454 CompileEvent::Line("warning: unused import".to_string())
2455 );
2456 }
2457
2458 #[test]
2459 fn compile_progress_classifies_through_ansi_color() {
2460 let colored = "\u{1b}[0m\u{1b}[1m\u{1b}[32m Compiling\u{1b}[0m serde v1.0.228";
2463 assert_eq!(
2464 classify_compile_line(colored),
2465 CompileEvent::Unit {
2466 phase: "Compiling",
2467 name: "serde".to_string(),
2468 version: Some("1.0.228".to_string()),
2469 }
2470 );
2471 let colored_finished =
2472 "\u{1b}[0m\u{1b}[1m\u{1b}[32m Finished\u{1b}[0m `dev` profile in 1.23s";
2473 assert_eq!(
2474 classify_compile_line(colored_finished),
2475 CompileEvent::Finished(colored_finished.trim().to_string())
2476 );
2477 }
2478
2479 #[test]
2485 fn same_named_projects_resolve_their_own_artifacts_in_one_shared_target() {
2486 use crate::project_model::project_types::{CrateName, generated_crate_name};
2487
2488 smol::block_on(async {
2489 let temporary = tempdir().expect("tempdir");
2490 let shared_target = temporary.path().join("shared-target");
2491 let demo = CrateName::try_from("demo").expect("crate name");
2492 let mut artifacts = Vec::new();
2493 for (directory, marker) in [("first", "first"), ("second", "second")] {
2494 let project_root = temporary.path().join(directory);
2495 let crate_dir = project_root.join("hydrolysis");
2496 std::fs::create_dir_all(crate_dir.join("src")).expect("crate dir");
2497 let package = generated_crate_name(&demo, "hydrolysis", &project_root);
2498 std::fs::write(
2499 crate_dir.join("Cargo.toml"),
2500 format!(
2501 "[package]\nname = \"{package}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n"
2502 ),
2503 )
2504 .expect("manifest");
2505 std::fs::write(
2506 crate_dir.join("src/main.rs"),
2507 format!("fn main() {{ println!(\"{marker}\"); }}\n"),
2508 )
2509 .expect("main.rs");
2510
2511 let artifact = super::RustBuild::new(&crate_dir, Triple::host())
2512 .with_target_dir(&shared_target)
2513 .build_binary(package.as_str(), false)
2514 .await
2515 .expect("the generated crate builds");
2516 assert!(artifact.is_file(), "the reported artifact exists");
2517 artifacts.push(artifact);
2518 }
2519
2520 assert_ne!(
2521 artifacts[0], artifacts[1],
2522 "each same-named project resolves its own artifact"
2523 );
2524 for (artifact, marker) in artifacts.iter().zip(["first", "second"]) {
2525 let ran = std::process::Command::new(artifact)
2526 .output()
2527 .expect("the resolved artifact executes");
2528 assert_eq!(
2529 String::from_utf8_lossy(&ran.stdout).trim(),
2530 marker,
2531 "the artifact is this project's binary, not the sibling's"
2532 );
2533 }
2534 });
2535 }
2536
2537 #[test]
2542 fn reported_artifact_selects_the_matching_manifests_file() {
2543 let temporary = tempdir().expect("tempdir");
2544 let crate_dir = temporary.path().join("demo-hydrolysis-deadbeef");
2545 std::fs::create_dir_all(&crate_dir).expect("crate dir");
2546 std::fs::write(crate_dir.join("Cargo.toml"), "[package]\n").expect("manifest");
2547 let manifest =
2548 dunce::canonicalize(crate_dir.join("Cargo.toml")).expect("canonical manifest");
2549 let reported = crate_dir.join("target/debug/deps/demo_hydrolysis_deadbeef-abc123.rlib");
2550 std::fs::create_dir_all(reported.parent().expect("deps dir")).expect("deps dir");
2551 std::fs::write(&reported, []).expect("reported artifact");
2552
2553 let artifact_json = |manifest: &std::path::Path, file: &std::path::Path, name: &str| {
2557 serde_json::json!({
2558 "reason": "compiler-artifact",
2559 "package_id": format!("path+file:///x#{name}@0.1.0"),
2560 "manifest_path": manifest,
2561 "target": {
2562 "kind": ["lib"],
2563 "crate_types": ["lib"],
2564 "name": name,
2565 "src_path": manifest.parent().expect("manifest dir").join("src/lib.rs"),
2566 "edition": "2021",
2567 "doc": true,
2568 "doctest": true,
2569 "test": true,
2570 },
2571 "profile": {
2572 "opt_level": "0",
2573 "debuginfo": 0,
2574 "debug_assertions": true,
2575 "overflow_checks": true,
2576 "test": false,
2577 },
2578 "features": [],
2579 "filenames": [file],
2580 "executable": null,
2581 "fresh": true,
2582 })
2583 .to_string()
2584 };
2585
2586 let other_manifest = temporary.path().join("other").join("Cargo.toml");
2587 let other_file = temporary.path().join("other.rlib");
2588 let stdout = format!(
2589 "{}\n{}\n",
2590 artifact_json(&other_manifest, &other_file, "other"),
2591 artifact_json(&manifest, &reported, "demo_hydrolysis_deadbeef"),
2592 );
2593 let resolved = super::reported_artifact(
2594 stdout.as_bytes(),
2595 &crate_dir,
2596 CargoTarget::Lib,
2597 Some("rlib"),
2598 )
2599 .expect("the matching manifest's artifact resolves");
2600 assert_eq!(resolved, reported);
2601
2602 let foreign_only = artifact_json(&other_manifest, &other_file, "other");
2603 assert!(
2604 super::reported_artifact(
2605 foreign_only.as_bytes(),
2606 &crate_dir,
2607 CargoTarget::Lib,
2608 Some("rlib"),
2609 )
2610 .is_err(),
2611 "an artifact for another manifest is never selected"
2612 );
2613 }
2614
2615 #[test]
2620 fn stale_shared_dylib_packages_flags_a_foreign_written_artifact() {
2621 smol::block_on(async {
2622 let temporary = tempdir().expect("tempdir");
2623 let deps = temporary.path().join("debug/deps");
2624 std::fs::create_dir_all(&deps).expect("deps dir");
2625 let dylib = deps.join("libwaterui_dylib.so");
2626 std::fs::write(&dylib, []).expect("dylib");
2627
2628 let ours = temporary.path().join("our project");
2632 std::fs::create_dir_all(ours.join("src")).expect("our manifest dir");
2633 let manifest = ours.join("Cargo.toml");
2634 std::fs::write(&manifest, "").expect("manifest");
2635 let own_source = ours.join("src/lib.rs");
2636 std::fs::write(&own_source, "").expect("own source");
2637
2638 let artifact = |fresh: bool| {
2639 serde_json::json!({
2640 "reason": "compiler-artifact",
2641 "package_id": "path+file:///x#waterui-dylib@0.1.0",
2642 "manifest_path": manifest,
2643 "target": {
2644 "kind": ["lib"],
2645 "crate_types": ["dylib"],
2646 "name": "waterui_dylib",
2647 "src_path": own_source,
2648 "edition": "2021",
2649 "doc": true,
2650 "doctest": true,
2651 "test": true,
2652 },
2653 "profile": {
2654 "opt_level": "0",
2655 "debuginfo": 0,
2656 "debug_assertions": true,
2657 "overflow_checks": true,
2658 "test": false,
2659 },
2660 "features": [],
2661 "filenames": [dylib],
2662 "executable": null,
2663 "fresh": fresh,
2664 })
2665 .to_string()
2666 };
2667 let dep_info = deps.join("waterui_dylib.d");
2668
2669 let foreign = temporary.path().join("foreign");
2673 std::fs::create_dir_all(foreign.join("src")).expect("foreign source dir");
2674 let foreign_source = foreign.join("src/lib.rs");
2675 std::fs::write(&foreign_source, "").expect("foreign source");
2676 let dep_escape =
2677 |path: &std::path::Path| path.display().to_string().replace(' ', "\\ ");
2678 let write_dep_info = |source: &std::path::Path| {
2679 std::fs::write(
2680 &dep_info,
2681 format!("{}: {}\n", dep_escape(&dylib), dep_escape(source)),
2682 )
2683 .expect("dep-info");
2684 };
2685
2686 write_dep_info(&foreign_source);
2688 let stale = super::stale_shared_dylib_packages(artifact(true).as_bytes())
2689 .await
2690 .expect("scan");
2691 assert_eq!(stale, ["waterui-dylib"]);
2692
2693 write_dep_info(&own_source);
2695 let stale = super::stale_shared_dylib_packages(artifact(true).as_bytes())
2696 .await
2697 .expect("scan");
2698 assert!(stale.is_empty(), "our own artifact is never stale");
2699
2700 write_dep_info(&foreign_source);
2702 let stale = super::stale_shared_dylib_packages(artifact(false).as_bytes())
2703 .await
2704 .expect("scan");
2705 assert!(stale.is_empty(), "a non-fresh unit wrote the file itself");
2706 });
2707 }
2708
2709 #[test]
2715 fn stale_check_reads_build_dir_dep_info_and_skips_proc_macros() {
2716 smol::block_on(async {
2717 let temporary = tempdir().expect("tempdir");
2718 let profile = temporary.path().join("debug");
2719 let unit_dir = profile.join("build/waterui-dylib/0123456789abcdef/out");
2720 std::fs::create_dir_all(&unit_dir).expect("unit dir");
2721 let dylib = profile.join("libwaterui_dylib.so");
2722 std::fs::write(&dylib, []).expect("dylib");
2723 let rmeta = unit_dir.join("libwaterui_dylib.rmeta");
2724 std::fs::write(&rmeta, []).expect("rmeta");
2725
2726 let ours = temporary.path().join("ours");
2727 std::fs::create_dir_all(ours.join("src")).expect("our manifest dir");
2728 let manifest = ours.join("Cargo.toml");
2729 std::fs::write(&manifest, "").expect("manifest");
2730 let foreign = temporary.path().join("foreign/src/lib.rs");
2731 std::fs::create_dir_all(foreign.parent().expect("parent")).expect("foreign dir");
2732 std::fs::write(&foreign, []).expect("foreign source");
2733 std::fs::write(
2734 unit_dir.join("waterui_dylib.d"),
2735 format!("{}: {}\n", dylib.display(), foreign.display()),
2736 )
2737 .expect("dep-info");
2738
2739 let unit = |name: &str, crate_type: &str, filenames: Vec<&std::path::Path>| {
2740 serde_json::json!({
2741 "reason": "compiler-artifact",
2742 "package_id": format!("path+file:///x#{name}@0.1.0"),
2743 "manifest_path": manifest,
2744 "target": {
2745 "kind": [if crate_type == "proc-macro" { "proc-macro" } else { "lib" }],
2746 "crate_types": [crate_type],
2747 "name": name.replace('-', "_"),
2748 "src_path": ours.join("src/lib.rs"),
2749 "edition": "2021",
2750 "doc": true,
2751 "doctest": true,
2752 "test": true,
2753 },
2754 "profile": {
2755 "opt_level": "0",
2756 "debuginfo": 0,
2757 "debug_assertions": true,
2758 "overflow_checks": true,
2759 "test": false,
2760 },
2761 "features": [],
2762 "filenames": filenames,
2763 "executable": null,
2764 "fresh": true,
2765 })
2766 .to_string()
2767 };
2768 let macro_dylib = unit_dir.join("libthiserror_impl-0123456789abcdef.so");
2772 let stdout = format!(
2773 "{}\n{}\n",
2774 unit("thiserror-impl", "proc-macro", vec![¯o_dylib]),
2775 unit("waterui-dylib", "dylib", vec![&dylib, &rmeta]),
2776 );
2777 let stale = super::stale_shared_dylib_packages(stdout.as_bytes())
2778 .await
2779 .expect("scan");
2780 assert_eq!(stale, ["waterui-dylib"]);
2781
2782 std::fs::remove_file(unit_dir.join("waterui_dylib.d")).expect("remove dep-info");
2785 let error = super::stale_shared_dylib_packages(stdout.as_bytes())
2786 .await
2787 .expect_err("a fresh dylib without dep-info is an error");
2788 assert!(
2789 error.to_string().contains("no dep-info was found"),
2790 "{error}"
2791 );
2792 });
2793 }
2794
2795 #[test]
2800 fn dep_info_prerequisites_unescape_spaces_and_join_continued_rules() {
2801 let contents = concat!(
2802 "C:\\out\\app.dll: C:\\work\\my\\ app\\src\\lib.rs \\\n",
2803 " C:\\work\\my\\ app\\build.rs C:\\work\\cost$$.rs\n",
2804 "\n",
2805 "C:\\work\\my\\ app\\src\\lib.rs:\n",
2806 );
2807 assert_eq!(
2808 super::dep_info_prerequisites(contents),
2809 vec![
2810 PathBuf::from("C:\\work\\my app\\src\\lib.rs"),
2811 PathBuf::from("C:\\work\\my app\\build.rs"),
2812 PathBuf::from("C:\\work\\cost$$.rs"),
2813 ]
2814 );
2815 }
2816
2817 #[test]
2818 fn static_packaging_removes_only_staged_android_runtime_libraries() {
2819 smol::block_on(async {
2820 let directory = tempdir().expect("temporary Android runtime directory");
2821 let android_triple = triple("aarch64-linux-android");
2822 for file_name in [
2823 "libwaterui_dylib.so",
2824 "libstd-old.so",
2825 "libwaterui_app.so",
2826 "libc++_shared.so",
2827 ] {
2828 std::fs::write(directory.path().join(file_name), [])
2829 .expect("write staged runtime test file");
2830 }
2831
2832 RustDynamicLibraries::remove_staged(directory.path(), &android_triple)
2833 .await
2834 .expect("remove shared Rust runtime libraries");
2835
2836 assert!(!directory.path().join("libwaterui_dylib.so").exists());
2837 assert!(!directory.path().join("libstd-old.so").exists());
2838 assert!(directory.path().join("libwaterui_app.so").exists());
2839 assert!(directory.path().join("libc++_shared.so").exists());
2840 });
2841 }
2842}