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};
14use tracing::warn;
15
16use crate::project::Project;
17use crate::utils::{run_command, std_output_enabled};
18
19#[must_use]
21pub const fn lib_extension_for_triple(triple: &Triple) -> &'static str {
22 match triple.operating_system {
23 OperatingSystem::Darwin(_)
24 | OperatingSystem::MacOSX { .. }
25 | OperatingSystem::IOS(_)
26 | OperatingSystem::TvOS(_)
27 | OperatingSystem::WatchOS(_)
28 | OperatingSystem::VisionOS(_) => "dylib",
29 OperatingSystem::Windows => "dll",
30 _ => "so",
32 }
33}
34
35pub async fn project_toolchain(project: &Project) -> eyre::Result<String> {
41 Ok(crate::toolchain::rust::project_rustup_toolchain(project.root()).await?)
42}
43
44pub async fn rust_target_libdir(triple: &Triple, toolchain: &str) -> eyre::Result<PathBuf> {
50 let target = triple.to_string();
51 let host = crate::toolchain::Host::current().with_env("RUSTUP_TOOLCHAIN", toolchain);
52 let output = host
53 .run(
54 "rustc",
55 ["--print", "target-libdir", "--target", target.as_str()],
56 )
57 .await?;
58 let libdir = output.trim();
59 if libdir.is_empty() {
60 bail!("`rustc --print target-libdir --target {target}` returned an empty path");
61 }
62 let path = PathBuf::from(libdir);
63 if !path.is_dir() {
64 bail!(
65 "Rust target libdir does not exist for dynamic linking: {}",
66 path.display()
67 );
68 }
69 Ok(path)
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub(crate) enum CargoTarget<'a> {
79 Lib,
81 Binary(&'a str),
83}
84
85impl<'a> CargoTarget<'a> {
86 fn cargo_args(self) -> Vec<&'a str> {
87 match self {
88 Self::Lib => vec!["--lib"],
89 Self::Binary(name) => vec!["--bin", name],
90 }
91 }
92
93 const fn accepts_crate_type_override(self) -> bool {
94 matches!(self, Self::Lib)
95 }
96
97 fn matches(&self, target: &cargo_metadata::Target) -> bool {
100 use cargo_metadata::TargetKind;
101 match self {
102 Self::Binary(name) => {
103 target.name.as_str() == *name && target.kind.contains(&TargetKind::Bin)
104 }
105 Self::Lib => target.kind.iter().any(|kind| {
106 matches!(
107 kind,
108 TargetKind::Lib
109 | TargetKind::RLib
110 | TargetKind::DyLib
111 | TargetKind::CDyLib
112 | TargetKind::StaticLib
113 | TargetKind::ProcMacro
114 )
115 }),
116 }
117 }
118}
119
120#[derive(Debug)]
123pub struct BuiltTarget {
124 pub profile_dir: PathBuf,
127 pub artifact: PathBuf,
131 pub shared_runtime: Option<PathBuf>,
134}
135
136impl BuiltTarget {
137 pub fn shared_runtime(&self) -> eyre::Result<&Path> {
142 self.shared_runtime.as_deref().ok_or_else(|| {
143 eyre::eyre!(
144 "Cargo reported no `waterui-dylib` dynamic library for the build in {}; the shared WaterUI runtime was not built",
145 self.profile_dir.display()
146 )
147 })
148 }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub enum RustLinkage {
154 Static,
156 SharedRuntime,
158}
159
160pub fn configure_generated_crate_compilation(command: &mut Command) {
176 command.env("CARGO_INCREMENTAL", "0");
177}
178
179fn with_managed_tools_path(command: &mut Command) {
184 if let Some((key, value)) =
185 crate::toolchain::managed_tool::managed_tools_path_env(&crate::toolchain::Host::current())
186 {
187 command.env(key, value);
188 }
189}
190
191#[derive(Debug, Clone, PartialEq, Eq)]
193pub struct RustDynamicLibraries {
194 waterui: PathBuf,
195 standard_library: PathBuf,
196 triple: Triple,
197}
198
199impl RustDynamicLibraries {
200 pub async fn resolve(
211 built: &BuiltTarget,
212 triple: &Triple,
213 project: &Project,
214 ) -> eyre::Result<Self> {
215 let waterui = built.shared_runtime()?.to_path_buf();
216 let lib_dir = &built.profile_dir;
217
218 let resolution_triple = triple.clone();
224 let deps_dir = lib_dir.join("deps");
225 let staged =
226 unblock(move || resolve_rust_standard_library_in(&deps_dir, &resolution_triple)).await;
227 let standard_library = match staged {
228 Ok(path) => path,
229 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
230 let toolchain = project_toolchain(project).await?;
231 let target_libdir = rust_target_libdir(triple, &toolchain).await?;
232 let resolution_triple = triple.clone();
233 unblock(move || {
234 resolve_rust_standard_library_in(&target_libdir, &resolution_triple)
235 })
236 .await?
237 }
238 Err(error) => return Err(error.into()),
239 };
240
241 Ok(Self {
242 waterui,
243 standard_library,
244 triple: triple.clone(),
245 })
246 }
247
248 #[must_use]
250 pub fn waterui(&self) -> &Path {
251 &self.waterui
252 }
253
254 #[must_use]
256 pub fn standard_library(&self) -> &Path {
257 &self.standard_library
258 }
259
260 pub fn iter(&self) -> impl Iterator<Item = &Path> {
262 [self.waterui(), self.standard_library()].into_iter()
263 }
264
265 pub async fn stage(&self, destination: &Path) -> eyre::Result<()> {
276 smol::fs::create_dir_all(destination).await?;
277 let sources: Vec<PathBuf> = self.iter().map(|path| (*path).to_path_buf()).collect();
282 Self::remove_staged_except(destination, &self.triple, &sources).await?;
283 for source in &sources {
284 let file_name = source.file_name().ok_or_else(|| {
285 eyre::eyre!(
286 "Dynamic library path has no file name: {}",
287 source.display()
288 )
289 })?;
290 let staged = destination.join(file_name);
291 if *source == staged {
292 continue;
293 }
294 crate::utils::copy_file(source, &staged)
295 .await
296 .wrap_err_with(|| {
297 format!(
298 "Failed to stage {} to {}",
299 source.display(),
300 staged.display()
301 )
302 })?;
303 }
304 Ok(())
305 }
306
307 pub async fn remove_staged(destination: &Path, triple: &Triple) -> eyre::Result<()> {
312 Self::remove_staged_except(destination, triple, &[]).await
313 }
314
315 async fn remove_staged_except(
319 destination: &Path,
320 triple: &Triple,
321 keep: &[PathBuf],
322 ) -> eyre::Result<()> {
323 if !destination.is_dir() {
324 return Ok(());
325 }
326
327 let waterui = dynamic_library_file_name("waterui_dylib", triple);
328 let (standard_library_prefix, extension) =
329 if triple.operating_system == OperatingSystem::Windows {
330 ("std-", "dll")
331 } else {
332 ("libstd-", lib_extension_for_triple(triple))
333 };
334 let mut entries = smol::fs::read_dir(destination).await?;
335 while let Some(entry) = entries.next().await {
336 let entry = entry?;
337 if keep.contains(&entry.path()) {
338 continue;
339 }
340 let file_name = entry.file_name();
341 let file_name = file_name.to_string_lossy();
342 if file_name == waterui
343 || (file_name.starts_with(standard_library_prefix)
344 && entry.path().extension().and_then(|value| value.to_str()) == Some(extension))
345 {
346 smol::fs::remove_file(entry.path()).await?;
347 }
348 }
349 Ok(())
350 }
351}
352
353fn dynamic_library_file_name(crate_name: &str, triple: &Triple) -> String {
354 if triple.operating_system == OperatingSystem::Windows {
355 format!("{crate_name}.dll")
356 } else {
357 format!("lib{crate_name}.{}", lib_extension_for_triple(triple))
358 }
359}
360
361fn resolve_rust_standard_library_in(libdir: &Path, triple: &Triple) -> std::io::Result<PathBuf> {
367 let (prefix, extension) = if triple.operating_system == OperatingSystem::Windows {
368 ("std-", "dll")
369 } else {
370 ("libstd-", lib_extension_for_triple(triple))
371 };
372 let entries = match std::fs::read_dir(libdir) {
373 Ok(entries) => entries,
374 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
375 return Err(std::io::Error::new(
376 std::io::ErrorKind::NotFound,
377 format!("{} does not exist", libdir.display()),
378 ));
379 }
380 Err(error) => return Err(error),
381 };
382 let mut matches = entries
383 .filter_map(|entry| entry.ok().map(|entry| entry.path()))
384 .filter(|path| {
385 path.file_name()
386 .and_then(|name| name.to_str())
387 .is_some_and(|name| {
388 name.starts_with(prefix)
389 && path.extension().and_then(|extension| extension.to_str())
390 == Some(extension)
391 })
392 })
393 .collect::<Vec<_>>();
394 matches.sort_unstable();
395 match matches.as_slice() {
396 [path] => Ok(path.clone()),
397 [] => Err(std::io::Error::new(
398 std::io::ErrorKind::NotFound,
399 format!(
400 "Rust target libdir {} contains no dynamic standard library for {triple}",
401 libdir.display()
402 ),
403 )),
404 _ => Err(std::io::Error::other(format!(
405 "Rust target libdir {} contains multiple dynamic standard libraries for {triple}: {}",
406 libdir.display(),
407 matches
408 .iter()
409 .map(|path| path.display().to_string())
410 .collect::<Vec<_>>()
411 .join(", ")
412 ))),
413 }
414}
415
416#[derive(Debug, Clone)]
418pub struct RustBuild {
419 path: PathBuf,
420 triple: Triple,
421 project: Option<Project>,
422 target_dir: Option<PathBuf>,
424 sccache_path: Option<PathBuf>,
426 features: Vec<String>,
428 crate_type_override: Option<String>,
430 rustc_flags: Vec<String>,
432 final_rustc_args: Vec<String>,
441 build_std_toolchain: Option<String>,
450 envs: Vec<(String, OsString)>,
452 progress: Option<BuildProgress>,
454}
455
456#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
462pub enum BuildProfile {
463 #[default]
465 Debug,
466 Optimized,
470 Release,
472 Profiling,
475}
476
477impl BuildProfile {
478 #[must_use]
481 pub const fn is_release(self) -> bool {
482 matches!(self, Self::Release | Self::Profiling)
483 }
484
485 #[must_use]
488 pub const fn is_development(self) -> bool {
489 !self.is_release()
490 }
491
492 fn development_envs(self) -> Vec<(String, OsString)> {
511 let entries: &[(&str, &str)] = match self {
512 Self::Debug => &[],
513 Self::Optimized => &[
514 ("CARGO_PROFILE_DEV_OPT_LEVEL", "1"),
515 ("CARGO_PROFILE_DEV_DEBUG", "true"),
516 ("CARGO_PROFILE_DEV_DEBUG_ASSERTIONS", "false"),
517 ("CARGO_PROFILE_DEV_OVERFLOW_CHECKS", "false"),
518 ],
519 Self::Release => &[
520 ("CARGO_PROFILE_RELEASE_OPT_LEVEL", "3"),
521 ("CARGO_PROFILE_RELEASE_PANIC", "unwind"),
522 ("CARGO_PROFILE_RELEASE_LTO", "off"),
523 ],
524 Self::Profiling => &[
525 ("CARGO_PROFILE_RELEASE_OPT_LEVEL", "3"),
526 ("CARGO_PROFILE_RELEASE_PANIC", "unwind"),
527 ("CARGO_PROFILE_RELEASE_LTO", "off"),
528 ("CARGO_PROFILE_RELEASE_DEBUG", "true"),
529 ("CARGO_PROFILE_RELEASE_STRIP", "none"),
530 ],
531 };
532 entries
533 .iter()
534 .map(|(key, value)| ((*key).to_string(), OsString::from(*value)))
535 .collect()
536 }
537}
538
539#[derive(Debug, Clone)]
541pub struct BuildOptions {
542 profile: BuildProfile,
543 output_dir: Option<std::path::PathBuf>,
544 sccache_path: Option<std::path::PathBuf>,
546 target_triple: Option<Triple>,
548 linkage: RustLinkage,
550 dynamic_module_loading: bool,
555 dev_server: bool,
558 cargo_envs: Vec<(String, OsString)>,
560 progress: Option<BuildProgress>,
562}
563
564impl BuildOptions {
565 #[must_use]
572 pub fn development(profile: BuildProfile) -> Self {
573 Self {
574 profile,
575 output_dir: None,
576 sccache_path: None,
577 target_triple: None,
578 linkage: RustLinkage::SharedRuntime,
579 dynamic_module_loading: false,
580 dev_server: false,
581 cargo_envs: profile.development_envs(),
582 progress: None,
583 }
584 }
585
586 #[must_use]
592 pub fn with_static_runtime(mut self) -> Self {
593 self.linkage = RustLinkage::Static;
594 self.cargo_envs.retain(|(key, _)| {
597 key != "CARGO_PROFILE_RELEASE_PANIC" && key != "CARGO_PROFILE_RELEASE_LTO"
598 });
599 self
600 }
601
602 #[must_use]
608 pub const fn packaging(profile: BuildProfile) -> Self {
609 Self {
610 profile,
611 output_dir: None,
612 sccache_path: None,
613 target_triple: None,
614 linkage: RustLinkage::Static,
615 dynamic_module_loading: false,
616 dev_server: false,
617 cargo_envs: Vec::new(),
618 progress: None,
619 }
620 }
621
622 #[must_use]
624 pub const fn is_release(&self) -> bool {
625 self.profile.is_release()
626 }
627
628 #[must_use]
630 pub const fn profile(&self) -> BuildProfile {
631 self.profile
632 }
633
634 #[must_use]
636 pub fn cargo_envs(&self) -> &[(String, OsString)] {
637 &self.cargo_envs
638 }
639
640 #[must_use]
642 pub const fn with_dev_server(mut self, dev_server: bool) -> Self {
643 self.dev_server = dev_server;
644 self
645 }
646
647 #[must_use]
649 pub const fn uses_dev_server(&self) -> bool {
650 self.dev_server
651 }
652
653 #[must_use]
655 pub fn output_dir(&self) -> Option<&std::path::Path> {
656 self.output_dir.as_deref()
657 }
658
659 #[must_use]
661 pub fn with_output_dir(mut self, output_dir: impl Into<std::path::PathBuf>) -> Self {
662 self.output_dir = Some(output_dir.into());
663 self
664 }
665
666 #[must_use]
668 pub fn sccache_path(&self) -> Option<&std::path::Path> {
669 self.sccache_path.as_deref()
670 }
671
672 #[must_use]
677 pub fn with_sccache(mut self, sccache_path: impl Into<std::path::PathBuf>) -> Self {
678 self.sccache_path = Some(sccache_path.into());
679 self
680 }
681
682 #[must_use]
684 pub const fn target_triple(&self) -> Option<&Triple> {
685 self.target_triple.as_ref()
686 }
687
688 #[must_use]
690 pub fn with_target_triple(mut self, target_triple: Triple) -> Self {
691 self.target_triple = Some(target_triple);
692 self
693 }
694
695 #[must_use]
697 pub const fn linkage(&self) -> RustLinkage {
698 self.linkage
699 }
700
701 #[must_use]
707 pub const fn with_dynamic_module_loading(mut self) -> Self {
708 self.dynamic_module_loading = true;
709 self
710 }
711
712 #[must_use]
714 pub const fn loads_dynamic_modules(&self) -> bool {
715 self.dynamic_module_loading
716 }
717
718 #[must_use]
721 pub fn with_progress(mut self, progress: BuildProgress) -> Self {
722 self.progress = Some(progress);
723 self
724 }
725
726 #[must_use]
728 pub const fn progress(&self) -> Option<&BuildProgress> {
729 self.progress.as_ref()
730 }
731}
732
733#[derive(Debug, thiserror::Error)]
735pub enum RustBuildError {
736 #[error("Failed to execute cargo build: {0}")]
738 FailToExecuteCargoBuild(std::io::Error),
739
740 #[error("Failed to build Rust library: {0}")]
742 FailToBuildRustLibrary(std::io::Error),
743}
744
745#[derive(Debug, Clone, PartialEq, Eq)]
753pub enum CompileEvent {
754 Unit {
758 phase: &'static str,
760 name: String,
762 version: Option<String>,
764 },
765 Finished(String),
767 Line(String),
770}
771
772#[derive(Clone)]
777pub struct BuildProgress {
778 report: std::sync::Arc<dyn Fn(CompileEvent) + Send + Sync>,
779 shows_all_lines: bool,
783}
784
785impl std::fmt::Debug for BuildProgress {
786 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
787 formatter.write_str("BuildProgress(..)")
788 }
789}
790
791impl BuildProgress {
792 #[must_use]
794 pub fn new(report: impl Fn(CompileEvent) + Send + Sync + 'static) -> Self {
795 Self {
796 report: std::sync::Arc::new(report),
797 shows_all_lines: false,
798 }
799 }
800
801 #[must_use]
804 pub const fn showing_all_lines(mut self) -> Self {
805 self.shows_all_lines = true;
806 self
807 }
808
809 #[must_use]
811 pub const fn shows_all_lines(&self) -> bool {
812 self.shows_all_lines
813 }
814
815 fn report(&self, event: CompileEvent) {
816 (self.report)(event);
817 }
818}
819
820const CARGO_UNIT_PHASES: &[&str] = &[
822 "Compiling",
823 "Checking",
824 "Fresh",
825 "Downloading",
826 "Downloaded",
827 "Doc-tests",
828];
829
830fn classify_compile_line(line: &str) -> CompileEvent {
838 let raw = line.trim();
839 let stripped = console::strip_ansi_codes(raw);
840 let text = stripped.trim();
841 for phase in CARGO_UNIT_PHASES {
842 let Some(rest) = text
843 .strip_prefix(phase)
844 .and_then(|rest| rest.strip_prefix(' '))
845 else {
846 continue;
847 };
848 let Some((name, version)) = rest.split_once(" v") else {
851 return CompileEvent::Line(raw.to_owned());
852 };
853 let version = version.split([' ', '(']).next().unwrap_or_default();
854 return CompileEvent::Unit {
855 phase,
856 name: name.to_owned(),
857 version: (!version.is_empty()).then(|| version.to_owned()),
858 };
859 }
860 if text.starts_with("Finished ") {
861 return CompileEvent::Finished(raw.to_owned());
862 }
863 CompileEvent::Line(raw.to_owned())
864}
865
866pub(crate) async fn command_output_with_progress(
878 command: &mut Command,
879 progress: Option<BuildProgress>,
880) -> io::Result<std::process::Output> {
881 let mut child = command
882 .kill_on_drop(true)
883 .stdin(Stdio::null())
884 .stdout(Stdio::piped())
885 .stderr(Stdio::piped())
886 .spawn()?;
887 let stdout_pipe = child.stdout.take().expect("stdout is piped");
888 let stderr_pipe = child.stderr.take().expect("stderr is piped");
889
890 let echo = progress.is_none() && std_output_enabled();
893 let stdout_task = smol::spawn(drain_pipe(stdout_pipe));
896 let stderr_task = smol::spawn(drain_cargo_stderr(stderr_pipe, progress, echo));
897 let status = child.status().await?;
898 let stdout = stdout_task.await?;
899 let stderr = stderr_task.await?;
900 Ok(std::process::Output {
901 status,
902 stdout,
903 stderr,
904 })
905}
906
907async fn drain_pipe(mut reader: impl smol::io::AsyncRead + Unpin) -> io::Result<Vec<u8>> {
909 let mut collected = Vec::new();
910 let mut chunk = [0u8; 8192];
911 loop {
912 let read = reader.read(&mut chunk).await?;
913 if read == 0 {
914 break;
915 }
916 collected.extend_from_slice(&chunk[..read]);
917 }
918 Ok(collected)
919}
920
921async fn drain_cargo_stderr(
925 mut reader: impl smol::io::AsyncRead + Unpin,
926 progress: Option<BuildProgress>,
927 echo: bool,
928) -> io::Result<Vec<u8>> {
929 let mut collected = Vec::new();
930 let mut pending: Vec<u8> = Vec::new();
931 let mut chunk = [0u8; 8192];
932 loop {
933 let read = reader.read(&mut chunk).await?;
934 if read == 0 {
935 break;
936 }
937 collected.extend_from_slice(&chunk[..read]);
938 if echo {
939 let _ = io::stderr().write_all(&chunk[..read]);
940 let _ = io::stderr().flush();
941 }
942 if let Some(sink) = &progress {
943 pending.extend_from_slice(&chunk[..read]);
944 while let Some(newline) = pending.iter().position(|byte| *byte == b'\n') {
948 let line: Vec<u8> = pending.drain(..=newline).collect();
949 let line = String::from_utf8_lossy(&line);
950 let line = line.trim_end();
951 if !line.trim().is_empty() {
952 sink.report(classify_compile_line(line));
953 }
954 }
955 }
956 }
957 if let Some(sink) = &progress {
958 let tail = String::from_utf8_lossy(&pending);
959 let tail = tail.trim_end();
960 if !tail.trim().is_empty() {
961 sink.report(classify_compile_line(tail));
962 }
963 }
964 Ok(collected)
965}
966
967impl RustBuild {
968 pub fn new(path: impl AsRef<Path>, triple: Triple) -> Self {
970 Self {
971 path: path.as_ref().to_path_buf(),
972 triple,
973 project: None,
974 target_dir: None,
975 sccache_path: None,
976 features: Vec::new(),
977 crate_type_override: None,
978 rustc_flags: Vec::new(),
979 final_rustc_args: Vec::new(),
980 build_std_toolchain: None,
981 envs: Vec::new(),
982 progress: None,
983 }
984 }
985
986 pub(crate) fn with_project(mut self, project: &Project) -> Self {
993 self.project = Some(project.clone());
994 self
995 }
996
997 #[must_use]
999 pub fn with_target_dir(mut self, target_dir: impl Into<PathBuf>) -> Self {
1000 self.target_dir = Some(target_dir.into());
1001 self
1002 }
1003
1004 #[must_use]
1009 pub fn with_sccache(mut self, sccache_path: PathBuf) -> Self {
1010 self.sccache_path = Some(sccache_path);
1011 self
1012 }
1013
1014 #[must_use]
1018 pub fn with_feature(mut self, feature: impl Into<String>) -> Self {
1019 self.features.push(feature.into());
1020 self
1021 }
1022
1023 #[must_use]
1025 pub fn with_features(mut self, features: impl IntoIterator<Item = impl Into<String>>) -> Self {
1026 self.features.extend(features.into_iter().map(Into::into));
1027 self
1028 }
1029
1030 #[must_use]
1032 pub fn features(&self) -> &[String] {
1033 &self.features
1034 }
1035
1036 #[must_use]
1038 pub fn with_rustc_flag(mut self, flag: impl Into<String>) -> Self {
1039 self.rustc_flags.push(flag.into());
1040 self
1041 }
1042
1043 #[must_use]
1050 pub fn with_final_rustc_arg(mut self, flag: impl Into<String>) -> Self {
1051 self.final_rustc_args.push(flag.into());
1052 self
1053 }
1054
1055 #[must_use]
1068 pub fn with_build_std(mut self, toolchain: impl Into<String>) -> Self {
1069 self.build_std_toolchain = Some(toolchain.into());
1070 self
1071 }
1072
1073 #[must_use]
1075 pub fn with_preferred_dynamic_linking(self) -> Self {
1076 self.with_rustc_flag("-Cprefer-dynamic")
1077 .with_rustc_flag("-Crpath")
1078 }
1079
1080 #[must_use]
1092 pub fn with_linkage(
1093 self,
1094 linkage: RustLinkage,
1095 development_feature: &str,
1096 loader_search_paths: &[&str],
1097 ) -> Self {
1098 if linkage == RustLinkage::Static {
1099 return self;
1100 }
1101 let build = self
1102 .with_feature(development_feature)
1103 .with_preferred_dynamic_linking();
1104 loader_search_paths.iter().fold(build, |build, path| {
1105 build.with_final_rustc_arg(format!("-Clink-arg=-Wl,-rpath,{path}"))
1106 })
1107 }
1108
1109 #[must_use]
1111 pub fn with_crate_type_override(mut self, crate_type: impl Into<String>) -> Self {
1112 self.crate_type_override = Some(crate_type.into());
1113 self
1114 }
1115
1116 #[must_use]
1118 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<OsString>) -> Self {
1119 self.envs.push((key.into(), value.into()));
1120 self
1121 }
1122
1123 #[must_use]
1125 pub fn with_envs(mut self, envs: impl IntoIterator<Item = (String, OsString)>) -> Self {
1126 self.envs.extend(envs);
1127 self
1128 }
1129
1130 #[must_use]
1135 pub fn with_progress(mut self, progress: BuildProgress) -> Self {
1136 self.progress = Some(progress);
1137 self
1138 }
1139
1140 #[must_use]
1142 pub const fn triple(&self) -> &Triple {
1143 &self.triple
1144 }
1145
1146 pub async fn dev_build(&self) -> Result<BuiltTarget, RustBuildError> {
1154 self.build_lib(false).await
1155 }
1156
1157 pub async fn release_build(&self) -> Result<BuiltTarget, RustBuildError> {
1163 self.build_lib(true).await
1164 }
1165
1166 pub async fn build_lib(&self, release: bool) -> Result<BuiltTarget, RustBuildError> {
1177 self.build_inner(release, CargoTarget::Lib, self.lib_artifact_extension())
1178 .await
1179 }
1180
1181 pub async fn build_dylib(&self, release: bool) -> Result<BuiltTarget, RustBuildError> {
1191 self.build_inner(
1192 release,
1193 CargoTarget::Lib,
1194 Some(lib_extension_for_triple(&self.triple)),
1195 )
1196 .await
1197 }
1198
1199 pub async fn build_binary(
1209 &self,
1210 binary_name: &str,
1211 release: bool,
1212 ) -> Result<BuiltTarget, RustBuildError> {
1213 self.build_inner(release, CargoTarget::Binary(binary_name), None)
1214 .await
1215 }
1216
1217 pub async fn dylib_path(
1225 &self,
1226 crate_name: &str,
1227 release: bool,
1228 ) -> Result<PathBuf, RustBuildError> {
1229 let lib_dir = self.lib_output_dir(release).await?;
1230 let lib_name = crate_name.replace('-', "_");
1231 let ext = lib_extension_for_triple(&self.triple);
1232 Ok(lib_dir.join(format!("lib{lib_name}.{ext}")))
1233 }
1234
1235 async fn build_inner(
1237 &self,
1238 release: bool,
1239 cargo_target: CargoTarget<'_>,
1240 artifact_extension: Option<&'static str>,
1241 ) -> Result<BuiltTarget, RustBuildError> {
1242 let mut output = self.cargo_build_output(release, cargo_target).await?;
1243
1244 if !output.status.success() {
1245 let mut combined = combined_build_output(&output);
1246
1247 if should_retry_after_cmake_generator_mismatch(&combined)
1250 && self.clean_stale_cmake_build_dirs().await?
1251 {
1252 output = self.cargo_build_output(release, cargo_target).await?;
1253 combined = combined_build_output(&output);
1254 }
1255
1256 if !output.status.success() && should_auto_install_meson(&combined) {
1257 match ensure_meson_installed_for_build().await {
1258 Ok(()) => {
1259 output = self.cargo_build_output(release, cargo_target).await?;
1260 }
1261 Err(install_err) => {
1262 return Err(RustBuildError::FailToBuildRustLibrary(
1263 std::io::Error::other(format!(
1264 "Cargo build failed and meson appears missing.\n\
1265Automatic meson installation failed: {install_err}\n\n{}",
1266 self.failure_report(&combined)
1267 )),
1268 ));
1269 }
1270 }
1271 }
1272 }
1273
1274 if !output.status.success() {
1275 let combined = combined_build_output(&output);
1276 return Err(RustBuildError::FailToBuildRustLibrary(
1277 std::io::Error::other(format!(
1278 "Cargo build failed:\n{}",
1279 self.failure_report(&combined)
1280 )),
1281 ));
1282 }
1283
1284 let stale = stale_shared_dylib_packages(&output.stdout).await?;
1296 if !stale.is_empty() {
1297 let target_dir = self.target_directory().await?;
1298 for unit in &stale {
1299 warn!(
1300 package = unit.package,
1301 artifact = %unit.artifact.display(),
1302 "discarding a shared dylib unit and rebuilding it: {}",
1303 unit.reason
1304 );
1305 clean_cargo_package(&self.path, &unit.package, &target_dir).await?;
1306 }
1307 output = self.cargo_build_output(release, cargo_target).await?;
1308 if !output.status.success() {
1309 let combined = combined_build_output(&output);
1310 return Err(RustBuildError::FailToBuildRustLibrary(
1311 std::io::Error::other(format!(
1312 "Cargo build failed:\n{}",
1313 self.failure_report(&combined)
1314 )),
1315 ));
1316 }
1317 let unrecovered = stale_shared_dylib_packages(&output.stdout).await?;
1318 if !unrecovered.is_empty() {
1319 return Err(unrecoverable_shared_dylib_error(&unrecovered, &target_dir));
1320 }
1321 }
1322
1323 let artifact =
1324 reported_artifact(&output.stdout, &self.path, cargo_target, artifact_extension)?;
1325 let shared_runtime = reported_shared_runtime(&output.stdout)?;
1326 let profile_dir = self.lib_output_dir(release).await?;
1327 Ok(BuiltTarget {
1328 profile_dir,
1329 artifact,
1330 shared_runtime,
1331 })
1332 }
1333
1334 fn lib_artifact_extension(&self) -> Option<&'static str> {
1337 self.crate_type_override
1338 .as_deref()
1339 .and_then(|crate_type| crate_type_artifact_extension(crate_type, &self.triple))
1340 }
1341
1342 fn failure_report(&self, combined: &str) -> String {
1345 if self
1346 .progress
1347 .as_ref()
1348 .is_some_and(BuildProgress::shows_all_lines)
1349 {
1350 output_tail(combined)
1351 } else {
1352 combined.to_owned()
1353 }
1354 }
1355
1356 async fn clean_stale_cmake_build_dirs(&self) -> Result<bool, RustBuildError> {
1357 let target_dir = self.target_directory().await?;
1358 let triple = self.triple.to_string();
1359
1360 let removed = unblock(move || {
1361 let mut removed = 0usize;
1362 removed +=
1363 remove_cmake_build_dirs_in(&target_dir.join(&triple).join("debug").join("build"))?;
1364 removed += remove_cmake_build_dirs_in(
1365 &target_dir.join(&triple).join("release").join("build"),
1366 )?;
1367 Ok::<usize, std::io::Error>(removed)
1368 })
1369 .await
1370 .map_err(|error| {
1371 RustBuildError::FailToBuildRustLibrary(std::io::Error::other(format!(
1372 "Failed to clean stale CMake cache: {error}"
1373 )))
1374 })?;
1375
1376 Ok(removed > 0)
1377 }
1378
1379 async fn cargo_build_output(
1380 &self,
1381 release: bool,
1382 cargo_target: CargoTarget<'_>,
1383 ) -> Result<std::process::Output, RustBuildError> {
1384 let framework = self.project.as_ref().and_then(|project| {
1385 project
1386 .manifest()
1387 .framework
1388 .as_ref()
1389 .map(|framework| (project, framework))
1390 });
1391 if let Some((project, framework)) = framework {
1392 framework
1393 .prepare_build(project, &self.path, &self.features)
1394 .await
1395 .map_err(|error| {
1396 RustBuildError::FailToBuildRustLibrary(std::io::Error::other(error.to_string()))
1397 })?;
1398 }
1399 let crate_type_override = if cargo_target.accepts_crate_type_override() {
1400 self.crate_type_override.as_deref()
1401 } else {
1402 None
1403 };
1404 let mut cmd = Command::new("cargo");
1405 let cargo_subcommand = if crate_type_override.is_some() || !self.final_rustc_args.is_empty()
1406 {
1407 "rustc"
1408 } else {
1409 "build"
1410 };
1411 let mut cmd = cmd.arg(cargo_subcommand);
1412 if self.build_std_toolchain.is_some() {
1413 cmd = cmd.arg("-Zbuild-std=std,panic_abort");
1423 cmd =
1424 cmd.arg("-Zbuild-std-features=panic-unwind,backtrace,default,compiler-builtins-c");
1425 }
1426 let mut cmd = cmd
1427 .arg("--message-format=json-render-diagnostics")
1428 .args(cargo_target.cargo_args())
1429 .args(["--target", self.triple.to_string().as_str()])
1430 .args(framework.is_some().then_some("--locked"))
1431 .current_dir(&self.path);
1432
1433 if let Some(target_dir) = &self.target_dir {
1434 cmd = cmd.arg("--target-dir").arg(target_dir);
1435 }
1436 with_managed_tools_path(cmd);
1437 for (key, value) in &self.envs {
1439 cmd.env(key, value);
1440 }
1441 let mut cmd = self.with_project_toolchain_env(cmd).await?;
1442
1443 if !self.rustc_flags.is_empty() {
1444 let mut rustflags = std::env::var_os("RUSTFLAGS").unwrap_or_default();
1445 if !rustflags.is_empty() {
1446 rustflags.push(" ");
1447 }
1448 rustflags.push(self.rustc_flags.join(" "));
1449 cmd = cmd.env("RUSTFLAGS", rustflags);
1450 }
1451
1452 configure_generated_crate_compilation(cmd);
1453
1454 if let Some(sccache_path) = &self.sccache_path {
1456 crate::toolchain::sccache::configure_compilation_cache(cmd, sccache_path).map_err(
1457 |error| {
1458 RustBuildError::FailToBuildRustLibrary(std::io::Error::other(error.to_string()))
1459 },
1460 )?;
1461 }
1462
1463 if self.build_std_toolchain.is_some() {
1469 cmd = self.with_build_std_envs(cmd, release).await?;
1470 }
1471
1472 if self.triple.environment == Environment::Sim
1479 && let Some(clang_args) = self.bindgen_clang_args_for_simulator().await
1480 {
1481 let bindgen_target_key = format!(
1482 "BINDGEN_EXTRA_CLANG_ARGS_{}",
1483 self.triple.to_string().replace('-', "_")
1484 );
1485 cmd = cmd.env(bindgen_target_key, clang_args);
1486 }
1487
1488 if release {
1489 cmd = cmd.arg("--release");
1490 }
1491
1492 if !self.features.is_empty() {
1494 cmd = cmd.args(["--features", &self.features.join(",")]);
1495 }
1496
1497 if crate_type_override.is_some() || !self.final_rustc_args.is_empty() {
1498 cmd = cmd.arg("--");
1499 if let Some(crate_type) = crate_type_override {
1500 cmd = cmd.arg("--crate-type").arg(crate_type);
1501 }
1502 cmd = cmd.args(&self.final_rustc_args);
1503 }
1504
1505 if std_output_enabled()
1510 && std::env::var_os("CARGO_TERM_COLOR").is_none()
1511 && !self.envs.iter().any(|(key, _)| key == "CARGO_TERM_COLOR")
1512 {
1513 cmd.env("CARGO_TERM_COLOR", "always");
1514 }
1515
1516 command_output_with_progress(cmd, self.progress.clone())
1517 .await
1518 .map_err(RustBuildError::FailToExecuteCargoBuild)
1519 }
1520
1521 async fn with_project_toolchain_env<'a>(
1527 &self,
1528 cmd: &'a mut Command,
1529 ) -> Result<&'a mut Command, RustBuildError> {
1530 if self.build_std_toolchain.is_some() {
1531 return Ok(cmd);
1532 }
1533 let Some(project) = &self.project else {
1534 return Ok(cmd);
1535 };
1536 let toolchain = project_toolchain(project).await.map_err(|error| {
1537 RustBuildError::FailToBuildRustLibrary(std::io::Error::other(error.to_string()))
1538 })?;
1539 Ok(cmd.env("RUSTUP_TOOLCHAIN", toolchain))
1540 }
1541
1542 async fn with_build_std_envs<'a>(
1546 &self,
1547 cmd: &'a mut Command,
1548 release: bool,
1549 ) -> Result<&'a mut Command, RustBuildError> {
1550 let Some(toolchain) = &self.build_std_toolchain else {
1551 return Ok(cmd);
1552 };
1553 let publish_dir = self.lib_output_dir(release).await?.join("deps");
1554 let cmd = cmd
1555 .env("RUSTUP_TOOLCHAIN", toolchain)
1556 .env(
1557 "RUSTC_WRAPPER",
1558 crate::toolchain::Host::current_exe()
1559 .map_err(RustBuildError::FailToExecuteCargoBuild)?,
1560 )
1561 .env(crate::workflows::rustc_wrapper::WRAPPER_MODE_ENV, "1")
1562 .env(
1563 crate::workflows::rustc_wrapper::BUILD_STD_TARGET_ENV,
1564 self.triple.to_string(),
1565 )
1566 .env(
1567 crate::workflows::rustc_wrapper::BUILD_STD_DYLIB_DIR_ENV,
1568 publish_dir,
1569 );
1570 if let Some(sccache_path) = &self.sccache_path {
1571 cmd.env(
1572 crate::workflows::rustc_wrapper::WRAPPER_CHAIN_ENV,
1573 sccache_path,
1574 );
1575 }
1576 cmd.env_remove("RUSTC_WORKSPACE_WRAPPER");
1582 cmd.env_remove("CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER");
1583 Ok(cmd)
1584 }
1585
1586 pub async fn lib_output_dir(&self, release: bool) -> Result<PathBuf, RustBuildError> {
1591 let target_directory = self.target_directory().await?;
1592 Ok(target_directory
1593 .join(self.triple.to_string())
1594 .join(if release { "release" } else { "debug" }))
1595 }
1596
1597 async fn target_directory(&self) -> Result<PathBuf, RustBuildError> {
1598 if let Some(target_dir) = &self.target_dir {
1599 return Ok(target_dir.clone());
1600 }
1601
1602 let build_path = self.path.clone();
1603 let metadata = unblock(move || {
1604 cargo_metadata::MetadataCommand::new()
1605 .no_deps()
1606 .current_dir(build_path)
1607 .exec()
1608 .map_err(|e| {
1609 RustBuildError::FailToBuildRustLibrary(std::io::Error::new(
1610 std::io::ErrorKind::InvalidData,
1611 e,
1612 ))
1613 })
1614 })
1615 .await?;
1616 Ok(metadata.target_directory.as_std_path().to_path_buf())
1617 }
1618
1619 async fn bindgen_clang_args_for_simulator(&self) -> Option<String> {
1624 let (sdk_name, target_os) = match self.triple.operating_system {
1625 OperatingSystem::IOS(_) => ("iphonesimulator", "ios"),
1626 OperatingSystem::TvOS(_) => ("appletvsimulator", "tvos"),
1627 OperatingSystem::WatchOS(_) => ("watchsimulator", "watchos"),
1628 OperatingSystem::VisionOS(_) => ("xrsimulator", "xros"),
1629 _ => return None,
1630 };
1631
1632 let arch = match self.triple.architecture {
1633 target_lexicon::Architecture::Aarch64(_) => "arm64",
1634 target_lexicon::Architecture::X86_64 => "x86_64",
1635 _ => return None,
1636 };
1637
1638 let sdk_path = run_command("xcrun", ["--sdk", sdk_name, "--show-sdk-path"])
1640 .await
1641 .ok()
1642 .map(|s| s.trim().to_string())?;
1643
1644 let min_version = if matches!(target_os, "ios" | "tvos") {
1646 "17.0"
1647 } else if target_os == "watchos" {
1648 "10.0"
1649 } else {
1650 debug_assert_eq!(
1651 target_os, "xros",
1652 "bindgen simulator target_os must be one of ios/tvos/watchos/xros"
1653 );
1654 "1.0"
1655 };
1656
1657 Some(format!(
1658 "--target={arch}-apple-{target_os}{min_version}-simulator -isysroot {sdk_path}"
1659 ))
1660 }
1661}
1662
1663fn crate_type_artifact_extension(crate_type: &str, triple: &Triple) -> Option<&'static str> {
1666 match crate_type {
1667 "lib" | "rlib" => Some("rlib"),
1668 "staticlib" => Some(if matches!(triple.environment, Environment::Msvc) {
1669 "lib"
1670 } else {
1671 "a"
1672 }),
1673 "cdylib" | "dylib" | "proc-macro" => Some(lib_extension_for_triple(triple)),
1674 _ => None,
1675 }
1676}
1677
1678pub(crate) fn reported_artifact(
1693 stdout: &[u8],
1694 crate_dir: &Path,
1695 cargo_target: CargoTarget<'_>,
1696 artifact_extension: Option<&'static str>,
1697) -> Result<PathBuf, RustBuildError> {
1698 let manifest_path = dunce::canonicalize(crate_dir.join("Cargo.toml")).map_err(|error| {
1699 RustBuildError::FailToBuildRustLibrary(io::Error::other(format!(
1700 "failed to canonicalize {}: {error}",
1701 crate_dir.join("Cargo.toml").display()
1702 )))
1703 })?;
1704 let mut artifacts = Vec::new();
1705 for artifact in compiler_artifacts(stdout)? {
1706 if cargo_target.matches(&artifact.target)
1707 && same_manifest_path(artifact.manifest_path.as_std_path(), &manifest_path)
1708 {
1709 artifacts.push(artifact);
1710 }
1711 }
1712 reported_artifact_file(&artifacts, cargo_target, artifact_extension, &manifest_path)
1713}
1714
1715pub(crate) fn compiler_artifacts(
1724 stdout: &[u8],
1725) -> Result<Vec<cargo_metadata::Artifact>, RustBuildError> {
1726 #[derive(serde::Deserialize)]
1728 struct Reason {
1729 reason: String,
1730 }
1731
1732 let mut artifacts = Vec::new();
1733 for (index, line) in stdout.split(|byte| *byte == b'\n').enumerate() {
1734 let Ok(line) = str::from_utf8(line) else {
1735 continue;
1736 };
1737 let line = line.trim_end();
1738 if line.is_empty() {
1739 continue;
1740 }
1741 let malformed = |error: serde_json::Error| {
1742 RustBuildError::FailToBuildRustLibrary(io::Error::new(
1743 io::ErrorKind::InvalidData,
1744 format!(
1745 "cargo emitted a malformed `compiler-artifact` message on line {}: {error}\n{line}",
1746 index + 1
1747 ),
1748 ))
1749 };
1750 match serde_json::from_str::<Reason>(line) {
1751 Ok(Reason { reason }) if reason == "compiler-artifact" => {
1752 let artifact =
1753 serde_json::from_str::<cargo_metadata::Artifact>(line).map_err(malformed)?;
1754 artifacts.push(artifact);
1755 }
1756 Err(error) if line.contains("\"reason\":\"compiler-artifact\"") => {
1760 return Err(malformed(error));
1761 }
1762 Ok(_) | Err(_) => {}
1763 }
1764 }
1765 Ok(artifacts)
1766}
1767
1768fn reported_shared_runtime(stdout: &[u8]) -> Result<Option<PathBuf>, RustBuildError> {
1769 let mut reported = Vec::new();
1770 for artifact in compiler_artifacts(stdout)? {
1771 if artifact_package_name(&artifact.package_id) != "waterui-dylib"
1772 || !artifact
1773 .target
1774 .kind
1775 .contains(&cargo_metadata::TargetKind::DyLib)
1776 {
1777 continue;
1778 }
1779 for filename in &artifact.filenames {
1780 let path = filename.as_std_path();
1781 if is_dynamic_library(path) {
1782 reported.push((path.to_path_buf(), artifact.manifest_path.clone()));
1783 }
1784 }
1785 }
1786 match reported.as_slice() {
1787 [] => Ok(None),
1788 [(path, _)] => Ok(Some(path.clone())),
1789 _ => Err(RustBuildError::FailToBuildRustLibrary(io::Error::other(
1790 format!(
1791 "Cargo reported multiple `waterui-dylib` dynamic libraries: {}",
1792 reported
1793 .iter()
1794 .map(|(_, manifest)| manifest.as_std_path().display().to_string())
1795 .collect::<Vec<_>>()
1796 .join(", ")
1797 ),
1798 ))),
1799 }
1800}
1801
1802pub(crate) fn same_manifest_path(reported: &Path, expected: &Path) -> bool {
1808 reported == expected
1809 || dunce::canonicalize(reported).is_ok_and(|canonical| canonical == expected)
1810}
1811
1812fn reported_artifact_file(
1815 artifacts: &[cargo_metadata::Artifact],
1816 cargo_target: CargoTarget<'_>,
1817 artifact_extension: Option<&'static str>,
1818 manifest_path: &Path,
1819) -> Result<PathBuf, RustBuildError> {
1820 let what = || -> String {
1821 match cargo_target {
1822 CargoTarget::Lib => format!("the library target of {}", manifest_path.display()),
1823 CargoTarget::Binary(name) => {
1824 format!("binary `{name}` of {}", manifest_path.display())
1825 }
1826 }
1827 };
1828 let not_found = |detail: String| {
1829 RustBuildError::FailToBuildRustLibrary(io::Error::new(io::ErrorKind::NotFound, detail))
1830 };
1831
1832 let files: Vec<PathBuf> = artifacts
1833 .iter()
1834 .flat_map(|artifact| {
1835 artifact
1836 .filenames
1837 .iter()
1838 .map(|file| file.as_std_path().to_path_buf())
1839 })
1840 .collect();
1841 let artifact = match cargo_target {
1842 CargoTarget::Binary(_) => artifacts
1843 .iter()
1844 .find_map(|artifact| artifact.executable.as_ref())
1845 .map(|path| path.as_std_path().to_path_buf())
1846 .ok_or_else(|| {
1847 not_found(format!(
1848 "Cargo reported no artifact for {} (reported files: {files:?})",
1849 what()
1850 ))
1851 })?,
1852 CargoTarget::Lib => {
1853 let matching: Vec<&PathBuf> = artifact_extension.map_or_else(
1854 || files.iter().collect(),
1855 |extension| {
1856 files
1857 .iter()
1858 .filter(|file| file.extension().is_some_and(|e| *e == *extension))
1859 .collect()
1860 },
1861 );
1862 match matching.as_slice() {
1863 [only] => (*only).clone(),
1864 _ => {
1865 return Err(not_found(artifact_extension.map_or_else(
1866 || {
1867 format!(
1868 "Cargo reported {} artifacts for {} — select one with a crate-type override (reported files: {files:?})",
1869 matching.len(),
1870 what()
1871 )
1872 },
1873 |extension| {
1874 format!(
1875 "Cargo reported no `.{extension}` artifact for {} (reported files: {files:?})",
1876 what()
1877 )
1878 },
1879 )));
1880 }
1881 }
1882 }
1883 };
1884 if !artifact.is_file() {
1885 return Err(not_found(format!(
1886 "Cargo reported {} for {} but the file does not exist",
1887 artifact.display(),
1888 what()
1889 )));
1890 }
1891 Ok(artifact)
1892}
1893
1894#[derive(Debug, Clone, PartialEq, Eq)]
1896enum StaleSharedDylibReason {
1897 ForeignDepInfo { dep_info: PathBuf },
1901 MissingDepInfo { reported_files: Vec<PathBuf> },
1904}
1905
1906impl std::fmt::Display for StaleSharedDylibReason {
1907 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1908 match self {
1909 Self::ForeignDepInfo { dep_info } => write!(
1910 f,
1911 "its dep-info {} names no source under this unit's manifest root, so another source's build wrote it",
1912 dep_info.display()
1913 ),
1914 Self::MissingDepInfo { reported_files } => write!(
1915 f,
1916 "no dep-info was found beside it or in its unit directory, so nothing records which sources produced it (reported files: {reported_files:?})"
1917 ),
1918 }
1919 }
1920}
1921
1922#[derive(Debug, Clone, PartialEq, Eq)]
1925struct StaleSharedDylib {
1926 package: String,
1927 artifact: PathBuf,
1928 reason: StaleSharedDylibReason,
1929}
1930
1931fn unrecoverable_shared_dylib_error(
1936 stale: &[StaleSharedDylib],
1937 target_dir: &Path,
1938) -> RustBuildError {
1939 let units = stale.iter().fold(String::new(), |mut units, unit| {
1940 let _ = std::fmt::Write::write_fmt(
1941 &mut units,
1942 format_args!(
1943 "\n - {} ({}): {}",
1944 unit.artifact.display(),
1945 unit.package,
1946 unit.reason
1947 ),
1948 );
1949 units
1950 });
1951 let message = format!(
1952 "Cargo still reports a shared dylib unit as fresh after its package was cleaned and rebuilt:{units}\nThe shared Cargo target directory {} cannot be repaired by rebuilding; remove it with `water gc build-cache --shared-target` and build again.",
1953 target_dir.display()
1954 );
1955 RustBuildError::FailToBuildRustLibrary(io::Error::other(message))
1956}
1957
1958async fn stale_shared_dylib_packages(
1971 stdout: &[u8],
1972) -> Result<Vec<StaleSharedDylib>, RustBuildError> {
1973 let mut stale: Vec<StaleSharedDylib> = Vec::new();
1974 for artifact in compiler_artifacts(stdout)? {
1975 if !artifact.fresh {
1976 continue;
1977 }
1978 let Some(manifest_dir) = artifact.manifest_path.as_std_path().parent() else {
1979 continue;
1980 };
1981 if !uplifts_dynamic_library(&artifact.target) {
1987 continue;
1988 }
1989 let manifest_root = dunce::simplified(manifest_dir);
1990 let package = artifact_package_name(&artifact.package_id);
1991 let mut package_stale = None;
1992 for filename in &artifact.filenames {
1993 let file = filename.as_std_path();
1994 if !is_dynamic_library(file) {
1995 continue;
1996 }
1997 let Some(dep_info) = dep_info_path(file, &artifact.filenames) else {
1998 package_stale = Some(StaleSharedDylib {
1999 package: package.to_owned(),
2000 artifact: file.to_path_buf(),
2001 reason: StaleSharedDylibReason::MissingDepInfo {
2002 reported_files: artifact
2003 .filenames
2004 .iter()
2005 .map(|reported| reported.as_std_path().to_path_buf())
2006 .collect(),
2007 },
2008 });
2009 break;
2010 };
2011 let contents = smol::fs::read_to_string(&dep_info).await.map_err(|error| {
2012 RustBuildError::FailToBuildRustLibrary(io::Error::other(format!(
2013 "Cargo reported {} fresh but its dep-info {} is unreadable: {error}",
2014 file.display(),
2015 dep_info.display()
2016 )))
2017 })?;
2018 if !dep_info_prerequisites(&contents).iter().any(|source| {
2022 let source = if source.is_absolute() {
2023 source.clone()
2024 } else {
2025 manifest_dir.join(source)
2026 };
2027 dunce::simplified(&source).starts_with(manifest_root)
2028 }) {
2029 package_stale = Some(StaleSharedDylib {
2030 package: package.to_owned(),
2031 artifact: file.to_path_buf(),
2032 reason: StaleSharedDylibReason::ForeignDepInfo { dep_info },
2033 });
2034 break;
2035 }
2036 }
2037 if let Some(unit) = package_stale
2038 && !stale.iter().any(|known| known.package == unit.package)
2039 {
2040 stale.push(unit);
2041 }
2042 }
2043 stale.sort_unstable_by(|left, right| left.package.cmp(&right.package));
2044 Ok(stale)
2045}
2046
2047fn is_dynamic_library(file: &Path) -> bool {
2050 file.extension()
2051 .is_some_and(|extension| matches!(extension.to_str(), Some("so" | "dylib" | "dll")))
2052}
2053
2054fn uplifts_dynamic_library(target: &cargo_metadata::Target) -> bool {
2058 target.crate_types.iter().any(|kind| {
2059 matches!(
2060 kind,
2061 cargo_metadata::CrateType::DyLib | cargo_metadata::CrateType::CDyLib
2062 )
2063 })
2064}
2065
2066fn dep_info_path(
2082 artifact_file: &Path,
2083 sibling_files: &[cargo_metadata::camino::Utf8PathBuf],
2084) -> Option<PathBuf> {
2085 let file_stem = artifact_file.file_stem()?.to_str()?;
2086 let name = file_stem.strip_prefix("lib").unwrap_or(file_stem);
2087 let dir = artifact_file.parent()?;
2088 let mut candidates = vec![
2093 dir.join(format!("{file_stem}.d")),
2094 dir.join("deps").join(format!("{name}.d")),
2095 ];
2096 candidates.extend(
2097 sibling_files
2098 .iter()
2099 .filter_map(|sibling| sibling.as_std_path().parent())
2100 .filter(|unit_dir| *unit_dir != dir)
2101 .map(|unit_dir| unit_dir.join(format!("{name}.d"))),
2102 );
2103 candidates.push(dir.join(format!("{name}.d")));
2104 candidates.into_iter().find(|candidate| candidate.is_file())
2105}
2106
2107fn dep_info_prerequisites(contents: &str) -> Vec<PathBuf> {
2125 let mut joined = String::with_capacity(contents.len());
2128 for line in contents.lines() {
2129 if let Some(head) = line.strip_suffix('\\') {
2130 joined.push_str(head);
2131 joined.push(' ');
2132 } else {
2133 joined.push_str(line);
2134 joined.push('\n');
2135 }
2136 }
2137 let mut prerequisites = Vec::new();
2138 for line in joined.lines() {
2139 let Some((_, rest)) = line.split_once(": ") else {
2140 continue;
2141 };
2142 let mut token = String::new();
2143 let mut chars = rest.chars().peekable();
2144 while let Some(c) = chars.next() {
2145 match c {
2146 '\\' if chars.peek() == Some(&' ') => {
2147 chars.next();
2148 token.push(' ');
2149 }
2150 c if c.is_whitespace() => {
2151 if !token.is_empty() {
2152 prerequisites.push(PathBuf::from(std::mem::take(&mut token)));
2153 }
2154 }
2155 c => token.push(c),
2156 }
2157 }
2158 if !token.is_empty() {
2159 prerequisites.push(PathBuf::from(token));
2160 }
2161 }
2162 prerequisites
2163}
2164
2165fn artifact_package_name(package_id: &cargo_metadata::PackageId) -> &str {
2168 let repr = package_id.repr.as_str();
2169 let (source, fragment) = repr.rsplit_once('#').unwrap_or((repr, ""));
2170 fragment.split_once('@').map_or_else(
2171 || source.rsplit('/').next().unwrap_or(repr),
2172 |(name, _)| name,
2173 )
2174}
2175
2176async fn clean_cargo_package(
2180 crate_dir: &Path,
2181 package: &str,
2182 target_dir: &Path,
2183) -> Result<(), RustBuildError> {
2184 let mut command = Command::new("cargo");
2185 command
2186 .arg("clean")
2187 .arg("-p")
2188 .arg(package)
2189 .arg("--target-dir")
2190 .arg(target_dir)
2191 .current_dir(crate_dir);
2192 configure_generated_crate_compilation(&mut command);
2193 let output = command
2194 .output()
2195 .await
2196 .map_err(RustBuildError::FailToExecuteCargoBuild)?;
2197 if !output.status.success() {
2198 return Err(RustBuildError::FailToBuildRustLibrary(io::Error::other(
2199 format!(
2200 "cargo clean -p {package} failed:\n{}",
2201 String::from_utf8_lossy(&output.stderr)
2202 ),
2203 )));
2204 }
2205 Ok(())
2206}
2207
2208fn combined_build_output(output: &std::process::Output) -> String {
2209 let stderr = String::from_utf8_lossy(&output.stderr);
2210 let stdout = String::from_utf8_lossy(&output.stdout);
2211 if stderr.is_empty() {
2212 stdout.to_string()
2213 } else {
2214 stderr.to_string()
2215 }
2216}
2217
2218const FAILURE_TAIL_LINES: usize = 40;
2221
2222pub(crate) fn output_tail(text: &str) -> String {
2225 let lines: Vec<&str> = text.lines().collect();
2226 if lines.len() <= FAILURE_TAIL_LINES {
2227 return text.to_owned();
2228 }
2229 format!(
2230 "… {} earlier lines already streamed above …\n{}",
2231 lines.len() - FAILURE_TAIL_LINES,
2232 lines[lines.len() - FAILURE_TAIL_LINES..].join("\n")
2233 )
2234}
2235
2236fn should_auto_install_meson(build_output: &str) -> bool {
2237 let lower = build_output.to_ascii_lowercase();
2238 lower.contains("meson")
2239 && (lower.contains("not found")
2240 || lower.contains("no such file")
2241 || lower.contains("failed to execute")
2242 || lower.contains("is required"))
2243}
2244
2245fn should_retry_after_cmake_generator_mismatch(build_output: &str) -> bool {
2246 let lower = build_output.to_ascii_lowercase();
2247 lower.contains("cmake error") && lower.contains("does not match the generator used previously")
2248}
2249
2250fn remove_cmake_build_dirs_in(build_root: &Path) -> std::io::Result<usize> {
2251 if !build_root.exists() {
2252 return Ok(0);
2253 }
2254
2255 let mut removed = 0usize;
2256 for entry in std::fs::read_dir(build_root)? {
2257 let entry = entry?;
2258 let path = entry.path();
2259 if !path.is_dir() {
2260 continue;
2261 }
2262
2263 let cmake_build_dir = path.join("out").join("build");
2264 if cmake_build_dir.join("CMakeCache.txt").exists() {
2265 std::fs::remove_dir_all(cmake_build_dir)?;
2266 removed += 1;
2267 }
2268 }
2269
2270 Ok(removed)
2271}
2272
2273#[cfg(target_os = "macos")]
2274async fn ensure_meson_installed_for_build() -> Result<(), String> {
2275 use crate::toolchain::meson::Meson;
2276 use crate::toolchain::{Installation as _, Toolchain as _, ToolchainError};
2277
2278 let host = crate::toolchain::Host::current();
2279 match Meson.check(&host).await {
2280 Ok(()) => Ok(()),
2281 Err(ToolchainError::Fixable(installation)) => {
2282 installation.install(&host).await.map_err(|e| e.to_string())
2283 }
2284 Err(ToolchainError::Unfixable(e)) => Err(e.to_string()),
2285 }
2286}
2287
2288#[cfg(not(target_os = "macos"))]
2289fn ensure_meson_installed_for_build() -> impl std::future::Future<Output = Result<(), String>> {
2290 std::future::ready(Err(
2291 "automatic meson installation is only supported on macOS".to_string(),
2292 ))
2293}
2294
2295#[cfg(test)]
2296mod tests {
2297 use target_lexicon::Triple;
2298 use tempfile::tempdir;
2299
2300 use std::ffi::OsString;
2301 use std::path::PathBuf;
2302
2303 use super::{
2304 BuildOptions, BuildProfile, BuiltTarget, CargoTarget, CompileEvent, RustBuild,
2305 RustDynamicLibraries, RustLinkage, classify_compile_line, dynamic_library_file_name,
2306 lib_extension_for_triple, reported_shared_runtime, resolve_rust_standard_library_in,
2307 };
2308
2309 fn shared_runtime_artifact_json(
2310 manifest: &std::path::Path,
2311 file: &std::path::Path,
2312 package: &str,
2313 ) -> String {
2314 serde_json::json!({
2315 "reason": "compiler-artifact",
2316 "package_id": format!("path+file:///x#{package}@0.1.0"),
2317 "manifest_path": manifest,
2318 "target": {
2319 "kind": ["dylib"],
2320 "crate_types": ["dylib"],
2321 "name": package,
2322 "src_path": manifest.parent().expect("manifest dir").join("src/lib.rs"),
2323 "edition": "2021",
2324 "doc": false,
2325 "doctest": false,
2326 "test": false,
2327 },
2328 "profile": {
2329 "opt_level": "0",
2330 "debuginfo": 0,
2331 "debug_assertions": true,
2332 "overflow_checks": true,
2333 "test": false,
2334 },
2335 "features": [],
2336 "filenames": [file],
2337 "executable": null,
2338 "fresh": true,
2339 })
2340 .to_string()
2341 }
2342
2343 #[test]
2344 fn reported_shared_runtime_selects_waterui_dylib_dynamic_artifact() {
2345 let temporary = tempdir().expect("tempdir");
2346 let manifest = temporary.path().join("waterui-dylib/Cargo.toml");
2347 let runtime = temporary
2348 .path()
2349 .join("target/debug/deps/libwaterui_dylib.so");
2350 let unrelated_manifest = temporary.path().join("app/Cargo.toml");
2351 let unrelated = temporary.path().join("target/debug/app");
2352 let stdout = format!(
2353 "{}\n{}\n",
2354 shared_runtime_artifact_json(&unrelated_manifest, &unrelated, "app"),
2355 shared_runtime_artifact_json(&manifest, &runtime, "waterui-dylib"),
2356 );
2357
2358 assert_eq!(
2359 reported_shared_runtime(stdout.as_bytes()).expect("runtime report"),
2360 Some(runtime)
2361 );
2362 }
2363
2364 #[test]
2365 fn missing_shared_runtime_report_is_none_and_accessor_errors() {
2366 let temporary = tempdir().expect("tempdir");
2367 let stdout = shared_runtime_artifact_json(
2368 &temporary.path().join("app/Cargo.toml"),
2369 &temporary.path().join("target/debug/app"),
2370 "app",
2371 );
2372 assert_eq!(
2373 reported_shared_runtime(stdout.as_bytes()).expect("runtime report"),
2374 None
2375 );
2376
2377 let profile_dir = temporary.path().join("target/debug");
2378 let error = BuiltTarget {
2379 profile_dir: profile_dir.clone(),
2380 artifact: temporary.path().join("app"),
2381 shared_runtime: None,
2382 }
2383 .shared_runtime()
2384 .expect_err("missing runtime should fail");
2385 let message = error.to_string();
2386 assert!(message.contains("waterui-dylib"));
2387 assert!(message.contains(&profile_dir.display().to_string()));
2388 }
2389
2390 #[test]
2391 fn reported_shared_runtime_rejects_multiple_manifests() {
2392 let temporary = tempdir().expect("tempdir");
2393 let first_manifest = temporary.path().join("first/Cargo.toml");
2394 let second_manifest = temporary.path().join("second/Cargo.toml");
2395 let stdout = format!(
2396 "{}\n{}\n",
2397 shared_runtime_artifact_json(
2398 &first_manifest,
2399 &temporary.path().join("target/debug/libfirst.so"),
2400 "waterui-dylib",
2401 ),
2402 shared_runtime_artifact_json(
2403 &second_manifest,
2404 &temporary.path().join("target/debug/libsecond.so"),
2405 "waterui-dylib",
2406 ),
2407 );
2408
2409 let error =
2410 reported_shared_runtime(stdout.as_bytes()).expect_err("ambiguous runtime report");
2411 let message = error.to_string();
2412 assert!(message.contains(&first_manifest.display().to_string()));
2413 assert!(message.contains(&second_manifest.display().to_string()));
2414 }
2415
2416 fn triple(value: &str) -> Triple {
2417 value.parse().expect("test target triple must parse")
2418 }
2419
2420 #[test]
2421 fn crate_type_override_applies_only_to_library_targets() {
2422 assert!(CargoTarget::Lib.accepts_crate_type_override());
2423 assert!(!CargoTarget::Binary("waterui-cef-helper").accepts_crate_type_override());
2424 assert_eq!(CargoTarget::Lib.cargo_args(), ["--lib"]);
2425 assert_eq!(
2426 CargoTarget::Binary("waterui-cef-helper").cargo_args(),
2427 ["--bin", "waterui-cef-helper"]
2428 );
2429 }
2430
2431 #[test]
2432 fn build_std_envs_wire_the_wrapper_and_clear_workspace_wrappers() {
2433 use std::ffi::OsStr;
2434
2435 let dir = tempdir().expect("target dir");
2436 let toolchain = "nightly-2026-09-09-aarch64-apple-darwin";
2437 let target_dir = dir.path().join("target");
2438 let build = RustBuild::new(dir.path(), triple("aarch64-linux-android"))
2439 .with_build_std(toolchain)
2440 .with_target_dir(target_dir.clone())
2441 .with_sccache(std::path::PathBuf::from("/fake/sccache"));
2442 let mut cmd = smol::process::Command::new("cargo");
2443 smol::block_on(build.with_build_std_envs(&mut cmd, false)).expect("build-std envs apply");
2444
2445 let env = |key: &str| -> Option<Option<OsString>> {
2446 cmd.get_envs()
2447 .find(|(name, _)| *name == OsStr::new(key))
2448 .map(|(_, value)| value.map(ToOwned::to_owned))
2449 };
2450 assert_eq!(
2451 env("RUSTUP_TOOLCHAIN"),
2452 Some(Some(OsString::from(toolchain)))
2453 );
2454 assert_eq!(
2455 env("RUSTC_WRAPPER"),
2456 Some(Some(
2457 crate::toolchain::Host::current_exe()
2458 .expect("the test binary path")
2459 .into_os_string()
2460 )),
2461 "the wrapper must name this binary"
2462 );
2463 assert_eq!(
2464 env(crate::workflows::rustc_wrapper::WRAPPER_MODE_ENV),
2465 Some(Some(OsString::from("1")))
2466 );
2467 assert_eq!(
2468 env(crate::workflows::rustc_wrapper::BUILD_STD_TARGET_ENV),
2469 Some(Some(OsString::from("aarch64-linux-android")))
2470 );
2471 let expected_dylib_dir = target_dir
2472 .join("aarch64-linux-android")
2473 .join("debug")
2474 .join("deps");
2475 assert_eq!(
2476 env(crate::workflows::rustc_wrapper::BUILD_STD_DYLIB_DIR_ENV),
2477 Some(Some(expected_dylib_dir.into_os_string()))
2478 );
2479 assert_eq!(
2480 env(crate::workflows::rustc_wrapper::WRAPPER_CHAIN_ENV),
2481 Some(Some(OsString::from("/fake/sccache"))),
2482 "a configured sccache chains behind the shim"
2483 );
2484 assert_eq!(env("RUSTC_WORKSPACE_WRAPPER"), Some(None));
2487 assert_eq!(env("CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER"), Some(None));
2488 }
2489
2490 #[test]
2491 fn apple_platform_dylibs_use_macho_extension() {
2492 assert_eq!(
2493 lib_extension_for_triple(&triple("aarch64-apple-darwin")),
2494 "dylib"
2495 );
2496 assert_eq!(
2497 lib_extension_for_triple(&triple("aarch64-apple-ios-sim")),
2498 "dylib"
2499 );
2500 assert_eq!(
2501 lib_extension_for_triple(&triple("aarch64-apple-ios")),
2502 "dylib"
2503 );
2504 }
2505
2506 #[test]
2507 fn non_apple_platform_dylibs_keep_platform_extensions() {
2508 assert_eq!(
2509 lib_extension_for_triple(&triple("aarch64-linux-android")),
2510 "so"
2511 );
2512 assert_eq!(
2513 lib_extension_for_triple(&triple("x86_64-unknown-linux-gnu")),
2514 "so"
2515 );
2516 assert_eq!(
2517 lib_extension_for_triple(&triple("x86_64-pc-windows-msvc")),
2518 "dll"
2519 );
2520 }
2521
2522 #[test]
2523 fn development_and_packaging_have_distinct_linkage() {
2524 assert_eq!(
2525 BuildOptions::development(BuildProfile::Debug).linkage(),
2526 RustLinkage::SharedRuntime
2527 );
2528 assert_eq!(
2529 BuildOptions::packaging(BuildProfile::Debug).linkage(),
2530 RustLinkage::Static
2531 );
2532 assert!(BuildOptions::development(BuildProfile::Release).is_release());
2533 assert!(BuildOptions::packaging(BuildProfile::Release).is_release());
2534 }
2535
2536 #[test]
2537 fn build_profile_release_variants_select_the_release_profile() {
2538 assert!(BuildProfile::Release.is_release());
2539 assert!(BuildProfile::Profiling.is_release());
2540 assert!(!BuildProfile::Debug.is_release());
2541 assert!(!BuildProfile::Optimized.is_release());
2542 }
2543
2544 #[test]
2545 fn development_profile_envs_realize_the_selected_trade_off() {
2546 let optimized = BuildOptions::development(BuildProfile::Optimized);
2547 let envs = optimized.cargo_envs();
2548 assert!(
2549 envs.contains(&(
2550 "CARGO_PROFILE_DEV_OPT_LEVEL".to_string(),
2551 OsString::from("1")
2552 )),
2553 "optimized development lifts the dev opt-level: {envs:?}"
2554 );
2555 assert!(
2556 envs.contains(&(
2557 "CARGO_PROFILE_DEV_DEBUG_ASSERTIONS".to_string(),
2558 OsString::from("false")
2559 )),
2560 "optimized development drops dep debug assertions: {envs:?}"
2561 );
2562 assert!(
2563 envs.contains(&(
2564 "CARGO_PROFILE_DEV_DEBUG".to_string(),
2565 OsString::from("true")
2566 )),
2567 "optimized development keeps full debug info: {envs:?}"
2568 );
2569
2570 let shared_runtime_envs = [
2571 (
2572 "CARGO_PROFILE_RELEASE_PANIC".to_string(),
2573 OsString::from("unwind"),
2574 ),
2575 (
2576 "CARGO_PROFILE_RELEASE_LTO".to_string(),
2577 OsString::from("off"),
2578 ),
2579 ];
2580 for env in &shared_runtime_envs {
2581 assert!(
2582 BuildOptions::development(BuildProfile::Release)
2583 .cargo_envs()
2584 .contains(env),
2585 "a release development build links the shared runtime: missing {env:?}"
2586 );
2587 assert!(
2588 !BuildOptions::development(BuildProfile::Release)
2589 .with_static_runtime()
2590 .cargo_envs()
2591 .contains(env),
2592 "a static runtime keeps the manifest's {env:?}"
2593 );
2594 }
2595 let unwind = &shared_runtime_envs[0];
2596
2597 let profiling = BuildOptions::development(BuildProfile::Profiling);
2598 let envs = profiling.cargo_envs();
2599 assert!(
2600 envs.contains(unwind),
2601 "profiling links the shared runtime too"
2602 );
2603 for key in [
2604 "CARGO_PROFILE_RELEASE_OPT_LEVEL",
2605 "CARGO_PROFILE_RELEASE_DEBUG",
2606 "CARGO_PROFILE_RELEASE_STRIP",
2607 ] {
2608 assert!(
2609 envs.iter().any(|(env_key, _)| env_key == key),
2610 "profiling keeps debug info and symbols: missing {key} in {envs:?}"
2611 );
2612 }
2613
2614 assert!(
2615 BuildOptions::development(BuildProfile::Debug)
2616 .cargo_envs()
2617 .is_empty(),
2618 "plain debug runs the declared dev profile"
2619 );
2620 }
2621
2622 #[test]
2623 fn packaging_never_overrides_the_declared_profile() {
2624 for profile in [
2625 BuildProfile::Debug,
2626 BuildProfile::Optimized,
2627 BuildProfile::Release,
2628 BuildProfile::Profiling,
2629 ] {
2630 assert!(
2631 BuildOptions::packaging(profile).cargo_envs().is_empty(),
2632 "packaging {profile:?} must ship the declared profile"
2633 );
2634 }
2635 }
2636
2637 #[test]
2638 fn resolves_target_standard_library_without_guessing_hash() {
2639 let directory = tempdir().expect("temporary target libdir");
2640 let android_triple = triple("aarch64-linux-android");
2641 let expected = directory.path().join("libstd-1234567890abcdef.so");
2642 std::fs::write(&expected, []).expect("write test std library");
2643 std::fs::write(directory.path().join("libcore.rlib"), []).expect("write unrelated library");
2644
2645 assert_eq!(
2646 resolve_rust_standard_library_in(directory.path(), &android_triple)
2647 .expect("resolve dynamic std"),
2648 expected
2649 );
2650 assert_eq!(
2651 dynamic_library_file_name("waterui_dylib", &android_triple),
2652 "libwaterui_dylib.so"
2653 );
2654 assert_eq!(
2655 dynamic_library_file_name("waterui_dylib", &triple("x86_64-pc-windows-msvc")),
2656 "waterui_dylib.dll"
2657 );
2658 }
2659
2660 #[test]
2661 fn compile_progress_classifies_cargo_unit_lines() {
2662 assert_eq!(
2663 classify_compile_line(" Compiling serde v1.0.228"),
2664 CompileEvent::Unit {
2665 phase: "Compiling",
2666 name: "serde".to_string(),
2667 version: Some("1.0.228".to_string()),
2668 }
2669 );
2670 assert_eq!(
2671 classify_compile_line(" Compiling waterui-app v0.1.0 (/tmp/app)"),
2672 CompileEvent::Unit {
2673 phase: "Compiling",
2674 name: "waterui-app".to_string(),
2675 version: Some("0.1.0".to_string()),
2676 }
2677 );
2678 assert_eq!(
2679 classify_compile_line(" Checking libc v0.2.171"),
2680 CompileEvent::Unit {
2681 phase: "Checking",
2682 name: "libc".to_string(),
2683 version: Some("0.2.171".to_string()),
2684 }
2685 );
2686 }
2687
2688 #[test]
2689 fn compile_progress_keeps_non_unit_lines_verbatim() {
2690 assert_eq!(
2691 classify_compile_line(" Compiling 12 crates"),
2692 CompileEvent::Line("Compiling 12 crates".to_string())
2693 );
2694 assert_eq!(
2695 classify_compile_line(" Downloaded 300 crates (5.2 MB) in 1.23s"),
2696 CompileEvent::Line("Downloaded 300 crates (5.2 MB) in 1.23s".to_string())
2697 );
2698 assert_eq!(
2699 classify_compile_line(
2700 " Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.23s"
2701 ),
2702 CompileEvent::Finished(
2703 "Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.23s".to_string()
2704 )
2705 );
2706 assert_eq!(
2707 classify_compile_line("warning: unused import"),
2708 CompileEvent::Line("warning: unused import".to_string())
2709 );
2710 }
2711
2712 #[test]
2713 fn compile_progress_classifies_through_ansi_color() {
2714 let colored = "\u{1b}[0m\u{1b}[1m\u{1b}[32m Compiling\u{1b}[0m serde v1.0.228";
2717 assert_eq!(
2718 classify_compile_line(colored),
2719 CompileEvent::Unit {
2720 phase: "Compiling",
2721 name: "serde".to_string(),
2722 version: Some("1.0.228".to_string()),
2723 }
2724 );
2725 let colored_finished =
2726 "\u{1b}[0m\u{1b}[1m\u{1b}[32m Finished\u{1b}[0m `dev` profile in 1.23s";
2727 assert_eq!(
2728 classify_compile_line(colored_finished),
2729 CompileEvent::Finished(colored_finished.trim().to_string())
2730 );
2731 }
2732
2733 #[test]
2739 fn same_named_projects_resolve_their_own_artifacts_in_one_shared_target() {
2740 use crate::project_model::project_types::{CrateName, generated_crate_name};
2741
2742 smol::block_on(async {
2743 let temporary = tempdir().expect("tempdir");
2744 let shared_target = temporary.path().join("shared-target");
2745 let demo = CrateName::try_from("demo").expect("crate name");
2746 let mut artifacts = Vec::new();
2747 for (directory, marker) in [("first", "first"), ("second", "second")] {
2748 let project_root = temporary.path().join(directory);
2749 let crate_dir = project_root.join("hydrolysis");
2750 std::fs::create_dir_all(crate_dir.join("src")).expect("crate dir");
2751 let package = generated_crate_name(&demo, "hydrolysis", &project_root);
2752 std::fs::write(
2753 crate_dir.join("Cargo.toml"),
2754 format!(
2755 "[package]\nname = \"{package}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n"
2756 ),
2757 )
2758 .expect("manifest");
2759 std::fs::write(
2760 crate_dir.join("src/main.rs"),
2761 format!("fn main() {{ println!(\"{marker}\"); }}\n"),
2762 )
2763 .expect("main.rs");
2764
2765 let artifact = super::RustBuild::new(&crate_dir, Triple::host())
2766 .with_target_dir(&shared_target)
2767 .build_binary(package.as_str(), false)
2768 .await
2769 .expect("the generated crate builds")
2770 .artifact;
2771 assert!(artifact.is_file(), "the reported artifact exists");
2772 artifacts.push(artifact);
2773 }
2774
2775 assert_ne!(
2776 artifacts[0], artifacts[1],
2777 "each same-named project resolves its own artifact"
2778 );
2779 for (artifact, marker) in artifacts.iter().zip(["first", "second"]) {
2780 let ran = std::process::Command::new(artifact)
2781 .output()
2782 .expect("the resolved artifact executes");
2783 assert_eq!(
2784 String::from_utf8_lossy(&ran.stdout).trim(),
2785 marker,
2786 "the artifact is this project's binary, not the sibling's"
2787 );
2788 }
2789 });
2790 }
2791
2792 #[test]
2797 fn reported_artifact_selects_the_matching_manifests_file() {
2798 let temporary = tempdir().expect("tempdir");
2799 let crate_dir = temporary.path().join("demo-hydrolysis-deadbeef");
2800 std::fs::create_dir_all(&crate_dir).expect("crate dir");
2801 std::fs::write(crate_dir.join("Cargo.toml"), "[package]\n").expect("manifest");
2802 let manifest =
2803 dunce::canonicalize(crate_dir.join("Cargo.toml")).expect("canonical manifest");
2804 let reported = crate_dir.join("target/debug/deps/demo_hydrolysis_deadbeef-abc123.rlib");
2805 std::fs::create_dir_all(reported.parent().expect("deps dir")).expect("deps dir");
2806 std::fs::write(&reported, []).expect("reported artifact");
2807
2808 let artifact_json = |manifest: &std::path::Path, file: &std::path::Path, name: &str| {
2812 serde_json::json!({
2813 "reason": "compiler-artifact",
2814 "package_id": format!("path+file:///x#{name}@0.1.0"),
2815 "manifest_path": manifest,
2816 "target": {
2817 "kind": ["lib"],
2818 "crate_types": ["lib"],
2819 "name": name,
2820 "src_path": manifest.parent().expect("manifest dir").join("src/lib.rs"),
2821 "edition": "2021",
2822 "doc": true,
2823 "doctest": true,
2824 "test": true,
2825 },
2826 "profile": {
2827 "opt_level": "0",
2828 "debuginfo": 0,
2829 "debug_assertions": true,
2830 "overflow_checks": true,
2831 "test": false,
2832 },
2833 "features": [],
2834 "filenames": [file],
2835 "executable": null,
2836 "fresh": true,
2837 })
2838 .to_string()
2839 };
2840
2841 let other_manifest = temporary.path().join("other").join("Cargo.toml");
2842 let other_file = temporary.path().join("other.rlib");
2843 let stdout = format!(
2844 "{}\n{}\n",
2845 artifact_json(&other_manifest, &other_file, "other"),
2846 artifact_json(&manifest, &reported, "demo_hydrolysis_deadbeef"),
2847 );
2848 let resolved = super::reported_artifact(
2849 stdout.as_bytes(),
2850 &crate_dir,
2851 CargoTarget::Lib,
2852 Some("rlib"),
2853 )
2854 .expect("the matching manifest's artifact resolves");
2855 assert_eq!(resolved, reported);
2856
2857 let foreign_only = artifact_json(&other_manifest, &other_file, "other");
2858 assert!(
2859 super::reported_artifact(
2860 foreign_only.as_bytes(),
2861 &crate_dir,
2862 CargoTarget::Lib,
2863 Some("rlib"),
2864 )
2865 .is_err(),
2866 "an artifact for another manifest is never selected"
2867 );
2868 }
2869
2870 #[test]
2875 fn stale_shared_dylib_packages_flags_a_foreign_written_artifact() {
2876 smol::block_on(async {
2877 let temporary = tempdir().expect("tempdir");
2878 let deps = temporary.path().join("debug/deps");
2879 std::fs::create_dir_all(&deps).expect("deps dir");
2880 let dylib = deps.join("libwaterui_dylib.so");
2881 std::fs::write(&dylib, []).expect("dylib");
2882
2883 let ours = temporary.path().join("our project");
2887 std::fs::create_dir_all(ours.join("src")).expect("our manifest dir");
2888 let manifest = ours.join("Cargo.toml");
2889 std::fs::write(&manifest, "").expect("manifest");
2890 let own_source = ours.join("src/lib.rs");
2891 std::fs::write(&own_source, "").expect("own source");
2892
2893 let artifact = |fresh: bool| {
2894 serde_json::json!({
2895 "reason": "compiler-artifact",
2896 "package_id": "path+file:///x#waterui-dylib@0.1.0",
2897 "manifest_path": manifest,
2898 "target": {
2899 "kind": ["lib"],
2900 "crate_types": ["dylib"],
2901 "name": "waterui_dylib",
2902 "src_path": own_source,
2903 "edition": "2021",
2904 "doc": true,
2905 "doctest": true,
2906 "test": true,
2907 },
2908 "profile": {
2909 "opt_level": "0",
2910 "debuginfo": 0,
2911 "debug_assertions": true,
2912 "overflow_checks": true,
2913 "test": false,
2914 },
2915 "features": [],
2916 "filenames": [dylib],
2917 "executable": null,
2918 "fresh": fresh,
2919 })
2920 .to_string()
2921 };
2922 let dep_info = deps.join("waterui_dylib.d");
2923
2924 let foreign = temporary.path().join("foreign");
2928 std::fs::create_dir_all(foreign.join("src")).expect("foreign source dir");
2929 let foreign_source = foreign.join("src/lib.rs");
2930 std::fs::write(&foreign_source, "").expect("foreign source");
2931 let dep_escape =
2932 |path: &std::path::Path| path.display().to_string().replace(' ', "\\ ");
2933 let write_dep_info = |source: &std::path::Path| {
2934 std::fs::write(
2935 &dep_info,
2936 format!("{}: {}\n", dep_escape(&dylib), dep_escape(source)),
2937 )
2938 .expect("dep-info");
2939 };
2940
2941 write_dep_info(&foreign_source);
2943 let stale = super::stale_shared_dylib_packages(artifact(true).as_bytes())
2944 .await
2945 .expect("scan");
2946 assert_eq!(
2947 stale,
2948 [super::StaleSharedDylib {
2949 package: "waterui-dylib".to_owned(),
2950 artifact: dylib.clone(),
2951 reason: super::StaleSharedDylibReason::ForeignDepInfo {
2952 dep_info: dep_info.clone(),
2953 },
2954 }]
2955 );
2956
2957 write_dep_info(&own_source);
2959 let stale = super::stale_shared_dylib_packages(artifact(true).as_bytes())
2960 .await
2961 .expect("scan");
2962 assert!(stale.is_empty(), "our own artifact is never stale");
2963
2964 write_dep_info(&foreign_source);
2966 let stale = super::stale_shared_dylib_packages(artifact(false).as_bytes())
2967 .await
2968 .expect("scan");
2969 assert!(stale.is_empty(), "a non-fresh unit wrote the file itself");
2970 });
2971 }
2972
2973 #[test]
2979 fn stale_check_reads_build_dir_dep_info_and_skips_proc_macros() {
2980 smol::block_on(async {
2981 let temporary = tempdir().expect("tempdir");
2982 let profile = temporary.path().join("debug");
2983 let unit_dir = profile.join("build/waterui-dylib/0123456789abcdef/out");
2984 std::fs::create_dir_all(&unit_dir).expect("unit dir");
2985 let dylib = profile.join("libwaterui_dylib.so");
2986 std::fs::write(&dylib, []).expect("dylib");
2987 let rmeta = unit_dir.join("libwaterui_dylib.rmeta");
2988 std::fs::write(&rmeta, []).expect("rmeta");
2989
2990 let ours = temporary.path().join("ours");
2991 std::fs::create_dir_all(ours.join("src")).expect("our manifest dir");
2992 let manifest = ours.join("Cargo.toml");
2993 std::fs::write(&manifest, "").expect("manifest");
2994 let foreign = temporary.path().join("foreign/src/lib.rs");
2995 std::fs::create_dir_all(foreign.parent().expect("parent")).expect("foreign dir");
2996 std::fs::write(&foreign, []).expect("foreign source");
2997 std::fs::write(
2998 unit_dir.join("waterui_dylib.d"),
2999 format!("{}: {}\n", dylib.display(), foreign.display()),
3000 )
3001 .expect("dep-info");
3002
3003 let unit = |name: &str, crate_type: &str, filenames: Vec<&std::path::Path>| {
3004 serde_json::json!({
3005 "reason": "compiler-artifact",
3006 "package_id": format!("path+file:///x#{name}@0.1.0"),
3007 "manifest_path": manifest,
3008 "target": {
3009 "kind": [if crate_type == "proc-macro" { "proc-macro" } else { "lib" }],
3010 "crate_types": [crate_type],
3011 "name": name.replace('-', "_"),
3012 "src_path": ours.join("src/lib.rs"),
3013 "edition": "2021",
3014 "doc": true,
3015 "doctest": true,
3016 "test": true,
3017 },
3018 "profile": {
3019 "opt_level": "0",
3020 "debuginfo": 0,
3021 "debug_assertions": true,
3022 "overflow_checks": true,
3023 "test": false,
3024 },
3025 "features": [],
3026 "filenames": filenames,
3027 "executable": null,
3028 "fresh": true,
3029 })
3030 .to_string()
3031 };
3032 let macro_dylib = unit_dir.join("libthiserror_impl-0123456789abcdef.so");
3036 let stdout = format!(
3037 "{}\n{}\n",
3038 unit("thiserror-impl", "proc-macro", vec![¯o_dylib]),
3039 unit("waterui-dylib", "dylib", vec![&dylib, &rmeta]),
3040 );
3041 let stale = super::stale_shared_dylib_packages(stdout.as_bytes())
3042 .await
3043 .expect("scan");
3044 assert_eq!(stale.len(), 1, "{stale:?}");
3045 assert_eq!(stale[0].package, "waterui-dylib");
3046 assert!(
3047 matches!(
3048 stale[0].reason,
3049 super::StaleSharedDylibReason::ForeignDepInfo { .. }
3050 ),
3051 "{:?}",
3052 stale[0].reason
3053 );
3054 });
3055 }
3056
3057 #[test]
3062 fn fresh_uplifted_dylib_without_dep_info_is_recovered_not_reported() {
3063 smol::block_on(async {
3064 let temporary = tempdir().expect("tempdir");
3065 let profile = temporary
3066 .path()
3067 .join("target/shared/x86_64-pc-windows-msvc/debug");
3068 let deps = profile.join("deps");
3069 std::fs::create_dir_all(&deps).expect("deps dir");
3070 let dylib = profile.join("waterui_dylib.dll");
3071 std::fs::write(&dylib, []).expect("dylib");
3072 let import_lib = profile.join("waterui_dylib.dll.lib");
3073 std::fs::write(&import_lib, []).expect("import lib");
3074
3075 let ours = temporary.path().join("ours");
3076 std::fs::create_dir_all(ours.join("src")).expect("our manifest dir");
3077 let manifest = ours.join("Cargo.toml");
3078 std::fs::write(&manifest, "").expect("manifest");
3079
3080 let stdout = serde_json::json!({
3081 "reason": "compiler-artifact",
3082 "package_id": "path+file:///x#waterui-dylib@0.1.0",
3083 "manifest_path": manifest,
3084 "target": {
3085 "kind": ["lib"],
3086 "crate_types": ["dylib"],
3087 "name": "waterui_dylib",
3088 "src_path": ours.join("src/lib.rs"),
3089 "edition": "2021",
3090 "doc": true,
3091 "doctest": true,
3092 "test": true,
3093 },
3094 "profile": {
3095 "opt_level": "0",
3096 "debuginfo": 0,
3097 "debug_assertions": true,
3098 "overflow_checks": true,
3099 "test": false,
3100 },
3101 "features": [],
3102 "filenames": [dylib, import_lib],
3103 "executable": null,
3104 "fresh": true,
3105 })
3106 .to_string();
3107
3108 let stale = super::stale_shared_dylib_packages(stdout.as_bytes())
3109 .await
3110 .expect("a fresh dylib without dep-info is recovered, not reported");
3111 assert_eq!(
3112 stale,
3113 [super::StaleSharedDylib {
3114 package: "waterui-dylib".to_owned(),
3115 artifact: dylib.clone(),
3116 reason: super::StaleSharedDylibReason::MissingDepInfo {
3117 reported_files: vec![dylib.clone(), import_lib.clone()],
3118 },
3119 }]
3120 );
3121 let reason = stale[0].reason.to_string();
3122 assert!(reason.contains("no dep-info was found"), "{reason}");
3123
3124 let target_dir = temporary.path().join("target/shared");
3128 let error = super::unrecoverable_shared_dylib_error(&stale, &target_dir).to_string();
3129 assert!(
3130 error.contains("after its package was cleaned and rebuilt"),
3131 "{error}"
3132 );
3133 assert!(error.contains("waterui-dylib"), "{error}");
3134 assert!(error.contains(&dylib.display().to_string()), "{error}");
3135 assert!(error.contains(&target_dir.display().to_string()), "{error}");
3136 });
3137 }
3138
3139 #[test]
3144 fn dep_info_prerequisites_unescape_spaces_and_join_continued_rules() {
3145 let contents = concat!(
3146 "C:\\out\\app.dll: C:\\work\\my\\ app\\src\\lib.rs \\\n",
3147 " C:\\work\\my\\ app\\build.rs C:\\work\\cost$$.rs\n",
3148 "\n",
3149 "C:\\work\\my\\ app\\src\\lib.rs:\n",
3150 );
3151 assert_eq!(
3152 super::dep_info_prerequisites(contents),
3153 vec![
3154 PathBuf::from("C:\\work\\my app\\src\\lib.rs"),
3155 PathBuf::from("C:\\work\\my app\\build.rs"),
3156 PathBuf::from("C:\\work\\cost$$.rs"),
3157 ]
3158 );
3159 }
3160
3161 #[test]
3162 fn static_packaging_removes_only_staged_android_runtime_libraries() {
3163 smol::block_on(async {
3164 let directory = tempdir().expect("temporary Android runtime directory");
3165 let android_triple = triple("aarch64-linux-android");
3166 for file_name in [
3167 "libwaterui_dylib.so",
3168 "libstd-old.so",
3169 "libwaterui_app.so",
3170 "libc++_shared.so",
3171 ] {
3172 std::fs::write(directory.path().join(file_name), [])
3173 .expect("write staged runtime test file");
3174 }
3175
3176 RustDynamicLibraries::remove_staged(directory.path(), &android_triple)
3177 .await
3178 .expect("remove shared Rust runtime libraries");
3179
3180 assert!(!directory.path().join("libwaterui_dylib.so").exists());
3181 assert!(!directory.path().join("libstd-old.so").exists());
3182 assert!(directory.path().join("libwaterui_app.so").exists());
3183 assert!(directory.path().join("libc++_shared.so").exists());
3184 });
3185 }
3186}