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 rust_target_libdir(triple: &Triple) -> eyre::Result<PathBuf> {
39 let target = triple.to_string();
40 let output = run_command(
41 "rustc",
42 ["--print", "target-libdir", "--target", target.as_str()],
43 )
44 .await?;
45 let libdir = output.trim();
46 if libdir.is_empty() {
47 bail!("`rustc --print target-libdir --target {target}` returned an empty path");
48 }
49 let path = PathBuf::from(libdir);
50 if !path.is_dir() {
51 bail!(
52 "Rust target libdir does not exist for dynamic linking: {}",
53 path.display()
54 );
55 }
56 Ok(path)
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub(crate) enum CargoTarget<'a> {
66 Lib,
68 Binary(&'a str),
70}
71
72impl<'a> CargoTarget<'a> {
73 fn cargo_args(self) -> Vec<&'a str> {
74 match self {
75 Self::Lib => vec!["--lib"],
76 Self::Binary(name) => vec!["--bin", name],
77 }
78 }
79
80 const fn accepts_crate_type_override(self) -> bool {
81 matches!(self, Self::Lib)
82 }
83
84 fn matches(&self, target: &cargo_metadata::Target) -> bool {
87 use cargo_metadata::TargetKind;
88 match self {
89 Self::Binary(name) => {
90 target.name.as_str() == *name && target.kind.contains(&TargetKind::Bin)
91 }
92 Self::Lib => target.kind.iter().any(|kind| {
93 matches!(
94 kind,
95 TargetKind::Lib
96 | TargetKind::RLib
97 | TargetKind::DyLib
98 | TargetKind::CDyLib
99 | TargetKind::StaticLib
100 | TargetKind::ProcMacro
101 )
102 }),
103 }
104 }
105}
106
107#[derive(Debug)]
110pub struct BuiltTarget {
111 pub profile_dir: PathBuf,
114 pub artifact: PathBuf,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum RustLinkage {
123 Static,
125 SharedRuntime,
127}
128
129pub fn configure_generated_crate_compilation(command: &mut Command) {
145 command.env("CARGO_INCREMENTAL", "0");
146}
147
148#[derive(Debug, Clone, PartialEq, Eq)]
150pub struct RustDynamicLibraries {
151 waterui: PathBuf,
152 standard_library: PathBuf,
153 triple: Triple,
154}
155
156impl RustDynamicLibraries {
157 pub async fn resolve(lib_dir: &Path, triple: &Triple) -> eyre::Result<Self> {
162 let file_name = dynamic_library_file_name("waterui_dylib", triple);
163 let waterui = [
168 lib_dir.join("deps").join(&file_name),
169 lib_dir.join(&file_name),
170 ]
171 .into_iter()
172 .find(|path| path.is_file())
173 .ok_or_else(|| {
174 eyre::eyre!(
175 "Shared WaterUI runtime was not built at {}",
176 lib_dir.join("deps").join(&file_name).display()
177 )
178 })?;
179
180 let resolution_triple = triple.clone();
186 let deps_dir = lib_dir.join("deps");
187 let staged =
188 unblock(move || resolve_rust_standard_library_in(&deps_dir, &resolution_triple)).await;
189 let standard_library = match staged {
190 Ok(path) => path,
191 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
192 let target_libdir = rust_target_libdir(triple).await?;
193 let resolution_triple = triple.clone();
194 unblock(move || {
195 resolve_rust_standard_library_in(&target_libdir, &resolution_triple)
196 })
197 .await?
198 }
199 Err(error) => return Err(error.into()),
200 };
201
202 Ok(Self {
203 waterui,
204 standard_library,
205 triple: triple.clone(),
206 })
207 }
208
209 #[must_use]
211 pub fn waterui(&self) -> &Path {
212 &self.waterui
213 }
214
215 #[must_use]
217 pub fn standard_library(&self) -> &Path {
218 &self.standard_library
219 }
220
221 pub fn iter(&self) -> impl Iterator<Item = &Path> {
223 [self.waterui(), self.standard_library()].into_iter()
224 }
225
226 pub async fn stage(&self, destination: &Path) -> eyre::Result<()> {
237 smol::fs::create_dir_all(destination).await?;
238 let sources: Vec<PathBuf> = self.iter().map(|path| (*path).to_path_buf()).collect();
243 Self::remove_staged_except(destination, &self.triple, &sources).await?;
244 for source in &sources {
245 let file_name = source.file_name().ok_or_else(|| {
246 eyre::eyre!(
247 "Dynamic library path has no file name: {}",
248 source.display()
249 )
250 })?;
251 let staged = destination.join(file_name);
252 if *source == staged {
253 continue;
254 }
255 crate::utils::copy_file(source, &staged)
256 .await
257 .wrap_err_with(|| {
258 format!(
259 "Failed to stage {} to {}",
260 source.display(),
261 staged.display()
262 )
263 })?;
264 }
265 Ok(())
266 }
267
268 pub async fn remove_staged(destination: &Path, triple: &Triple) -> eyre::Result<()> {
273 Self::remove_staged_except(destination, triple, &[]).await
274 }
275
276 async fn remove_staged_except(
280 destination: &Path,
281 triple: &Triple,
282 keep: &[PathBuf],
283 ) -> eyre::Result<()> {
284 if !destination.is_dir() {
285 return Ok(());
286 }
287
288 let waterui = dynamic_library_file_name("waterui_dylib", triple);
289 let (standard_library_prefix, extension) =
290 if triple.operating_system == OperatingSystem::Windows {
291 ("std-", "dll")
292 } else {
293 ("libstd-", lib_extension_for_triple(triple))
294 };
295 let mut entries = smol::fs::read_dir(destination).await?;
296 while let Some(entry) = entries.next().await {
297 let entry = entry?;
298 if keep.contains(&entry.path()) {
299 continue;
300 }
301 let file_name = entry.file_name();
302 let file_name = file_name.to_string_lossy();
303 if file_name == waterui
304 || (file_name.starts_with(standard_library_prefix)
305 && entry.path().extension().and_then(|value| value.to_str()) == Some(extension))
306 {
307 smol::fs::remove_file(entry.path()).await?;
308 }
309 }
310 Ok(())
311 }
312}
313
314fn dynamic_library_file_name(crate_name: &str, triple: &Triple) -> String {
315 if triple.operating_system == OperatingSystem::Windows {
316 format!("{crate_name}.dll")
317 } else {
318 format!("lib{crate_name}.{}", lib_extension_for_triple(triple))
319 }
320}
321
322fn resolve_rust_standard_library_in(libdir: &Path, triple: &Triple) -> std::io::Result<PathBuf> {
328 let (prefix, extension) = if triple.operating_system == OperatingSystem::Windows {
329 ("std-", "dll")
330 } else {
331 ("libstd-", lib_extension_for_triple(triple))
332 };
333 let entries = match std::fs::read_dir(libdir) {
334 Ok(entries) => entries,
335 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
336 return Err(std::io::Error::new(
337 std::io::ErrorKind::NotFound,
338 format!("{} does not exist", libdir.display()),
339 ));
340 }
341 Err(error) => return Err(error),
342 };
343 let mut matches = entries
344 .filter_map(|entry| entry.ok().map(|entry| entry.path()))
345 .filter(|path| {
346 path.file_name()
347 .and_then(|name| name.to_str())
348 .is_some_and(|name| {
349 name.starts_with(prefix)
350 && path.extension().and_then(|extension| extension.to_str())
351 == Some(extension)
352 })
353 })
354 .collect::<Vec<_>>();
355 matches.sort_unstable();
356 match matches.as_slice() {
357 [path] => Ok(path.clone()),
358 [] => Err(std::io::Error::new(
359 std::io::ErrorKind::NotFound,
360 format!(
361 "Rust target libdir {} contains no dynamic standard library for {triple}",
362 libdir.display()
363 ),
364 )),
365 _ => Err(std::io::Error::other(format!(
366 "Rust target libdir {} contains multiple dynamic standard libraries for {triple}: {}",
367 libdir.display(),
368 matches
369 .iter()
370 .map(|path| path.display().to_string())
371 .collect::<Vec<_>>()
372 .join(", ")
373 ))),
374 }
375}
376
377#[derive(Debug, Clone)]
379pub struct RustBuild {
380 path: PathBuf,
381 triple: Triple,
382 project: Option<Project>,
383 target_dir: Option<PathBuf>,
385 sccache_path: Option<PathBuf>,
387 features: Vec<String>,
389 crate_type_override: Option<String>,
391 rustc_flags: Vec<String>,
393 final_rustc_args: Vec<String>,
402 build_std_toolchain: Option<String>,
411 envs: Vec<(String, OsString)>,
413 progress: Option<BuildProgress>,
415}
416
417#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
423pub enum BuildProfile {
424 #[default]
426 Debug,
427 Optimized,
431 Release,
433 Profiling,
436}
437
438impl BuildProfile {
439 #[must_use]
442 pub const fn is_release(self) -> bool {
443 matches!(self, Self::Release | Self::Profiling)
444 }
445
446 #[must_use]
449 pub const fn is_development(self) -> bool {
450 !self.is_release()
451 }
452
453 fn development_envs(self) -> Vec<(String, OsString)> {
463 let entries: &[(&str, &str)] = match self {
464 Self::Debug => &[],
465 Self::Optimized => &[
466 ("CARGO_PROFILE_DEV_OPT_LEVEL", "1"),
467 ("CARGO_PROFILE_DEV_DEBUG", "true"),
468 ("CARGO_PROFILE_DEV_DEBUG_ASSERTIONS", "false"),
469 ("CARGO_PROFILE_DEV_OVERFLOW_CHECKS", "false"),
470 ],
471 Self::Release => &[("CARGO_PROFILE_RELEASE_OPT_LEVEL", "3")],
472 Self::Profiling => &[
473 ("CARGO_PROFILE_RELEASE_OPT_LEVEL", "3"),
474 ("CARGO_PROFILE_RELEASE_DEBUG", "true"),
475 ("CARGO_PROFILE_RELEASE_STRIP", "none"),
476 ],
477 };
478 entries
479 .iter()
480 .map(|(key, value)| ((*key).to_string(), OsString::from(*value)))
481 .collect()
482 }
483}
484
485#[derive(Debug, Clone)]
487pub struct BuildOptions {
488 profile: BuildProfile,
489 output_dir: Option<std::path::PathBuf>,
490 sccache_path: Option<std::path::PathBuf>,
492 target_triple: Option<Triple>,
494 linkage: RustLinkage,
496 dynamic_module_loading: bool,
501 dev_server: bool,
504 cargo_envs: Vec<(String, OsString)>,
506 progress: Option<BuildProgress>,
508}
509
510impl BuildOptions {
511 #[must_use]
518 pub fn development(profile: BuildProfile) -> Self {
519 Self {
520 profile,
521 output_dir: None,
522 sccache_path: None,
523 target_triple: None,
524 linkage: RustLinkage::SharedRuntime,
525 dynamic_module_loading: false,
526 dev_server: false,
527 cargo_envs: profile.development_envs(),
528 progress: None,
529 }
530 }
531
532 #[must_use]
538 pub const fn with_static_runtime(mut self) -> Self {
539 self.linkage = RustLinkage::Static;
540 self
541 }
542
543 #[must_use]
549 pub const fn packaging(profile: BuildProfile) -> Self {
550 Self {
551 profile,
552 output_dir: None,
553 sccache_path: None,
554 target_triple: None,
555 linkage: RustLinkage::Static,
556 dynamic_module_loading: false,
557 dev_server: false,
558 cargo_envs: Vec::new(),
559 progress: None,
560 }
561 }
562
563 #[must_use]
565 pub const fn is_release(&self) -> bool {
566 self.profile.is_release()
567 }
568
569 #[must_use]
571 pub const fn profile(&self) -> BuildProfile {
572 self.profile
573 }
574
575 #[must_use]
577 pub fn cargo_envs(&self) -> &[(String, OsString)] {
578 &self.cargo_envs
579 }
580
581 #[must_use]
583 pub const fn with_dev_server(mut self, dev_server: bool) -> Self {
584 self.dev_server = dev_server;
585 self
586 }
587
588 #[must_use]
590 pub const fn uses_dev_server(&self) -> bool {
591 self.dev_server
592 }
593
594 #[must_use]
596 pub fn output_dir(&self) -> Option<&std::path::Path> {
597 self.output_dir.as_deref()
598 }
599
600 #[must_use]
602 pub fn with_output_dir(mut self, output_dir: impl Into<std::path::PathBuf>) -> Self {
603 self.output_dir = Some(output_dir.into());
604 self
605 }
606
607 #[must_use]
609 pub fn sccache_path(&self) -> Option<&std::path::Path> {
610 self.sccache_path.as_deref()
611 }
612
613 #[must_use]
618 pub fn with_sccache(mut self, sccache_path: impl Into<std::path::PathBuf>) -> Self {
619 self.sccache_path = Some(sccache_path.into());
620 self
621 }
622
623 #[must_use]
625 pub const fn target_triple(&self) -> Option<&Triple> {
626 self.target_triple.as_ref()
627 }
628
629 #[must_use]
631 pub fn with_target_triple(mut self, target_triple: Triple) -> Self {
632 self.target_triple = Some(target_triple);
633 self
634 }
635
636 #[must_use]
638 pub const fn linkage(&self) -> RustLinkage {
639 self.linkage
640 }
641
642 #[must_use]
648 pub const fn with_dynamic_module_loading(mut self) -> Self {
649 self.dynamic_module_loading = true;
650 self
651 }
652
653 #[must_use]
655 pub const fn loads_dynamic_modules(&self) -> bool {
656 self.dynamic_module_loading
657 }
658
659 #[must_use]
662 pub fn with_progress(mut self, progress: BuildProgress) -> Self {
663 self.progress = Some(progress);
664 self
665 }
666
667 #[must_use]
669 pub const fn progress(&self) -> Option<&BuildProgress> {
670 self.progress.as_ref()
671 }
672}
673
674#[derive(Debug, thiserror::Error)]
676pub enum RustBuildError {
677 #[error("Failed to execute cargo build: {0}")]
679 FailToExecuteCargoBuild(std::io::Error),
680
681 #[error("Failed to build Rust library: {0}")]
683 FailToBuildRustLibrary(std::io::Error),
684}
685
686#[derive(Debug, Clone, PartialEq, Eq)]
694pub enum CompileEvent {
695 Unit {
699 phase: &'static str,
701 name: String,
703 version: Option<String>,
705 },
706 Finished(String),
708 Line(String),
711}
712
713#[derive(Clone)]
718pub struct BuildProgress {
719 report: std::sync::Arc<dyn Fn(CompileEvent) + Send + Sync>,
720 shows_all_lines: bool,
724}
725
726impl std::fmt::Debug for BuildProgress {
727 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
728 formatter.write_str("BuildProgress(..)")
729 }
730}
731
732impl BuildProgress {
733 #[must_use]
735 pub fn new(report: impl Fn(CompileEvent) + Send + Sync + 'static) -> Self {
736 Self {
737 report: std::sync::Arc::new(report),
738 shows_all_lines: false,
739 }
740 }
741
742 #[must_use]
745 pub const fn showing_all_lines(mut self) -> Self {
746 self.shows_all_lines = true;
747 self
748 }
749
750 #[must_use]
752 pub const fn shows_all_lines(&self) -> bool {
753 self.shows_all_lines
754 }
755
756 fn report(&self, event: CompileEvent) {
757 (self.report)(event);
758 }
759}
760
761const CARGO_UNIT_PHASES: &[&str] = &[
763 "Compiling",
764 "Checking",
765 "Fresh",
766 "Downloading",
767 "Downloaded",
768 "Doc-tests",
769];
770
771fn classify_compile_line(line: &str) -> CompileEvent {
779 let raw = line.trim();
780 let stripped = console::strip_ansi_codes(raw);
781 let text = stripped.trim();
782 for phase in CARGO_UNIT_PHASES {
783 let Some(rest) = text
784 .strip_prefix(phase)
785 .and_then(|rest| rest.strip_prefix(' '))
786 else {
787 continue;
788 };
789 let Some((name, version)) = rest.split_once(" v") else {
792 return CompileEvent::Line(raw.to_owned());
793 };
794 let version = version.split([' ', '(']).next().unwrap_or_default();
795 return CompileEvent::Unit {
796 phase,
797 name: name.to_owned(),
798 version: (!version.is_empty()).then(|| version.to_owned()),
799 };
800 }
801 if text.starts_with("Finished ") {
802 return CompileEvent::Finished(raw.to_owned());
803 }
804 CompileEvent::Line(raw.to_owned())
805}
806
807pub(crate) async fn command_output_with_progress(
819 command: &mut Command,
820 progress: Option<BuildProgress>,
821) -> io::Result<std::process::Output> {
822 let mut child = command
823 .kill_on_drop(true)
824 .stdin(Stdio::null())
825 .stdout(Stdio::piped())
826 .stderr(Stdio::piped())
827 .spawn()?;
828 let stdout_pipe = child.stdout.take().expect("stdout is piped");
829 let stderr_pipe = child.stderr.take().expect("stderr is piped");
830
831 let echo = progress.is_none() && std_output_enabled();
834 let stdout_task = smol::spawn(drain_pipe(stdout_pipe));
837 let stderr_task = smol::spawn(drain_cargo_stderr(stderr_pipe, progress, echo));
838 let status = child.status().await?;
839 let stdout = stdout_task.await?;
840 let stderr = stderr_task.await?;
841 Ok(std::process::Output {
842 status,
843 stdout,
844 stderr,
845 })
846}
847
848async fn drain_pipe(mut reader: impl smol::io::AsyncRead + Unpin) -> io::Result<Vec<u8>> {
850 let mut collected = Vec::new();
851 let mut chunk = [0u8; 8192];
852 loop {
853 let read = reader.read(&mut chunk).await?;
854 if read == 0 {
855 break;
856 }
857 collected.extend_from_slice(&chunk[..read]);
858 }
859 Ok(collected)
860}
861
862async fn drain_cargo_stderr(
866 mut reader: impl smol::io::AsyncRead + Unpin,
867 progress: Option<BuildProgress>,
868 echo: bool,
869) -> io::Result<Vec<u8>> {
870 let mut collected = Vec::new();
871 let mut pending: Vec<u8> = Vec::new();
872 let mut chunk = [0u8; 8192];
873 loop {
874 let read = reader.read(&mut chunk).await?;
875 if read == 0 {
876 break;
877 }
878 collected.extend_from_slice(&chunk[..read]);
879 if echo {
880 let _ = io::stderr().write_all(&chunk[..read]);
881 let _ = io::stderr().flush();
882 }
883 if let Some(sink) = &progress {
884 pending.extend_from_slice(&chunk[..read]);
885 while let Some(newline) = pending.iter().position(|byte| *byte == b'\n') {
889 let line: Vec<u8> = pending.drain(..=newline).collect();
890 let line = String::from_utf8_lossy(&line);
891 let line = line.trim_end();
892 if !line.trim().is_empty() {
893 sink.report(classify_compile_line(line));
894 }
895 }
896 }
897 }
898 if let Some(sink) = &progress {
899 let tail = String::from_utf8_lossy(&pending);
900 let tail = tail.trim_end();
901 if !tail.trim().is_empty() {
902 sink.report(classify_compile_line(tail));
903 }
904 }
905 Ok(collected)
906}
907
908impl RustBuild {
909 pub fn new(path: impl AsRef<Path>, triple: Triple) -> Self {
911 Self {
912 path: path.as_ref().to_path_buf(),
913 triple,
914 project: None,
915 target_dir: None,
916 sccache_path: None,
917 features: Vec::new(),
918 crate_type_override: None,
919 rustc_flags: Vec::new(),
920 final_rustc_args: Vec::new(),
921 build_std_toolchain: None,
922 envs: Vec::new(),
923 progress: None,
924 }
925 }
926
927 pub(crate) fn with_project(mut self, project: &Project) -> Self {
928 self.project = Some(project.clone());
929 self
930 }
931
932 #[must_use]
934 pub fn with_target_dir(mut self, target_dir: impl Into<PathBuf>) -> Self {
935 self.target_dir = Some(target_dir.into());
936 self
937 }
938
939 #[must_use]
944 pub fn with_sccache(mut self, sccache_path: PathBuf) -> Self {
945 self.sccache_path = Some(sccache_path);
946 self
947 }
948
949 #[must_use]
953 pub fn with_feature(mut self, feature: impl Into<String>) -> Self {
954 self.features.push(feature.into());
955 self
956 }
957
958 #[must_use]
960 pub fn with_features(mut self, features: impl IntoIterator<Item = impl Into<String>>) -> Self {
961 self.features.extend(features.into_iter().map(Into::into));
962 self
963 }
964
965 #[must_use]
967 pub fn features(&self) -> &[String] {
968 &self.features
969 }
970
971 #[must_use]
973 pub fn with_rustc_flag(mut self, flag: impl Into<String>) -> Self {
974 self.rustc_flags.push(flag.into());
975 self
976 }
977
978 #[must_use]
985 pub fn with_final_rustc_arg(mut self, flag: impl Into<String>) -> Self {
986 self.final_rustc_args.push(flag.into());
987 self
988 }
989
990 #[must_use]
1003 pub fn with_build_std(mut self, toolchain: impl Into<String>) -> Self {
1004 self.build_std_toolchain = Some(toolchain.into());
1005 self
1006 }
1007
1008 #[must_use]
1010 pub fn with_preferred_dynamic_linking(self) -> Self {
1011 self.with_rustc_flag("-Cprefer-dynamic")
1012 .with_rustc_flag("-Crpath")
1013 }
1014
1015 #[must_use]
1022 pub fn with_linkage(
1023 self,
1024 linkage: RustLinkage,
1025 development_feature: &str,
1026 loader_search_path: Option<&str>,
1027 ) -> Self {
1028 if linkage == RustLinkage::Static {
1029 return self;
1030 }
1031 let build = self
1032 .with_feature(development_feature)
1033 .with_preferred_dynamic_linking();
1034 match loader_search_path {
1035 Some(path) => build.with_final_rustc_arg(format!("-Clink-arg=-Wl,-rpath,{path}")),
1036 None => build,
1037 }
1038 }
1039
1040 #[must_use]
1042 pub fn with_crate_type_override(mut self, crate_type: impl Into<String>) -> Self {
1043 self.crate_type_override = Some(crate_type.into());
1044 self
1045 }
1046
1047 #[must_use]
1049 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<OsString>) -> Self {
1050 self.envs.push((key.into(), value.into()));
1051 self
1052 }
1053
1054 #[must_use]
1056 pub fn with_envs(mut self, envs: impl IntoIterator<Item = (String, OsString)>) -> Self {
1057 self.envs.extend(envs);
1058 self
1059 }
1060
1061 #[must_use]
1066 pub fn with_progress(mut self, progress: BuildProgress) -> Self {
1067 self.progress = Some(progress);
1068 self
1069 }
1070
1071 #[must_use]
1073 pub const fn triple(&self) -> &Triple {
1074 &self.triple
1075 }
1076
1077 pub async fn dev_build(&self) -> Result<BuiltTarget, RustBuildError> {
1085 self.build_lib(false).await
1086 }
1087
1088 pub async fn release_build(&self) -> Result<BuiltTarget, RustBuildError> {
1094 self.build_lib(true).await
1095 }
1096
1097 pub async fn build_lib(&self, release: bool) -> Result<BuiltTarget, RustBuildError> {
1108 self.build_inner(release, CargoTarget::Lib, self.lib_artifact_extension())
1109 .await
1110 }
1111
1112 pub async fn build_dylib(&self, release: bool) -> Result<PathBuf, RustBuildError> {
1122 let built = self
1123 .build_inner(
1124 release,
1125 CargoTarget::Lib,
1126 Some(lib_extension_for_triple(&self.triple)),
1127 )
1128 .await?;
1129 Ok(built.artifact)
1130 }
1131
1132 pub async fn build_binary(
1142 &self,
1143 binary_name: &str,
1144 release: bool,
1145 ) -> Result<PathBuf, RustBuildError> {
1146 let built = self
1147 .build_inner(release, CargoTarget::Binary(binary_name), None)
1148 .await?;
1149 Ok(built.artifact)
1150 }
1151
1152 pub async fn dylib_path(
1160 &self,
1161 crate_name: &str,
1162 release: bool,
1163 ) -> Result<PathBuf, RustBuildError> {
1164 let lib_dir = self.lib_output_dir(release).await?;
1165 let lib_name = crate_name.replace('-', "_");
1166 let ext = lib_extension_for_triple(&self.triple);
1167 Ok(lib_dir.join(format!("lib{lib_name}.{ext}")))
1168 }
1169
1170 async fn build_inner(
1172 &self,
1173 release: bool,
1174 cargo_target: CargoTarget<'_>,
1175 artifact_extension: Option<&'static str>,
1176 ) -> Result<BuiltTarget, RustBuildError> {
1177 let mut output = self.cargo_build_output(release, cargo_target).await?;
1178
1179 if !output.status.success() {
1180 let mut combined = combined_build_output(&output);
1181
1182 if should_retry_after_cmake_generator_mismatch(&combined)
1185 && self.clean_stale_cmake_build_dirs().await?
1186 {
1187 output = self.cargo_build_output(release, cargo_target).await?;
1188 combined = combined_build_output(&output);
1189 }
1190
1191 if !output.status.success() && should_auto_install_meson(&combined) {
1192 match ensure_meson_installed_for_build().await {
1193 Ok(()) => {
1194 output = self.cargo_build_output(release, cargo_target).await?;
1195 }
1196 Err(install_err) => {
1197 return Err(RustBuildError::FailToBuildRustLibrary(
1198 std::io::Error::other(format!(
1199 "Cargo build failed and meson appears missing.\n\
1200Automatic meson installation failed: {install_err}\n\n{}",
1201 self.failure_report(&combined)
1202 )),
1203 ));
1204 }
1205 }
1206 }
1207 }
1208
1209 if !output.status.success() {
1210 let combined = combined_build_output(&output);
1211 return Err(RustBuildError::FailToBuildRustLibrary(
1212 std::io::Error::other(format!(
1213 "Cargo build failed:\n{}",
1214 self.failure_report(&combined)
1215 )),
1216 ));
1217 }
1218
1219 let stale = stale_shared_dylib_packages(&output.stdout).await?;
1228 if !stale.is_empty() {
1229 let target_dir = self.target_directory().await?;
1230 for package in &stale {
1231 clean_cargo_package(&self.path, package, &target_dir).await?;
1232 }
1233 output = self.cargo_build_output(release, cargo_target).await?;
1234 if !output.status.success() {
1235 let combined = combined_build_output(&output);
1236 return Err(RustBuildError::FailToBuildRustLibrary(
1237 std::io::Error::other(format!(
1238 "Cargo build failed:\n{}",
1239 self.failure_report(&combined)
1240 )),
1241 ));
1242 }
1243 }
1244
1245 let artifact =
1246 reported_artifact(&output.stdout, &self.path, cargo_target, artifact_extension)?;
1247 let profile_dir = self.lib_output_dir(release).await?;
1248 Ok(BuiltTarget {
1249 profile_dir,
1250 artifact,
1251 })
1252 }
1253
1254 fn lib_artifact_extension(&self) -> Option<&'static str> {
1257 self.crate_type_override
1258 .as_deref()
1259 .and_then(|crate_type| crate_type_artifact_extension(crate_type, &self.triple))
1260 }
1261
1262 fn failure_report(&self, combined: &str) -> String {
1265 if self
1266 .progress
1267 .as_ref()
1268 .is_some_and(BuildProgress::shows_all_lines)
1269 {
1270 output_tail(combined)
1271 } else {
1272 combined.to_owned()
1273 }
1274 }
1275
1276 async fn clean_stale_cmake_build_dirs(&self) -> Result<bool, RustBuildError> {
1277 let target_dir = self.target_directory().await?;
1278 let triple = self.triple.to_string();
1279
1280 let removed = unblock(move || {
1281 let mut removed = 0usize;
1282 removed +=
1283 remove_cmake_build_dirs_in(&target_dir.join(&triple).join("debug").join("build"))?;
1284 removed += remove_cmake_build_dirs_in(
1285 &target_dir.join(&triple).join("release").join("build"),
1286 )?;
1287 Ok::<usize, std::io::Error>(removed)
1288 })
1289 .await
1290 .map_err(|error| {
1291 RustBuildError::FailToBuildRustLibrary(std::io::Error::other(format!(
1292 "Failed to clean stale CMake cache: {error}"
1293 )))
1294 })?;
1295
1296 Ok(removed > 0)
1297 }
1298
1299 async fn cargo_build_output(
1300 &self,
1301 release: bool,
1302 cargo_target: CargoTarget<'_>,
1303 ) -> Result<std::process::Output, RustBuildError> {
1304 let framework = self.project.as_ref().and_then(|project| {
1305 project
1306 .manifest()
1307 .framework
1308 .as_ref()
1309 .map(|framework| (project, framework))
1310 });
1311 if let Some((project, framework)) = framework {
1312 framework
1313 .prepare_build(project, &self.path, &self.features)
1314 .await
1315 .map_err(|error| {
1316 RustBuildError::FailToBuildRustLibrary(std::io::Error::other(error.to_string()))
1317 })?;
1318 }
1319 let crate_type_override = if cargo_target.accepts_crate_type_override() {
1320 self.crate_type_override.as_deref()
1321 } else {
1322 None
1323 };
1324 let mut cmd = Command::new("cargo");
1325 let cargo_subcommand = if crate_type_override.is_some() || !self.final_rustc_args.is_empty()
1326 {
1327 "rustc"
1328 } else {
1329 "build"
1330 };
1331 let mut cmd = cmd.arg(cargo_subcommand);
1332 if self.build_std_toolchain.is_some() {
1333 cmd = cmd.arg("-Zbuild-std=std,panic_abort");
1343 cmd =
1344 cmd.arg("-Zbuild-std-features=panic-unwind,backtrace,default,compiler-builtins-c");
1345 }
1346 let mut cmd = cmd
1347 .arg("--message-format=json-render-diagnostics")
1348 .args(cargo_target.cargo_args())
1349 .args(["--target", self.triple.to_string().as_str()])
1350 .current_dir(&self.path);
1351 if framework.is_some() {
1352 cmd = cmd.arg("--locked");
1353 }
1354
1355 if let Some(target_dir) = &self.target_dir {
1356 cmd = cmd.arg("--target-dir").arg(target_dir);
1357 }
1358
1359 for (key, value) in &self.envs {
1361 cmd.env(key, value);
1362 }
1363
1364 if !self.rustc_flags.is_empty() {
1365 let mut rustflags = std::env::var_os("RUSTFLAGS").unwrap_or_default();
1366 if !rustflags.is_empty() {
1367 rustflags.push(" ");
1368 }
1369 rustflags.push(self.rustc_flags.join(" "));
1370 cmd = cmd.env("RUSTFLAGS", rustflags);
1371 }
1372
1373 configure_generated_crate_compilation(cmd);
1374
1375 if let Some(sccache_path) = &self.sccache_path {
1377 crate::toolchain::sccache::configure_compilation_cache(cmd, sccache_path).map_err(
1378 |error| {
1379 RustBuildError::FailToBuildRustLibrary(std::io::Error::other(error.to_string()))
1380 },
1381 )?;
1382 }
1383
1384 if self.build_std_toolchain.is_some() {
1390 cmd = self.with_build_std_envs(cmd, release).await?;
1391 }
1392
1393 if self.triple.environment == Environment::Sim
1400 && let Some(clang_args) = self.bindgen_clang_args_for_simulator().await
1401 {
1402 let bindgen_target_key = format!(
1403 "BINDGEN_EXTRA_CLANG_ARGS_{}",
1404 self.triple.to_string().replace('-', "_")
1405 );
1406 cmd = cmd.env(bindgen_target_key, clang_args);
1407 }
1408
1409 if release {
1410 cmd = cmd.arg("--release");
1411 }
1412
1413 if !self.features.is_empty() {
1415 cmd = cmd.args(["--features", &self.features.join(",")]);
1416 }
1417
1418 if crate_type_override.is_some() || !self.final_rustc_args.is_empty() {
1419 cmd = cmd.arg("--");
1420 if let Some(crate_type) = crate_type_override {
1421 cmd = cmd.arg("--crate-type").arg(crate_type);
1422 }
1423 cmd = cmd.args(&self.final_rustc_args);
1424 }
1425
1426 if std_output_enabled()
1431 && std::env::var_os("CARGO_TERM_COLOR").is_none()
1432 && !self.envs.iter().any(|(key, _)| key == "CARGO_TERM_COLOR")
1433 {
1434 cmd.env("CARGO_TERM_COLOR", "always");
1435 }
1436
1437 command_output_with_progress(cmd, self.progress.clone())
1438 .await
1439 .map_err(RustBuildError::FailToExecuteCargoBuild)
1440 }
1441
1442 async fn with_build_std_envs<'a>(
1446 &self,
1447 cmd: &'a mut Command,
1448 release: bool,
1449 ) -> Result<&'a mut Command, RustBuildError> {
1450 let Some(toolchain) = &self.build_std_toolchain else {
1451 return Ok(cmd);
1452 };
1453 let publish_dir = self.lib_output_dir(release).await?.join("deps");
1454 let cmd = cmd
1455 .env("RUSTUP_TOOLCHAIN", toolchain)
1456 .env(
1457 "RUSTC_WRAPPER",
1458 crate::toolchain::Host::current_exe()
1459 .map_err(RustBuildError::FailToExecuteCargoBuild)?,
1460 )
1461 .env(crate::workflows::rustc_wrapper::WRAPPER_MODE_ENV, "1")
1462 .env(
1463 crate::workflows::rustc_wrapper::BUILD_STD_TARGET_ENV,
1464 self.triple.to_string(),
1465 )
1466 .env(
1467 crate::workflows::rustc_wrapper::BUILD_STD_DYLIB_DIR_ENV,
1468 publish_dir,
1469 );
1470 if let Some(sccache_path) = &self.sccache_path {
1471 cmd.env(
1472 crate::workflows::rustc_wrapper::WRAPPER_CHAIN_ENV,
1473 sccache_path,
1474 );
1475 }
1476 cmd.env_remove("RUSTC_WORKSPACE_WRAPPER");
1482 cmd.env_remove("CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER");
1483 Ok(cmd)
1484 }
1485
1486 pub async fn lib_output_dir(&self, release: bool) -> Result<PathBuf, RustBuildError> {
1491 let target_directory = self.target_directory().await?;
1492 Ok(target_directory
1493 .join(self.triple.to_string())
1494 .join(if release { "release" } else { "debug" }))
1495 }
1496
1497 async fn target_directory(&self) -> Result<PathBuf, RustBuildError> {
1498 if let Some(target_dir) = &self.target_dir {
1499 return Ok(target_dir.clone());
1500 }
1501
1502 let build_path = self.path.clone();
1503 let metadata = unblock(move || {
1504 cargo_metadata::MetadataCommand::new()
1505 .no_deps()
1506 .current_dir(build_path)
1507 .exec()
1508 .map_err(|e| {
1509 RustBuildError::FailToBuildRustLibrary(std::io::Error::new(
1510 std::io::ErrorKind::InvalidData,
1511 e,
1512 ))
1513 })
1514 })
1515 .await?;
1516 Ok(metadata.target_directory.as_std_path().to_path_buf())
1517 }
1518
1519 async fn bindgen_clang_args_for_simulator(&self) -> Option<String> {
1524 let (sdk_name, target_os) = match self.triple.operating_system {
1525 OperatingSystem::IOS(_) => ("iphonesimulator", "ios"),
1526 OperatingSystem::TvOS(_) => ("appletvsimulator", "tvos"),
1527 OperatingSystem::WatchOS(_) => ("watchsimulator", "watchos"),
1528 OperatingSystem::VisionOS(_) => ("xrsimulator", "xros"),
1529 _ => return None,
1530 };
1531
1532 let arch = match self.triple.architecture {
1533 target_lexicon::Architecture::Aarch64(_) => "arm64",
1534 target_lexicon::Architecture::X86_64 => "x86_64",
1535 _ => return None,
1536 };
1537
1538 let sdk_path = run_command("xcrun", ["--sdk", sdk_name, "--show-sdk-path"])
1540 .await
1541 .ok()
1542 .map(|s| s.trim().to_string())?;
1543
1544 let min_version = if matches!(target_os, "ios" | "tvos") {
1546 "17.0"
1547 } else if target_os == "watchos" {
1548 "10.0"
1549 } else {
1550 debug_assert_eq!(
1551 target_os, "xros",
1552 "bindgen simulator target_os must be one of ios/tvos/watchos/xros"
1553 );
1554 "1.0"
1555 };
1556
1557 Some(format!(
1558 "--target={arch}-apple-{target_os}{min_version}-simulator -isysroot {sdk_path}"
1559 ))
1560 }
1561}
1562
1563fn crate_type_artifact_extension(crate_type: &str, triple: &Triple) -> Option<&'static str> {
1566 match crate_type {
1567 "lib" | "rlib" => Some("rlib"),
1568 "staticlib" => Some(if matches!(triple.environment, Environment::Msvc) {
1569 "lib"
1570 } else {
1571 "a"
1572 }),
1573 "cdylib" | "dylib" | "proc-macro" => Some(lib_extension_for_triple(triple)),
1574 _ => None,
1575 }
1576}
1577
1578pub(crate) fn reported_artifact(
1593 stdout: &[u8],
1594 crate_dir: &Path,
1595 cargo_target: CargoTarget<'_>,
1596 artifact_extension: Option<&'static str>,
1597) -> Result<PathBuf, RustBuildError> {
1598 let manifest_path = dunce::canonicalize(crate_dir.join("Cargo.toml")).map_err(|error| {
1599 RustBuildError::FailToBuildRustLibrary(io::Error::other(format!(
1600 "failed to canonicalize {}: {error}",
1601 crate_dir.join("Cargo.toml").display()
1602 )))
1603 })?;
1604 let mut artifacts = Vec::new();
1605 for artifact in compiler_artifacts(stdout)? {
1606 if cargo_target.matches(&artifact.target)
1607 && same_manifest_path(artifact.manifest_path.as_std_path(), &manifest_path)
1608 {
1609 artifacts.push(artifact);
1610 }
1611 }
1612 reported_artifact_file(&artifacts, cargo_target, artifact_extension, &manifest_path)
1613}
1614
1615pub(crate) fn compiler_artifacts(
1624 stdout: &[u8],
1625) -> Result<Vec<cargo_metadata::Artifact>, RustBuildError> {
1626 #[derive(serde::Deserialize)]
1628 struct Reason {
1629 reason: String,
1630 }
1631
1632 let mut artifacts = Vec::new();
1633 for (index, line) in stdout.split(|byte| *byte == b'\n').enumerate() {
1634 let Ok(line) = str::from_utf8(line) else {
1635 continue;
1636 };
1637 let line = line.trim_end();
1638 if line.is_empty() {
1639 continue;
1640 }
1641 let malformed = |error: serde_json::Error| {
1642 RustBuildError::FailToBuildRustLibrary(io::Error::new(
1643 io::ErrorKind::InvalidData,
1644 format!(
1645 "cargo emitted a malformed `compiler-artifact` message on line {}: {error}\n{line}",
1646 index + 1
1647 ),
1648 ))
1649 };
1650 match serde_json::from_str::<Reason>(line) {
1651 Ok(Reason { reason }) if reason == "compiler-artifact" => {
1652 let artifact =
1653 serde_json::from_str::<cargo_metadata::Artifact>(line).map_err(malformed)?;
1654 artifacts.push(artifact);
1655 }
1656 Err(error) if line.contains("\"reason\":\"compiler-artifact\"") => {
1660 return Err(malformed(error));
1661 }
1662 Ok(_) | Err(_) => {}
1663 }
1664 }
1665 Ok(artifacts)
1666}
1667
1668pub(crate) fn same_manifest_path(reported: &Path, expected: &Path) -> bool {
1674 reported == expected
1675 || dunce::canonicalize(reported).is_ok_and(|canonical| canonical == expected)
1676}
1677
1678fn reported_artifact_file(
1681 artifacts: &[cargo_metadata::Artifact],
1682 cargo_target: CargoTarget<'_>,
1683 artifact_extension: Option<&'static str>,
1684 manifest_path: &Path,
1685) -> Result<PathBuf, RustBuildError> {
1686 let what = || -> String {
1687 match cargo_target {
1688 CargoTarget::Lib => format!("the library target of {}", manifest_path.display()),
1689 CargoTarget::Binary(name) => {
1690 format!("binary `{name}` of {}", manifest_path.display())
1691 }
1692 }
1693 };
1694 let not_found = |detail: String| {
1695 RustBuildError::FailToBuildRustLibrary(io::Error::new(io::ErrorKind::NotFound, detail))
1696 };
1697
1698 let files: Vec<PathBuf> = artifacts
1699 .iter()
1700 .flat_map(|artifact| {
1701 artifact
1702 .filenames
1703 .iter()
1704 .map(|file| file.as_std_path().to_path_buf())
1705 })
1706 .collect();
1707 let artifact = match cargo_target {
1708 CargoTarget::Binary(_) => artifacts
1709 .iter()
1710 .find_map(|artifact| artifact.executable.as_ref())
1711 .map(|path| path.as_std_path().to_path_buf())
1712 .ok_or_else(|| {
1713 not_found(format!(
1714 "Cargo reported no artifact for {} (reported files: {files:?})",
1715 what()
1716 ))
1717 })?,
1718 CargoTarget::Lib => {
1719 let matching: Vec<&PathBuf> = artifact_extension.map_or_else(
1720 || files.iter().collect(),
1721 |extension| {
1722 files
1723 .iter()
1724 .filter(|file| file.extension().is_some_and(|e| *e == *extension))
1725 .collect()
1726 },
1727 );
1728 match matching.as_slice() {
1729 [only] => (*only).clone(),
1730 _ => {
1731 return Err(not_found(artifact_extension.map_or_else(
1732 || {
1733 format!(
1734 "Cargo reported {} artifacts for {} — select one with a crate-type override (reported files: {files:?})",
1735 matching.len(),
1736 what()
1737 )
1738 },
1739 |extension| {
1740 format!(
1741 "Cargo reported no `.{extension}` artifact for {} (reported files: {files:?})",
1742 what()
1743 )
1744 },
1745 )));
1746 }
1747 }
1748 }
1749 };
1750 if !artifact.is_file() {
1751 return Err(not_found(format!(
1752 "Cargo reported {} for {} but the file does not exist",
1753 artifact.display(),
1754 what()
1755 )));
1756 }
1757 Ok(artifact)
1758}
1759
1760async fn stale_shared_dylib_packages(stdout: &[u8]) -> Result<Vec<String>, RustBuildError> {
1770 let mut stale = Vec::new();
1771 for artifact in compiler_artifacts(stdout)? {
1772 if !artifact.fresh {
1773 continue;
1774 }
1775 let Some(manifest_dir) = artifact.manifest_path.as_std_path().parent() else {
1776 continue;
1777 };
1778 if !uplifts_dynamic_library(&artifact.target) {
1784 continue;
1785 }
1786 let manifest_root = dunce::simplified(manifest_dir);
1787 let mut package_stale = false;
1788 for filename in &artifact.filenames {
1789 let file = filename.as_std_path();
1790 if !is_dynamic_library(file) {
1791 continue;
1792 }
1793 let Some(dep_info) = dep_info_path(file, &artifact.filenames) else {
1794 return Err(RustBuildError::FailToBuildRustLibrary(io::Error::new(
1795 io::ErrorKind::NotFound,
1796 format!(
1797 "Cargo reported {} fresh but no dep-info was found beside it or in its unit directory (reported files: {:?})",
1798 file.display(),
1799 artifact.filenames
1800 ),
1801 )));
1802 };
1803 let contents = smol::fs::read_to_string(&dep_info).await.map_err(|error| {
1804 RustBuildError::FailToBuildRustLibrary(io::Error::other(format!(
1805 "Cargo reported {} fresh but its dep-info {} is unreadable: {error}",
1806 file.display(),
1807 dep_info.display()
1808 )))
1809 })?;
1810 if !dep_info_prerequisites(&contents).iter().any(|source| {
1814 let source = if source.is_absolute() {
1815 source.clone()
1816 } else {
1817 manifest_dir.join(source)
1818 };
1819 dunce::simplified(&source).starts_with(manifest_root)
1820 }) {
1821 package_stale = true;
1822 }
1823 }
1824 if package_stale {
1825 stale.push(artifact_package_name(&artifact.package_id).to_owned());
1826 }
1827 }
1828 stale.sort_unstable();
1829 stale.dedup();
1830 Ok(stale)
1831}
1832
1833fn is_dynamic_library(file: &Path) -> bool {
1836 file.extension()
1837 .is_some_and(|extension| matches!(extension.to_str(), Some("so" | "dylib" | "dll")))
1838}
1839
1840fn uplifts_dynamic_library(target: &cargo_metadata::Target) -> bool {
1844 target.crate_types.iter().any(|kind| {
1845 matches!(
1846 kind,
1847 cargo_metadata::CrateType::DyLib | cargo_metadata::CrateType::CDyLib
1848 )
1849 })
1850}
1851
1852fn dep_info_path(
1868 artifact_file: &Path,
1869 sibling_files: &[cargo_metadata::camino::Utf8PathBuf],
1870) -> Option<PathBuf> {
1871 let file_stem = artifact_file.file_stem()?.to_str()?;
1872 let name = file_stem.strip_prefix("lib").unwrap_or(file_stem);
1873 let dir = artifact_file.parent()?;
1874 let mut candidates = vec![
1879 dir.join(format!("{file_stem}.d")),
1880 dir.join("deps").join(format!("{name}.d")),
1881 ];
1882 candidates.extend(
1883 sibling_files
1884 .iter()
1885 .filter_map(|sibling| sibling.as_std_path().parent())
1886 .filter(|unit_dir| *unit_dir != dir)
1887 .map(|unit_dir| unit_dir.join(format!("{name}.d"))),
1888 );
1889 candidates.push(dir.join(format!("{name}.d")));
1890 candidates.into_iter().find(|candidate| candidate.is_file())
1891}
1892
1893fn dep_info_prerequisites(contents: &str) -> Vec<PathBuf> {
1911 let mut joined = String::with_capacity(contents.len());
1914 for line in contents.lines() {
1915 if let Some(head) = line.strip_suffix('\\') {
1916 joined.push_str(head);
1917 joined.push(' ');
1918 } else {
1919 joined.push_str(line);
1920 joined.push('\n');
1921 }
1922 }
1923 let mut prerequisites = Vec::new();
1924 for line in joined.lines() {
1925 let Some((_, rest)) = line.split_once(": ") else {
1926 continue;
1927 };
1928 let mut token = String::new();
1929 let mut chars = rest.chars().peekable();
1930 while let Some(c) = chars.next() {
1931 match c {
1932 '\\' if chars.peek() == Some(&' ') => {
1933 chars.next();
1934 token.push(' ');
1935 }
1936 c if c.is_whitespace() => {
1937 if !token.is_empty() {
1938 prerequisites.push(PathBuf::from(std::mem::take(&mut token)));
1939 }
1940 }
1941 c => token.push(c),
1942 }
1943 }
1944 if !token.is_empty() {
1945 prerequisites.push(PathBuf::from(token));
1946 }
1947 }
1948 prerequisites
1949}
1950
1951fn artifact_package_name(package_id: &cargo_metadata::PackageId) -> &str {
1954 let repr = package_id.repr.as_str();
1955 let (source, fragment) = repr.rsplit_once('#').unwrap_or((repr, ""));
1956 fragment.split_once('@').map_or_else(
1957 || source.rsplit('/').next().unwrap_or(repr),
1958 |(name, _)| name,
1959 )
1960}
1961
1962async fn clean_cargo_package(
1966 crate_dir: &Path,
1967 package: &str,
1968 target_dir: &Path,
1969) -> Result<(), RustBuildError> {
1970 let mut command = Command::new("cargo");
1971 command
1972 .arg("clean")
1973 .arg("-p")
1974 .arg(package)
1975 .arg("--target-dir")
1976 .arg(target_dir)
1977 .current_dir(crate_dir);
1978 configure_generated_crate_compilation(&mut command);
1979 let output = command
1980 .output()
1981 .await
1982 .map_err(RustBuildError::FailToExecuteCargoBuild)?;
1983 if !output.status.success() {
1984 return Err(RustBuildError::FailToBuildRustLibrary(io::Error::other(
1985 format!(
1986 "cargo clean -p {package} failed:\n{}",
1987 String::from_utf8_lossy(&output.stderr)
1988 ),
1989 )));
1990 }
1991 Ok(())
1992}
1993
1994fn combined_build_output(output: &std::process::Output) -> String {
1995 let stderr = String::from_utf8_lossy(&output.stderr);
1996 let stdout = String::from_utf8_lossy(&output.stdout);
1997 if stderr.is_empty() {
1998 stdout.to_string()
1999 } else {
2000 stderr.to_string()
2001 }
2002}
2003
2004const FAILURE_TAIL_LINES: usize = 40;
2007
2008pub(crate) fn output_tail(text: &str) -> String {
2011 let lines: Vec<&str> = text.lines().collect();
2012 if lines.len() <= FAILURE_TAIL_LINES {
2013 return text.to_owned();
2014 }
2015 format!(
2016 "… {} earlier lines already streamed above …\n{}",
2017 lines.len() - FAILURE_TAIL_LINES,
2018 lines[lines.len() - FAILURE_TAIL_LINES..].join("\n")
2019 )
2020}
2021
2022fn should_auto_install_meson(build_output: &str) -> bool {
2023 let lower = build_output.to_ascii_lowercase();
2024 lower.contains("meson")
2025 && (lower.contains("not found")
2026 || lower.contains("no such file")
2027 || lower.contains("failed to execute")
2028 || lower.contains("is required"))
2029}
2030
2031fn should_retry_after_cmake_generator_mismatch(build_output: &str) -> bool {
2032 let lower = build_output.to_ascii_lowercase();
2033 lower.contains("cmake error") && lower.contains("does not match the generator used previously")
2034}
2035
2036fn remove_cmake_build_dirs_in(build_root: &Path) -> std::io::Result<usize> {
2037 if !build_root.exists() {
2038 return Ok(0);
2039 }
2040
2041 let mut removed = 0usize;
2042 for entry in std::fs::read_dir(build_root)? {
2043 let entry = entry?;
2044 let path = entry.path();
2045 if !path.is_dir() {
2046 continue;
2047 }
2048
2049 let cmake_build_dir = path.join("out").join("build");
2050 if cmake_build_dir.join("CMakeCache.txt").exists() {
2051 std::fs::remove_dir_all(cmake_build_dir)?;
2052 removed += 1;
2053 }
2054 }
2055
2056 Ok(removed)
2057}
2058
2059#[cfg(target_os = "macos")]
2060async fn ensure_meson_installed_for_build() -> Result<(), String> {
2061 use crate::toolchain::meson::Meson;
2062 use crate::toolchain::{Installation as _, Toolchain as _, ToolchainError};
2063
2064 let host = crate::toolchain::Host::current();
2065 match Meson.check(&host).await {
2066 Ok(()) => Ok(()),
2067 Err(ToolchainError::Fixable(installation)) => {
2068 installation.install(&host).await.map_err(|e| e.to_string())
2069 }
2070 Err(ToolchainError::Unfixable(e)) => Err(e.to_string()),
2071 }
2072}
2073
2074#[cfg(not(target_os = "macos"))]
2075fn ensure_meson_installed_for_build() -> impl std::future::Future<Output = Result<(), String>> {
2076 std::future::ready(Err(
2077 "automatic meson installation is only supported on macOS".to_string(),
2078 ))
2079}
2080
2081#[cfg(test)]
2082mod tests {
2083 use target_lexicon::Triple;
2084 use tempfile::tempdir;
2085
2086 use std::ffi::OsString;
2087 use std::path::PathBuf;
2088
2089 use super::{
2090 BuildOptions, BuildProfile, CargoTarget, CompileEvent, RustBuild, RustDynamicLibraries,
2091 RustLinkage, classify_compile_line, dynamic_library_file_name, lib_extension_for_triple,
2092 resolve_rust_standard_library_in,
2093 };
2094
2095 fn triple(value: &str) -> Triple {
2096 value.parse().expect("test target triple must parse")
2097 }
2098
2099 #[test]
2100 fn crate_type_override_applies_only_to_library_targets() {
2101 assert!(CargoTarget::Lib.accepts_crate_type_override());
2102 assert!(!CargoTarget::Binary("waterui-cef-helper").accepts_crate_type_override());
2103 assert_eq!(CargoTarget::Lib.cargo_args(), ["--lib"]);
2104 assert_eq!(
2105 CargoTarget::Binary("waterui-cef-helper").cargo_args(),
2106 ["--bin", "waterui-cef-helper"]
2107 );
2108 }
2109
2110 #[test]
2111 fn build_std_envs_wire_the_wrapper_and_clear_workspace_wrappers() {
2112 use std::ffi::OsStr;
2113
2114 let dir = tempdir().expect("target dir");
2115 let toolchain = "nightly-2026-09-09-aarch64-apple-darwin";
2116 let target_dir = dir.path().join("target");
2117 let build = RustBuild::new(dir.path(), triple("aarch64-linux-android"))
2118 .with_build_std(toolchain)
2119 .with_target_dir(target_dir.clone())
2120 .with_sccache(std::path::PathBuf::from("/fake/sccache"));
2121 let mut cmd = smol::process::Command::new("cargo");
2122 smol::block_on(build.with_build_std_envs(&mut cmd, false)).expect("build-std envs apply");
2123
2124 let env = |key: &str| -> Option<Option<OsString>> {
2125 cmd.get_envs()
2126 .find(|(name, _)| *name == OsStr::new(key))
2127 .map(|(_, value)| value.map(ToOwned::to_owned))
2128 };
2129 assert_eq!(
2130 env("RUSTUP_TOOLCHAIN"),
2131 Some(Some(OsString::from(toolchain)))
2132 );
2133 assert_eq!(
2134 env("RUSTC_WRAPPER"),
2135 Some(Some(
2136 crate::toolchain::Host::current_exe()
2137 .expect("the test binary path")
2138 .into_os_string()
2139 )),
2140 "the wrapper must name this binary"
2141 );
2142 assert_eq!(
2143 env(crate::workflows::rustc_wrapper::WRAPPER_MODE_ENV),
2144 Some(Some(OsString::from("1")))
2145 );
2146 assert_eq!(
2147 env(crate::workflows::rustc_wrapper::BUILD_STD_TARGET_ENV),
2148 Some(Some(OsString::from("aarch64-linux-android")))
2149 );
2150 let expected_dylib_dir = target_dir
2151 .join("aarch64-linux-android")
2152 .join("debug")
2153 .join("deps");
2154 assert_eq!(
2155 env(crate::workflows::rustc_wrapper::BUILD_STD_DYLIB_DIR_ENV),
2156 Some(Some(expected_dylib_dir.into_os_string()))
2157 );
2158 assert_eq!(
2159 env(crate::workflows::rustc_wrapper::WRAPPER_CHAIN_ENV),
2160 Some(Some(OsString::from("/fake/sccache"))),
2161 "a configured sccache chains behind the shim"
2162 );
2163 assert_eq!(env("RUSTC_WORKSPACE_WRAPPER"), Some(None));
2166 assert_eq!(env("CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER"), Some(None));
2167 }
2168
2169 #[test]
2170 fn apple_platform_dylibs_use_macho_extension() {
2171 assert_eq!(
2172 lib_extension_for_triple(&triple("aarch64-apple-darwin")),
2173 "dylib"
2174 );
2175 assert_eq!(
2176 lib_extension_for_triple(&triple("aarch64-apple-ios-sim")),
2177 "dylib"
2178 );
2179 assert_eq!(
2180 lib_extension_for_triple(&triple("aarch64-apple-ios")),
2181 "dylib"
2182 );
2183 }
2184
2185 #[test]
2186 fn non_apple_platform_dylibs_keep_platform_extensions() {
2187 assert_eq!(
2188 lib_extension_for_triple(&triple("aarch64-linux-android")),
2189 "so"
2190 );
2191 assert_eq!(
2192 lib_extension_for_triple(&triple("x86_64-unknown-linux-gnu")),
2193 "so"
2194 );
2195 assert_eq!(
2196 lib_extension_for_triple(&triple("x86_64-pc-windows-msvc")),
2197 "dll"
2198 );
2199 }
2200
2201 #[test]
2202 fn development_and_packaging_have_distinct_linkage() {
2203 assert_eq!(
2204 BuildOptions::development(BuildProfile::Debug).linkage(),
2205 RustLinkage::SharedRuntime
2206 );
2207 assert_eq!(
2208 BuildOptions::packaging(BuildProfile::Debug).linkage(),
2209 RustLinkage::Static
2210 );
2211 assert!(BuildOptions::development(BuildProfile::Release).is_release());
2212 assert!(BuildOptions::packaging(BuildProfile::Release).is_release());
2213 }
2214
2215 #[test]
2216 fn build_profile_release_variants_select_the_release_profile() {
2217 assert!(BuildProfile::Release.is_release());
2218 assert!(BuildProfile::Profiling.is_release());
2219 assert!(!BuildProfile::Debug.is_release());
2220 assert!(!BuildProfile::Optimized.is_release());
2221 }
2222
2223 #[test]
2224 fn development_profile_envs_realize_the_selected_trade_off() {
2225 let optimized = BuildOptions::development(BuildProfile::Optimized);
2226 let envs = optimized.cargo_envs();
2227 assert!(
2228 envs.contains(&(
2229 "CARGO_PROFILE_DEV_OPT_LEVEL".to_string(),
2230 OsString::from("1")
2231 )),
2232 "optimized development lifts the dev opt-level: {envs:?}"
2233 );
2234 assert!(
2235 envs.contains(&(
2236 "CARGO_PROFILE_DEV_DEBUG_ASSERTIONS".to_string(),
2237 OsString::from("false")
2238 )),
2239 "optimized development drops dep debug assertions: {envs:?}"
2240 );
2241 assert!(
2242 envs.contains(&(
2243 "CARGO_PROFILE_DEV_DEBUG".to_string(),
2244 OsString::from("true")
2245 )),
2246 "optimized development keeps full debug info: {envs:?}"
2247 );
2248
2249 let profiling = BuildOptions::development(BuildProfile::Profiling);
2250 let envs = profiling.cargo_envs();
2251 for key in [
2252 "CARGO_PROFILE_RELEASE_OPT_LEVEL",
2253 "CARGO_PROFILE_RELEASE_DEBUG",
2254 "CARGO_PROFILE_RELEASE_STRIP",
2255 ] {
2256 assert!(
2257 envs.iter().any(|(env_key, _)| env_key == key),
2258 "profiling keeps debug info and symbols: missing {key} in {envs:?}"
2259 );
2260 }
2261
2262 assert!(
2263 BuildOptions::development(BuildProfile::Debug)
2264 .cargo_envs()
2265 .is_empty(),
2266 "plain debug runs the declared dev profile"
2267 );
2268 }
2269
2270 #[test]
2271 fn packaging_never_overrides_the_declared_profile() {
2272 for profile in [
2273 BuildProfile::Debug,
2274 BuildProfile::Optimized,
2275 BuildProfile::Release,
2276 BuildProfile::Profiling,
2277 ] {
2278 assert!(
2279 BuildOptions::packaging(profile).cargo_envs().is_empty(),
2280 "packaging {profile:?} must ship the declared profile"
2281 );
2282 }
2283 }
2284
2285 #[test]
2286 fn resolves_target_standard_library_without_guessing_hash() {
2287 let directory = tempdir().expect("temporary target libdir");
2288 let android_triple = triple("aarch64-linux-android");
2289 let expected = directory.path().join("libstd-1234567890abcdef.so");
2290 std::fs::write(&expected, []).expect("write test std library");
2291 std::fs::write(directory.path().join("libcore.rlib"), []).expect("write unrelated library");
2292
2293 assert_eq!(
2294 resolve_rust_standard_library_in(directory.path(), &android_triple)
2295 .expect("resolve dynamic std"),
2296 expected
2297 );
2298 assert_eq!(
2299 dynamic_library_file_name("waterui_dylib", &android_triple),
2300 "libwaterui_dylib.so"
2301 );
2302 assert_eq!(
2303 dynamic_library_file_name("waterui_dylib", &triple("x86_64-pc-windows-msvc")),
2304 "waterui_dylib.dll"
2305 );
2306 }
2307
2308 #[test]
2309 fn compile_progress_classifies_cargo_unit_lines() {
2310 assert_eq!(
2311 classify_compile_line(" Compiling serde v1.0.228"),
2312 CompileEvent::Unit {
2313 phase: "Compiling",
2314 name: "serde".to_string(),
2315 version: Some("1.0.228".to_string()),
2316 }
2317 );
2318 assert_eq!(
2319 classify_compile_line(" Compiling waterui-app v0.1.0 (/tmp/app)"),
2320 CompileEvent::Unit {
2321 phase: "Compiling",
2322 name: "waterui-app".to_string(),
2323 version: Some("0.1.0".to_string()),
2324 }
2325 );
2326 assert_eq!(
2327 classify_compile_line(" Checking libc v0.2.171"),
2328 CompileEvent::Unit {
2329 phase: "Checking",
2330 name: "libc".to_string(),
2331 version: Some("0.2.171".to_string()),
2332 }
2333 );
2334 }
2335
2336 #[test]
2337 fn compile_progress_keeps_non_unit_lines_verbatim() {
2338 assert_eq!(
2339 classify_compile_line(" Compiling 12 crates"),
2340 CompileEvent::Line("Compiling 12 crates".to_string())
2341 );
2342 assert_eq!(
2343 classify_compile_line(" Downloaded 300 crates (5.2 MB) in 1.23s"),
2344 CompileEvent::Line("Downloaded 300 crates (5.2 MB) in 1.23s".to_string())
2345 );
2346 assert_eq!(
2347 classify_compile_line(
2348 " Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.23s"
2349 ),
2350 CompileEvent::Finished(
2351 "Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.23s".to_string()
2352 )
2353 );
2354 assert_eq!(
2355 classify_compile_line("warning: unused import"),
2356 CompileEvent::Line("warning: unused import".to_string())
2357 );
2358 }
2359
2360 #[test]
2361 fn compile_progress_classifies_through_ansi_color() {
2362 let colored = "\u{1b}[0m\u{1b}[1m\u{1b}[32m Compiling\u{1b}[0m serde v1.0.228";
2365 assert_eq!(
2366 classify_compile_line(colored),
2367 CompileEvent::Unit {
2368 phase: "Compiling",
2369 name: "serde".to_string(),
2370 version: Some("1.0.228".to_string()),
2371 }
2372 );
2373 let colored_finished =
2374 "\u{1b}[0m\u{1b}[1m\u{1b}[32m Finished\u{1b}[0m `dev` profile in 1.23s";
2375 assert_eq!(
2376 classify_compile_line(colored_finished),
2377 CompileEvent::Finished(colored_finished.trim().to_string())
2378 );
2379 }
2380
2381 #[test]
2387 fn same_named_projects_resolve_their_own_artifacts_in_one_shared_target() {
2388 use crate::project_model::project_types::{CrateName, generated_crate_name};
2389
2390 smol::block_on(async {
2391 let temporary = tempdir().expect("tempdir");
2392 let shared_target = temporary.path().join("shared-target");
2393 let demo = CrateName::try_from("demo").expect("crate name");
2394 let mut artifacts = Vec::new();
2395 for (directory, marker) in [("first", "first"), ("second", "second")] {
2396 let project_root = temporary.path().join(directory);
2397 let crate_dir = project_root.join("hydrolysis");
2398 std::fs::create_dir_all(crate_dir.join("src")).expect("crate dir");
2399 let package = generated_crate_name(&demo, "hydrolysis", &project_root);
2400 std::fs::write(
2401 crate_dir.join("Cargo.toml"),
2402 format!(
2403 "[package]\nname = \"{package}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n"
2404 ),
2405 )
2406 .expect("manifest");
2407 std::fs::write(
2408 crate_dir.join("src/main.rs"),
2409 format!("fn main() {{ println!(\"{marker}\"); }}\n"),
2410 )
2411 .expect("main.rs");
2412
2413 let artifact = super::RustBuild::new(&crate_dir, Triple::host())
2414 .with_target_dir(&shared_target)
2415 .build_binary(package.as_str(), false)
2416 .await
2417 .expect("the generated crate builds");
2418 assert!(artifact.is_file(), "the reported artifact exists");
2419 artifacts.push(artifact);
2420 }
2421
2422 assert_ne!(
2423 artifacts[0], artifacts[1],
2424 "each same-named project resolves its own artifact"
2425 );
2426 for (artifact, marker) in artifacts.iter().zip(["first", "second"]) {
2427 let ran = std::process::Command::new(artifact)
2428 .output()
2429 .expect("the resolved artifact executes");
2430 assert_eq!(
2431 String::from_utf8_lossy(&ran.stdout).trim(),
2432 marker,
2433 "the artifact is this project's binary, not the sibling's"
2434 );
2435 }
2436 });
2437 }
2438
2439 #[test]
2444 fn reported_artifact_selects_the_matching_manifests_file() {
2445 let temporary = tempdir().expect("tempdir");
2446 let crate_dir = temporary.path().join("demo-hydrolysis-deadbeef");
2447 std::fs::create_dir_all(&crate_dir).expect("crate dir");
2448 std::fs::write(crate_dir.join("Cargo.toml"), "[package]\n").expect("manifest");
2449 let manifest =
2450 dunce::canonicalize(crate_dir.join("Cargo.toml")).expect("canonical manifest");
2451 let reported = crate_dir.join("target/debug/deps/demo_hydrolysis_deadbeef-abc123.rlib");
2452 std::fs::create_dir_all(reported.parent().expect("deps dir")).expect("deps dir");
2453 std::fs::write(&reported, []).expect("reported artifact");
2454
2455 let artifact_json = |manifest: &std::path::Path, file: &std::path::Path, name: &str| {
2459 serde_json::json!({
2460 "reason": "compiler-artifact",
2461 "package_id": format!("path+file:///x#{name}@0.1.0"),
2462 "manifest_path": manifest,
2463 "target": {
2464 "kind": ["lib"],
2465 "crate_types": ["lib"],
2466 "name": name,
2467 "src_path": manifest.parent().expect("manifest dir").join("src/lib.rs"),
2468 "edition": "2021",
2469 "doc": true,
2470 "doctest": true,
2471 "test": true,
2472 },
2473 "profile": {
2474 "opt_level": "0",
2475 "debuginfo": 0,
2476 "debug_assertions": true,
2477 "overflow_checks": true,
2478 "test": false,
2479 },
2480 "features": [],
2481 "filenames": [file],
2482 "executable": null,
2483 "fresh": true,
2484 })
2485 .to_string()
2486 };
2487
2488 let other_manifest = temporary.path().join("other").join("Cargo.toml");
2489 let other_file = temporary.path().join("other.rlib");
2490 let stdout = format!(
2491 "{}\n{}\n",
2492 artifact_json(&other_manifest, &other_file, "other"),
2493 artifact_json(&manifest, &reported, "demo_hydrolysis_deadbeef"),
2494 );
2495 let resolved = super::reported_artifact(
2496 stdout.as_bytes(),
2497 &crate_dir,
2498 CargoTarget::Lib,
2499 Some("rlib"),
2500 )
2501 .expect("the matching manifest's artifact resolves");
2502 assert_eq!(resolved, reported);
2503
2504 let foreign_only = artifact_json(&other_manifest, &other_file, "other");
2505 assert!(
2506 super::reported_artifact(
2507 foreign_only.as_bytes(),
2508 &crate_dir,
2509 CargoTarget::Lib,
2510 Some("rlib"),
2511 )
2512 .is_err(),
2513 "an artifact for another manifest is never selected"
2514 );
2515 }
2516
2517 #[test]
2522 fn stale_shared_dylib_packages_flags_a_foreign_written_artifact() {
2523 smol::block_on(async {
2524 let temporary = tempdir().expect("tempdir");
2525 let deps = temporary.path().join("debug/deps");
2526 std::fs::create_dir_all(&deps).expect("deps dir");
2527 let dylib = deps.join("libwaterui_dylib.so");
2528 std::fs::write(&dylib, []).expect("dylib");
2529
2530 let ours = temporary.path().join("our project");
2534 std::fs::create_dir_all(ours.join("src")).expect("our manifest dir");
2535 let manifest = ours.join("Cargo.toml");
2536 std::fs::write(&manifest, "").expect("manifest");
2537 let own_source = ours.join("src/lib.rs");
2538 std::fs::write(&own_source, "").expect("own source");
2539
2540 let artifact = |fresh: bool| {
2541 serde_json::json!({
2542 "reason": "compiler-artifact",
2543 "package_id": "path+file:///x#waterui-dylib@0.1.0",
2544 "manifest_path": manifest,
2545 "target": {
2546 "kind": ["lib"],
2547 "crate_types": ["dylib"],
2548 "name": "waterui_dylib",
2549 "src_path": own_source,
2550 "edition": "2021",
2551 "doc": true,
2552 "doctest": true,
2553 "test": true,
2554 },
2555 "profile": {
2556 "opt_level": "0",
2557 "debuginfo": 0,
2558 "debug_assertions": true,
2559 "overflow_checks": true,
2560 "test": false,
2561 },
2562 "features": [],
2563 "filenames": [dylib],
2564 "executable": null,
2565 "fresh": fresh,
2566 })
2567 .to_string()
2568 };
2569 let dep_info = deps.join("waterui_dylib.d");
2570
2571 let foreign = temporary.path().join("foreign");
2575 std::fs::create_dir_all(foreign.join("src")).expect("foreign source dir");
2576 let foreign_source = foreign.join("src/lib.rs");
2577 std::fs::write(&foreign_source, "").expect("foreign source");
2578 let dep_escape =
2579 |path: &std::path::Path| path.display().to_string().replace(' ', "\\ ");
2580 let write_dep_info = |source: &std::path::Path| {
2581 std::fs::write(
2582 &dep_info,
2583 format!("{}: {}\n", dep_escape(&dylib), dep_escape(source)),
2584 )
2585 .expect("dep-info");
2586 };
2587
2588 write_dep_info(&foreign_source);
2590 let stale = super::stale_shared_dylib_packages(artifact(true).as_bytes())
2591 .await
2592 .expect("scan");
2593 assert_eq!(stale, ["waterui-dylib"]);
2594
2595 write_dep_info(&own_source);
2597 let stale = super::stale_shared_dylib_packages(artifact(true).as_bytes())
2598 .await
2599 .expect("scan");
2600 assert!(stale.is_empty(), "our own artifact is never stale");
2601
2602 write_dep_info(&foreign_source);
2604 let stale = super::stale_shared_dylib_packages(artifact(false).as_bytes())
2605 .await
2606 .expect("scan");
2607 assert!(stale.is_empty(), "a non-fresh unit wrote the file itself");
2608 });
2609 }
2610
2611 #[test]
2617 fn stale_check_reads_build_dir_dep_info_and_skips_proc_macros() {
2618 smol::block_on(async {
2619 let temporary = tempdir().expect("tempdir");
2620 let profile = temporary.path().join("debug");
2621 let unit_dir = profile.join("build/waterui-dylib/0123456789abcdef/out");
2622 std::fs::create_dir_all(&unit_dir).expect("unit dir");
2623 let dylib = profile.join("libwaterui_dylib.so");
2624 std::fs::write(&dylib, []).expect("dylib");
2625 let rmeta = unit_dir.join("libwaterui_dylib.rmeta");
2626 std::fs::write(&rmeta, []).expect("rmeta");
2627
2628 let ours = temporary.path().join("ours");
2629 std::fs::create_dir_all(ours.join("src")).expect("our manifest dir");
2630 let manifest = ours.join("Cargo.toml");
2631 std::fs::write(&manifest, "").expect("manifest");
2632 let foreign = temporary.path().join("foreign/src/lib.rs");
2633 std::fs::create_dir_all(foreign.parent().expect("parent")).expect("foreign dir");
2634 std::fs::write(&foreign, []).expect("foreign source");
2635 std::fs::write(
2636 unit_dir.join("waterui_dylib.d"),
2637 format!("{}: {}\n", dylib.display(), foreign.display()),
2638 )
2639 .expect("dep-info");
2640
2641 let unit = |name: &str, crate_type: &str, filenames: Vec<&std::path::Path>| {
2642 serde_json::json!({
2643 "reason": "compiler-artifact",
2644 "package_id": format!("path+file:///x#{name}@0.1.0"),
2645 "manifest_path": manifest,
2646 "target": {
2647 "kind": [if crate_type == "proc-macro" { "proc-macro" } else { "lib" }],
2648 "crate_types": [crate_type],
2649 "name": name.replace('-', "_"),
2650 "src_path": ours.join("src/lib.rs"),
2651 "edition": "2021",
2652 "doc": true,
2653 "doctest": true,
2654 "test": true,
2655 },
2656 "profile": {
2657 "opt_level": "0",
2658 "debuginfo": 0,
2659 "debug_assertions": true,
2660 "overflow_checks": true,
2661 "test": false,
2662 },
2663 "features": [],
2664 "filenames": filenames,
2665 "executable": null,
2666 "fresh": true,
2667 })
2668 .to_string()
2669 };
2670 let macro_dylib = unit_dir.join("libthiserror_impl-0123456789abcdef.so");
2674 let stdout = format!(
2675 "{}\n{}\n",
2676 unit("thiserror-impl", "proc-macro", vec![¯o_dylib]),
2677 unit("waterui-dylib", "dylib", vec![&dylib, &rmeta]),
2678 );
2679 let stale = super::stale_shared_dylib_packages(stdout.as_bytes())
2680 .await
2681 .expect("scan");
2682 assert_eq!(stale, ["waterui-dylib"]);
2683
2684 std::fs::remove_file(unit_dir.join("waterui_dylib.d")).expect("remove dep-info");
2687 let error = super::stale_shared_dylib_packages(stdout.as_bytes())
2688 .await
2689 .expect_err("a fresh dylib without dep-info is an error");
2690 assert!(
2691 error.to_string().contains("no dep-info was found"),
2692 "{error}"
2693 );
2694 });
2695 }
2696
2697 #[test]
2702 fn dep_info_prerequisites_unescape_spaces_and_join_continued_rules() {
2703 let contents = concat!(
2704 "C:\\out\\app.dll: C:\\work\\my\\ app\\src\\lib.rs \\\n",
2705 " C:\\work\\my\\ app\\build.rs C:\\work\\cost$$.rs\n",
2706 "\n",
2707 "C:\\work\\my\\ app\\src\\lib.rs:\n",
2708 );
2709 assert_eq!(
2710 super::dep_info_prerequisites(contents),
2711 vec![
2712 PathBuf::from("C:\\work\\my app\\src\\lib.rs"),
2713 PathBuf::from("C:\\work\\my app\\build.rs"),
2714 PathBuf::from("C:\\work\\cost$$.rs"),
2715 ]
2716 );
2717 }
2718
2719 #[test]
2720 fn static_packaging_removes_only_staged_android_runtime_libraries() {
2721 smol::block_on(async {
2722 let directory = tempdir().expect("temporary Android runtime directory");
2723 let android_triple = triple("aarch64-linux-android");
2724 for file_name in [
2725 "libwaterui_dylib.so",
2726 "libstd-old.so",
2727 "libwaterui_app.so",
2728 "libc++_shared.so",
2729 ] {
2730 std::fs::write(directory.path().join(file_name), [])
2731 .expect("write staged runtime test file");
2732 }
2733
2734 RustDynamicLibraries::remove_staged(directory.path(), &android_triple)
2735 .await
2736 .expect("remove shared Rust runtime libraries");
2737
2738 assert!(!directory.path().join("libwaterui_dylib.so").exists());
2739 assert!(!directory.path().join("libstd-old.so").exists());
2740 assert!(directory.path().join("libwaterui_app.so").exists());
2741 assert!(directory.path().join("libc++_shared.so").exists());
2742 });
2743 }
2744}