1use std::{
4 ffi::OsString,
5 path::{Path, PathBuf},
6};
7
8use eyre::bail;
9use futures_util::StreamExt as _;
10use smol::{process::Command, unblock};
11use target_lexicon::{Environment, OperatingSystem, Triple};
12
13use crate::project::Project;
14use crate::utils::{command, run_command};
15
16#[must_use]
18pub const fn lib_extension_for_triple(triple: &Triple) -> &'static str {
19 match triple.operating_system {
20 OperatingSystem::Darwin(_)
21 | OperatingSystem::MacOSX { .. }
22 | OperatingSystem::IOS(_)
23 | OperatingSystem::TvOS(_)
24 | OperatingSystem::WatchOS(_)
25 | OperatingSystem::VisionOS(_) => "dylib",
26 OperatingSystem::Windows => "dll",
27 _ => "so",
29 }
30}
31
32pub async fn rust_target_libdir(triple: &Triple) -> eyre::Result<PathBuf> {
37 let target = triple.to_string();
38 let output = run_command(
39 "rustc",
40 ["--print", "target-libdir", "--target", target.as_str()],
41 )
42 .await?;
43 let libdir = output.trim();
44 if libdir.is_empty() {
45 bail!("`rustc --print target-libdir --target {target}` returned an empty path");
46 }
47 let path = PathBuf::from(libdir);
48 if !path.is_dir() {
49 bail!(
50 "Rust target libdir does not exist for dynamic linking: {}",
51 path.display()
52 );
53 }
54 Ok(path)
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63enum CargoTarget<'a> {
64 Lib,
66 Binary(&'a str),
68}
69
70impl<'a> CargoTarget<'a> {
71 fn cargo_args(self) -> Vec<&'a str> {
72 match self {
73 Self::Lib => vec!["--lib"],
74 Self::Binary(name) => vec!["--bin", name],
75 }
76 }
77
78 const fn accepts_crate_type_override(self) -> bool {
79 matches!(self, Self::Lib)
80 }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum RustLinkage {
86 Static,
88 SharedRuntime,
90}
91
92pub fn configure_generated_crate_compilation(command: &mut Command) {
108 command.env("CARGO_INCREMENTAL", "0");
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct RustDynamicLibraries {
114 waterui: PathBuf,
115 standard_library: PathBuf,
116 triple: Triple,
117}
118
119impl RustDynamicLibraries {
120 pub async fn resolve(lib_dir: &Path, triple: &Triple) -> eyre::Result<Self> {
125 let file_name = dynamic_library_file_name("waterui_dylib", triple);
126 let waterui = [
129 lib_dir.join(&file_name),
130 lib_dir.join("deps").join(&file_name),
131 ]
132 .into_iter()
133 .find(|path| path.is_file())
134 .ok_or_else(|| {
135 eyre::eyre!(
136 "Shared WaterUI runtime was not built at {}",
137 lib_dir.join("deps").join(&file_name).display()
138 )
139 })?;
140
141 let target_libdir = rust_target_libdir(triple).await?;
142 let resolution_triple = triple.clone();
143 let standard_library =
144 unblock(move || resolve_rust_standard_library_in(&target_libdir, &resolution_triple))
145 .await?;
146
147 Ok(Self {
148 waterui,
149 standard_library,
150 triple: triple.clone(),
151 })
152 }
153
154 #[must_use]
156 pub fn waterui(&self) -> &Path {
157 &self.waterui
158 }
159
160 #[must_use]
162 pub fn standard_library(&self) -> &Path {
163 &self.standard_library
164 }
165
166 pub fn iter(&self) -> impl Iterator<Item = &Path> {
168 [self.waterui(), self.standard_library()].into_iter()
169 }
170
171 pub async fn stage(&self, destination: &Path) -> eyre::Result<()> {
182 smol::fs::create_dir_all(destination).await?;
183 Self::remove_staged(destination, &self.triple).await?;
184 for source in self.iter() {
185 let file_name = source.file_name().ok_or_else(|| {
186 eyre::eyre!(
187 "Dynamic library path has no file name: {}",
188 source.display()
189 )
190 })?;
191 crate::utils::copy_file(source, destination.join(file_name)).await?;
192 }
193 Ok(())
194 }
195
196 pub async fn remove_staged(destination: &Path, triple: &Triple) -> eyre::Result<()> {
201 if !destination.is_dir() {
202 return Ok(());
203 }
204
205 let waterui = dynamic_library_file_name("waterui_dylib", triple);
206 let (standard_library_prefix, extension) =
207 if triple.operating_system == OperatingSystem::Windows {
208 ("std-", "dll")
209 } else {
210 ("libstd-", lib_extension_for_triple(triple))
211 };
212 let mut entries = smol::fs::read_dir(destination).await?;
213 while let Some(entry) = entries.next().await {
214 let entry = entry?;
215 let file_name = entry.file_name();
216 let file_name = file_name.to_string_lossy();
217 if file_name == waterui
218 || (file_name.starts_with(standard_library_prefix)
219 && entry.path().extension().and_then(|value| value.to_str()) == Some(extension))
220 {
221 smol::fs::remove_file(entry.path()).await?;
222 }
223 }
224 Ok(())
225 }
226}
227
228fn dynamic_library_file_name(crate_name: &str, triple: &Triple) -> String {
229 if triple.operating_system == OperatingSystem::Windows {
230 format!("{crate_name}.dll")
231 } else {
232 format!("lib{crate_name}.{}", lib_extension_for_triple(triple))
233 }
234}
235
236fn resolve_rust_standard_library_in(libdir: &Path, triple: &Triple) -> eyre::Result<PathBuf> {
237 let (prefix, extension) = if triple.operating_system == OperatingSystem::Windows {
238 ("std-", "dll")
239 } else {
240 ("libstd-", lib_extension_for_triple(triple))
241 };
242 let entries = std::fs::read_dir(libdir)?
243 .map(|entry| entry.map(|entry| entry.path()))
244 .collect::<std::io::Result<Vec<_>>>()?;
245 let mut matches = entries
246 .into_iter()
247 .filter(|path| {
248 path.file_name()
249 .and_then(|name| name.to_str())
250 .is_some_and(|name| {
251 name.starts_with(prefix)
252 && path.extension().and_then(|extension| extension.to_str())
253 == Some(extension)
254 })
255 })
256 .collect::<Vec<_>>();
257 matches.sort_unstable();
258 match matches.as_slice() {
259 [path] => Ok(path.clone()),
260 [] => {
261 bail!(
262 "Rust target libdir {} contains no dynamic standard library for {triple}",
263 libdir.display()
264 );
265 }
266 _ => {
267 bail!(
268 "Rust target libdir {} contains multiple dynamic standard libraries for {triple}: {}",
269 libdir.display(),
270 matches
271 .iter()
272 .map(|path| path.display().to_string())
273 .collect::<Vec<_>>()
274 .join(", ")
275 );
276 }
277 }
278}
279
280#[derive(Debug, Clone)]
282pub struct RustBuild {
283 path: PathBuf,
284 triple: Triple,
285 project: Option<Project>,
286 target_dir: Option<PathBuf>,
288 sccache_path: Option<PathBuf>,
290 features: Vec<String>,
292 crate_type_override: Option<String>,
294 rustc_flags: Vec<String>,
296 final_rustc_args: Vec<String>,
305 envs: Vec<(String, OsString)>,
307}
308
309#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
315pub enum BuildProfile {
316 #[default]
318 Debug,
319 Optimized,
323 Release,
325 Profiling,
328}
329
330impl BuildProfile {
331 #[must_use]
334 pub const fn is_release(self) -> bool {
335 matches!(self, Self::Release | Self::Profiling)
336 }
337
338 #[must_use]
341 pub const fn is_development(self) -> bool {
342 !self.is_release()
343 }
344
345 fn development_envs(self) -> Vec<(String, OsString)> {
355 let entries: &[(&str, &str)] = match self {
356 Self::Debug => &[],
357 Self::Optimized => &[
358 ("CARGO_PROFILE_DEV_OPT_LEVEL", "1"),
359 ("CARGO_PROFILE_DEV_DEBUG", "true"),
360 ("CARGO_PROFILE_DEV_DEBUG_ASSERTIONS", "false"),
361 ("CARGO_PROFILE_DEV_OVERFLOW_CHECKS", "false"),
362 ],
363 Self::Release => &[("CARGO_PROFILE_RELEASE_OPT_LEVEL", "3")],
364 Self::Profiling => &[
365 ("CARGO_PROFILE_RELEASE_OPT_LEVEL", "3"),
366 ("CARGO_PROFILE_RELEASE_DEBUG", "true"),
367 ("CARGO_PROFILE_RELEASE_STRIP", "none"),
368 ],
369 };
370 entries
371 .iter()
372 .map(|(key, value)| ((*key).to_string(), OsString::from(*value)))
373 .collect()
374 }
375}
376
377#[derive(Debug, Clone)]
379pub struct BuildOptions {
380 profile: BuildProfile,
381 output_dir: Option<std::path::PathBuf>,
382 sccache_path: Option<std::path::PathBuf>,
384 target_triple: Option<Triple>,
386 linkage: RustLinkage,
388 dev_server: bool,
391 cargo_envs: Vec<(String, OsString)>,
393}
394
395impl BuildOptions {
396 #[must_use]
403 pub fn development(profile: BuildProfile) -> Self {
404 Self {
405 profile,
406 output_dir: None,
407 sccache_path: None,
408 target_triple: None,
409 linkage: RustLinkage::SharedRuntime,
410 dev_server: false,
411 cargo_envs: profile.development_envs(),
412 }
413 }
414
415 #[must_use]
421 pub const fn with_static_runtime(mut self) -> Self {
422 self.linkage = RustLinkage::Static;
423 self
424 }
425
426 #[must_use]
432 pub const fn packaging(profile: BuildProfile) -> Self {
433 Self {
434 profile,
435 output_dir: None,
436 sccache_path: None,
437 target_triple: None,
438 linkage: RustLinkage::Static,
439 dev_server: false,
440 cargo_envs: Vec::new(),
441 }
442 }
443
444 #[must_use]
446 pub const fn is_release(&self) -> bool {
447 self.profile.is_release()
448 }
449
450 #[must_use]
452 pub const fn profile(&self) -> BuildProfile {
453 self.profile
454 }
455
456 #[must_use]
458 pub fn cargo_envs(&self) -> &[(String, OsString)] {
459 &self.cargo_envs
460 }
461
462 #[must_use]
464 pub const fn with_dev_server(mut self, dev_server: bool) -> Self {
465 self.dev_server = dev_server;
466 self
467 }
468
469 #[must_use]
471 pub const fn uses_dev_server(&self) -> bool {
472 self.dev_server
473 }
474
475 #[must_use]
477 pub fn output_dir(&self) -> Option<&std::path::Path> {
478 self.output_dir.as_deref()
479 }
480
481 #[must_use]
483 pub fn with_output_dir(mut self, output_dir: impl Into<std::path::PathBuf>) -> Self {
484 self.output_dir = Some(output_dir.into());
485 self
486 }
487
488 #[must_use]
490 pub fn sccache_path(&self) -> Option<&std::path::Path> {
491 self.sccache_path.as_deref()
492 }
493
494 #[must_use]
499 pub fn with_sccache(mut self, sccache_path: impl Into<std::path::PathBuf>) -> Self {
500 self.sccache_path = Some(sccache_path.into());
501 self
502 }
503
504 #[must_use]
506 pub const fn target_triple(&self) -> Option<&Triple> {
507 self.target_triple.as_ref()
508 }
509
510 #[must_use]
512 pub fn with_target_triple(mut self, target_triple: Triple) -> Self {
513 self.target_triple = Some(target_triple);
514 self
515 }
516
517 #[must_use]
519 pub const fn linkage(&self) -> RustLinkage {
520 self.linkage
521 }
522}
523
524#[derive(Debug, thiserror::Error)]
526pub enum RustBuildError {
527 #[error("Failed to execute cargo build: {0}")]
529 FailToExecuteCargoBuild(std::io::Error),
530
531 #[error("Failed to build Rust library: {0}")]
533 FailToBuildRustLibrary(std::io::Error),
534}
535
536impl RustBuild {
537 pub fn new(path: impl AsRef<Path>, triple: Triple) -> Self {
539 Self {
540 path: path.as_ref().to_path_buf(),
541 triple,
542 project: None,
543 target_dir: None,
544 sccache_path: None,
545 features: Vec::new(),
546 crate_type_override: None,
547 rustc_flags: Vec::new(),
548 final_rustc_args: Vec::new(),
549 envs: Vec::new(),
550 }
551 }
552
553 pub(crate) fn with_project(mut self, project: &Project) -> Self {
554 self.project = Some(project.clone());
555 self
556 }
557
558 #[must_use]
560 pub fn with_target_dir(mut self, target_dir: impl Into<PathBuf>) -> Self {
561 self.target_dir = Some(target_dir.into());
562 self
563 }
564
565 #[must_use]
570 pub fn with_sccache(mut self, sccache_path: PathBuf) -> Self {
571 self.sccache_path = Some(sccache_path);
572 self
573 }
574
575 #[must_use]
579 pub fn with_feature(mut self, feature: impl Into<String>) -> Self {
580 self.features.push(feature.into());
581 self
582 }
583
584 #[must_use]
586 pub fn with_features(mut self, features: impl IntoIterator<Item = impl Into<String>>) -> Self {
587 self.features.extend(features.into_iter().map(Into::into));
588 self
589 }
590
591 #[must_use]
593 pub fn features(&self) -> &[String] {
594 &self.features
595 }
596
597 #[must_use]
599 pub fn with_rustc_flag(mut self, flag: impl Into<String>) -> Self {
600 self.rustc_flags.push(flag.into());
601 self
602 }
603
604 #[must_use]
611 pub fn with_final_rustc_arg(mut self, flag: impl Into<String>) -> Self {
612 self.final_rustc_args.push(flag.into());
613 self
614 }
615
616 #[must_use]
618 pub fn with_preferred_dynamic_linking(self) -> Self {
619 self.with_rustc_flag("-Cprefer-dynamic")
620 .with_rustc_flag("-Crpath")
621 }
622
623 #[must_use]
630 pub fn with_linkage(
631 self,
632 linkage: RustLinkage,
633 development_feature: &str,
634 loader_search_path: Option<&str>,
635 ) -> Self {
636 if linkage == RustLinkage::Static {
637 return self;
638 }
639 let build = self
640 .with_feature(development_feature)
641 .with_preferred_dynamic_linking();
642 match loader_search_path {
643 Some(path) => build.with_final_rustc_arg(format!("-Clink-arg=-Wl,-rpath,{path}")),
644 None => build,
645 }
646 }
647
648 #[must_use]
650 pub fn with_crate_type_override(mut self, crate_type: impl Into<String>) -> Self {
651 self.crate_type_override = Some(crate_type.into());
652 self
653 }
654
655 #[must_use]
657 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<OsString>) -> Self {
658 self.envs.push((key.into(), value.into()));
659 self
660 }
661
662 #[must_use]
664 pub fn with_envs(mut self, envs: impl IntoIterator<Item = (String, OsString)>) -> Self {
665 self.envs.extend(envs);
666 self
667 }
668
669 #[must_use]
671 pub const fn triple(&self) -> &Triple {
672 &self.triple
673 }
674
675 pub async fn dev_build(&self) -> Result<PathBuf, RustBuildError> {
685 self.build_lib(false).await
686 }
687
688 pub async fn release_build(&self) -> Result<PathBuf, RustBuildError> {
696 self.build_lib(true).await
697 }
698
699 pub async fn build_lib(&self, release: bool) -> Result<PathBuf, RustBuildError> {
707 self.build_inner(release, CargoTarget::Lib).await
708 }
709
710 pub async fn build_dylib(
719 &self,
720 crate_name: &str,
721 release: bool,
722 ) -> Result<PathBuf, RustBuildError> {
723 let lib_dir = self.build_inner(release, CargoTarget::Lib).await?;
724
725 let lib_name = crate_name.replace('-', "_");
726 let ext = lib_extension_for_triple(&self.triple);
727 let dylib_path = lib_dir.join(format!("lib{lib_name}.{ext}"));
728
729 if !dylib_path.exists() {
730 return Err(RustBuildError::FailToBuildRustLibrary(std::io::Error::new(
731 std::io::ErrorKind::NotFound,
732 format!(
733 "Dynamic library not found at {} after cargo build",
734 dylib_path.display()
735 ),
736 )));
737 }
738
739 Ok(dylib_path)
740 }
741
742 pub async fn build_binary(
748 &self,
749 binary_name: &str,
750 release: bool,
751 ) -> Result<PathBuf, RustBuildError> {
752 let output_dir = self
753 .build_inner(release, CargoTarget::Binary(binary_name))
754 .await?;
755 let binary_file_name = if self.triple.operating_system == OperatingSystem::Windows {
756 format!("{binary_name}.exe")
757 } else {
758 binary_name.to_string()
759 };
760 let binary_path = output_dir.join(binary_file_name);
761 if !binary_path.is_file() {
762 return Err(RustBuildError::FailToBuildRustLibrary(std::io::Error::new(
763 std::io::ErrorKind::NotFound,
764 format!(
765 "Binary not found at {} after cargo build",
766 binary_path.display()
767 ),
768 )));
769 }
770 Ok(binary_path)
771 }
772
773 pub async fn dylib_path(
781 &self,
782 crate_name: &str,
783 release: bool,
784 ) -> Result<PathBuf, RustBuildError> {
785 let lib_dir = self.lib_output_dir(release).await?;
786 let lib_name = crate_name.replace('-', "_");
787 let ext = lib_extension_for_triple(&self.triple);
788 Ok(lib_dir.join(format!("lib{lib_name}.{ext}")))
789 }
790
791 async fn build_inner(
793 &self,
794 release: bool,
795 cargo_target: CargoTarget<'_>,
796 ) -> Result<PathBuf, RustBuildError> {
797 let mut output = self.cargo_build_output(release, cargo_target).await?;
798
799 if !output.status.success() {
800 let mut combined = combined_build_output(&output);
801
802 if should_retry_after_cmake_generator_mismatch(&combined)
805 && self.clean_stale_cmake_build_dirs().await?
806 {
807 output = self.cargo_build_output(release, cargo_target).await?;
808 combined = combined_build_output(&output);
809 }
810
811 if !output.status.success() && should_auto_install_meson(&combined) {
812 match ensure_meson_installed_for_build().await {
813 Ok(()) => {
814 output = self.cargo_build_output(release, cargo_target).await?;
815 }
816 Err(install_err) => {
817 return Err(RustBuildError::FailToBuildRustLibrary(
818 std::io::Error::other(format!(
819 "Cargo build failed and meson appears missing.\n\
820Automatic meson installation failed: {install_err}\n\n{combined}"
821 )),
822 ));
823 }
824 }
825 }
826 }
827
828 if !output.status.success() {
829 let combined = combined_build_output(&output);
830 return Err(RustBuildError::FailToBuildRustLibrary(
831 std::io::Error::other(format!("Cargo build failed:\n{combined}")),
832 ));
833 }
834
835 self.lib_output_dir(release).await
836 }
837
838 async fn clean_stale_cmake_build_dirs(&self) -> Result<bool, RustBuildError> {
839 let target_dir = self.target_directory().await?;
840 let triple = self.triple.to_string();
841
842 let removed = unblock(move || {
843 let mut removed = 0usize;
844 removed +=
845 remove_cmake_build_dirs_in(&target_dir.join(&triple).join("debug").join("build"))?;
846 removed += remove_cmake_build_dirs_in(
847 &target_dir.join(&triple).join("release").join("build"),
848 )?;
849 Ok::<usize, std::io::Error>(removed)
850 })
851 .await
852 .map_err(|error| {
853 RustBuildError::FailToBuildRustLibrary(std::io::Error::other(format!(
854 "Failed to clean stale CMake cache: {error}"
855 )))
856 })?;
857
858 Ok(removed > 0)
859 }
860
861 async fn cargo_build_output(
862 &self,
863 release: bool,
864 cargo_target: CargoTarget<'_>,
865 ) -> Result<std::process::Output, RustBuildError> {
866 let framework = self.project.as_ref().and_then(|project| {
867 project
868 .manifest()
869 .framework
870 .as_ref()
871 .map(|framework| (project, framework))
872 });
873 if let Some((project, framework)) = framework {
874 framework
875 .prepare_build(project, &self.path, &self.features)
876 .await
877 .map_err(|error| {
878 RustBuildError::FailToBuildRustLibrary(std::io::Error::other(error.to_string()))
879 })?;
880 }
881 let crate_type_override = if cargo_target.accepts_crate_type_override() {
882 self.crate_type_override.as_deref()
883 } else {
884 None
885 };
886 let mut cmd = Command::new("cargo");
887 let cargo_subcommand = if crate_type_override.is_some() || !self.final_rustc_args.is_empty()
888 {
889 "rustc"
890 } else {
891 "build"
892 };
893 let mut cmd = command(&mut cmd)
894 .arg(cargo_subcommand)
895 .args(cargo_target.cargo_args())
896 .args(["--target", self.triple.to_string().as_str()])
897 .current_dir(&self.path);
898 if framework.is_some() {
899 cmd = cmd.arg("--locked");
900 }
901
902 if let Some(target_dir) = &self.target_dir {
903 cmd = cmd.arg("--target-dir").arg(target_dir);
904 }
905
906 for (key, value) in &self.envs {
908 cmd.env(key, value);
909 }
910
911 if !self.rustc_flags.is_empty() {
912 let mut rustflags = std::env::var_os("RUSTFLAGS").unwrap_or_default();
913 if !rustflags.is_empty() {
914 rustflags.push(" ");
915 }
916 rustflags.push(self.rustc_flags.join(" "));
917 cmd = cmd.env("RUSTFLAGS", rustflags);
918 }
919
920 configure_generated_crate_compilation(cmd);
921
922 if let Some(sccache_path) = &self.sccache_path {
924 crate::toolchain::sccache::configure_compilation_cache(cmd, sccache_path);
925 }
926
927 if self.triple.environment == Environment::Sim
934 && let Some(clang_args) = self.bindgen_clang_args_for_simulator().await
935 {
936 let bindgen_target_key = format!(
937 "BINDGEN_EXTRA_CLANG_ARGS_{}",
938 self.triple.to_string().replace('-', "_")
939 );
940 cmd = cmd.env(bindgen_target_key, clang_args);
941 }
942
943 if release {
944 cmd = cmd.arg("--release");
945 }
946
947 if !self.features.is_empty() {
949 cmd = cmd.args(["--features", &self.features.join(",")]);
950 }
951
952 if crate_type_override.is_some() || !self.final_rustc_args.is_empty() {
953 cmd = cmd.arg("--");
954 if let Some(crate_type) = crate_type_override {
955 cmd = cmd.arg("--crate-type").arg(crate_type);
956 }
957 cmd = cmd.args(&self.final_rustc_args);
958 }
959
960 let output = cmd
961 .output()
962 .await
963 .map_err(RustBuildError::FailToExecuteCargoBuild)?;
964 Ok(output)
965 }
966
967 pub async fn lib_output_dir(&self, release: bool) -> Result<PathBuf, RustBuildError> {
972 let target_directory = self.target_directory().await?;
973 Ok(target_directory
974 .join(self.triple.to_string())
975 .join(if release { "release" } else { "debug" }))
976 }
977
978 async fn target_directory(&self) -> Result<PathBuf, RustBuildError> {
979 if let Some(target_dir) = &self.target_dir {
980 return Ok(target_dir.clone());
981 }
982
983 let build_path = self.path.clone();
984 let metadata = unblock(move || {
985 cargo_metadata::MetadataCommand::new()
986 .no_deps()
987 .current_dir(build_path)
988 .exec()
989 .map_err(|e| {
990 RustBuildError::FailToBuildRustLibrary(std::io::Error::new(
991 std::io::ErrorKind::InvalidData,
992 e,
993 ))
994 })
995 })
996 .await?;
997 Ok(metadata.target_directory.as_std_path().to_path_buf())
998 }
999
1000 async fn bindgen_clang_args_for_simulator(&self) -> Option<String> {
1005 let (sdk_name, target_os) = match self.triple.operating_system {
1006 OperatingSystem::IOS(_) => ("iphonesimulator", "ios"),
1007 OperatingSystem::TvOS(_) => ("appletvsimulator", "tvos"),
1008 OperatingSystem::WatchOS(_) => ("watchsimulator", "watchos"),
1009 OperatingSystem::VisionOS(_) => ("xrsimulator", "xros"),
1010 _ => return None,
1011 };
1012
1013 let arch = match self.triple.architecture {
1014 target_lexicon::Architecture::Aarch64(_) => "arm64",
1015 target_lexicon::Architecture::X86_64 => "x86_64",
1016 _ => return None,
1017 };
1018
1019 let sdk_path = run_command("xcrun", ["--sdk", sdk_name, "--show-sdk-path"])
1021 .await
1022 .ok()
1023 .map(|s| s.trim().to_string())?;
1024
1025 let min_version = if matches!(target_os, "ios" | "tvos") {
1027 "17.0"
1028 } else if target_os == "watchos" {
1029 "10.0"
1030 } else {
1031 debug_assert_eq!(
1032 target_os, "xros",
1033 "bindgen simulator target_os must be one of ios/tvos/watchos/xros"
1034 );
1035 "1.0"
1036 };
1037
1038 Some(format!(
1039 "--target={arch}-apple-{target_os}{min_version}-simulator -isysroot {sdk_path}"
1040 ))
1041 }
1042}
1043
1044fn combined_build_output(output: &std::process::Output) -> String {
1045 let stderr = String::from_utf8_lossy(&output.stderr);
1046 let stdout = String::from_utf8_lossy(&output.stdout);
1047 if stderr.is_empty() {
1048 stdout.to_string()
1049 } else {
1050 stderr.to_string()
1051 }
1052}
1053
1054fn should_auto_install_meson(build_output: &str) -> bool {
1055 let lower = build_output.to_ascii_lowercase();
1056 lower.contains("meson")
1057 && (lower.contains("not found")
1058 || lower.contains("no such file")
1059 || lower.contains("failed to execute")
1060 || lower.contains("is required"))
1061}
1062
1063fn should_retry_after_cmake_generator_mismatch(build_output: &str) -> bool {
1064 let lower = build_output.to_ascii_lowercase();
1065 lower.contains("cmake error") && lower.contains("does not match the generator used previously")
1066}
1067
1068fn remove_cmake_build_dirs_in(build_root: &Path) -> std::io::Result<usize> {
1069 if !build_root.exists() {
1070 return Ok(0);
1071 }
1072
1073 let mut removed = 0usize;
1074 for entry in std::fs::read_dir(build_root)? {
1075 let entry = entry?;
1076 let path = entry.path();
1077 if !path.is_dir() {
1078 continue;
1079 }
1080
1081 let cmake_build_dir = path.join("out").join("build");
1082 if cmake_build_dir.join("CMakeCache.txt").exists() {
1083 std::fs::remove_dir_all(cmake_build_dir)?;
1084 removed += 1;
1085 }
1086 }
1087
1088 Ok(removed)
1089}
1090
1091#[cfg(target_os = "macos")]
1092async fn ensure_meson_installed_for_build() -> Result<(), String> {
1093 use crate::toolchain::meson::Meson;
1094 use crate::toolchain::{Installation as _, Toolchain as _, ToolchainError};
1095
1096 let host = crate::toolchain::Host::current();
1097 match Meson.check(&host).await {
1098 Ok(()) => Ok(()),
1099 Err(ToolchainError::Fixable(installation)) => {
1100 installation.install(&host).await.map_err(|e| e.to_string())
1101 }
1102 Err(ToolchainError::Unfixable(e)) => Err(e.to_string()),
1103 }
1104}
1105
1106#[cfg(not(target_os = "macos"))]
1107fn ensure_meson_installed_for_build() -> impl std::future::Future<Output = Result<(), String>> {
1108 std::future::ready(Err(
1109 "automatic meson installation is only supported on macOS".to_string(),
1110 ))
1111}
1112
1113#[cfg(test)]
1114mod tests {
1115 use target_lexicon::Triple;
1116 use tempfile::tempdir;
1117
1118 use std::ffi::OsString;
1119
1120 use super::{
1121 BuildOptions, BuildProfile, CargoTarget, RustDynamicLibraries, RustLinkage,
1122 dynamic_library_file_name, lib_extension_for_triple, resolve_rust_standard_library_in,
1123 };
1124
1125 fn triple(value: &str) -> Triple {
1126 value.parse().expect("test target triple must parse")
1127 }
1128
1129 #[test]
1130 fn crate_type_override_applies_only_to_library_targets() {
1131 assert!(CargoTarget::Lib.accepts_crate_type_override());
1132 assert!(!CargoTarget::Binary("waterui-cef-helper").accepts_crate_type_override());
1133 assert_eq!(CargoTarget::Lib.cargo_args(), ["--lib"]);
1134 assert_eq!(
1135 CargoTarget::Binary("waterui-cef-helper").cargo_args(),
1136 ["--bin", "waterui-cef-helper"]
1137 );
1138 }
1139
1140 #[test]
1141 fn apple_platform_dylibs_use_macho_extension() {
1142 assert_eq!(
1143 lib_extension_for_triple(&triple("aarch64-apple-darwin")),
1144 "dylib"
1145 );
1146 assert_eq!(
1147 lib_extension_for_triple(&triple("aarch64-apple-ios-sim")),
1148 "dylib"
1149 );
1150 assert_eq!(
1151 lib_extension_for_triple(&triple("aarch64-apple-ios")),
1152 "dylib"
1153 );
1154 }
1155
1156 #[test]
1157 fn non_apple_platform_dylibs_keep_platform_extensions() {
1158 assert_eq!(
1159 lib_extension_for_triple(&triple("aarch64-linux-android")),
1160 "so"
1161 );
1162 assert_eq!(
1163 lib_extension_for_triple(&triple("x86_64-unknown-linux-gnu")),
1164 "so"
1165 );
1166 assert_eq!(
1167 lib_extension_for_triple(&triple("x86_64-pc-windows-msvc")),
1168 "dll"
1169 );
1170 }
1171
1172 #[test]
1173 fn development_and_packaging_have_distinct_linkage() {
1174 assert_eq!(
1175 BuildOptions::development(BuildProfile::Debug).linkage(),
1176 RustLinkage::SharedRuntime
1177 );
1178 assert_eq!(
1179 BuildOptions::packaging(BuildProfile::Debug).linkage(),
1180 RustLinkage::Static
1181 );
1182 assert!(BuildOptions::development(BuildProfile::Release).is_release());
1183 assert!(BuildOptions::packaging(BuildProfile::Release).is_release());
1184 }
1185
1186 #[test]
1187 fn build_profile_release_variants_select_the_release_profile() {
1188 assert!(BuildProfile::Release.is_release());
1189 assert!(BuildProfile::Profiling.is_release());
1190 assert!(!BuildProfile::Debug.is_release());
1191 assert!(!BuildProfile::Optimized.is_release());
1192 }
1193
1194 #[test]
1195 fn development_profile_envs_realize_the_selected_trade_off() {
1196 let optimized = BuildOptions::development(BuildProfile::Optimized);
1197 let envs = optimized.cargo_envs();
1198 assert!(
1199 envs.contains(&(
1200 "CARGO_PROFILE_DEV_OPT_LEVEL".to_string(),
1201 OsString::from("1")
1202 )),
1203 "optimized development lifts the dev opt-level: {envs:?}"
1204 );
1205 assert!(
1206 envs.contains(&(
1207 "CARGO_PROFILE_DEV_DEBUG_ASSERTIONS".to_string(),
1208 OsString::from("false")
1209 )),
1210 "optimized development drops dep debug assertions: {envs:?}"
1211 );
1212 assert!(
1213 envs.contains(&(
1214 "CARGO_PROFILE_DEV_DEBUG".to_string(),
1215 OsString::from("true")
1216 )),
1217 "optimized development keeps full debug info: {envs:?}"
1218 );
1219
1220 let profiling = BuildOptions::development(BuildProfile::Profiling);
1221 let envs = profiling.cargo_envs();
1222 for key in [
1223 "CARGO_PROFILE_RELEASE_OPT_LEVEL",
1224 "CARGO_PROFILE_RELEASE_DEBUG",
1225 "CARGO_PROFILE_RELEASE_STRIP",
1226 ] {
1227 assert!(
1228 envs.iter().any(|(env_key, _)| env_key == key),
1229 "profiling keeps debug info and symbols: missing {key} in {envs:?}"
1230 );
1231 }
1232
1233 assert!(
1234 BuildOptions::development(BuildProfile::Debug)
1235 .cargo_envs()
1236 .is_empty(),
1237 "plain debug runs the declared dev profile"
1238 );
1239 }
1240
1241 #[test]
1242 fn packaging_never_overrides_the_declared_profile() {
1243 for profile in [
1244 BuildProfile::Debug,
1245 BuildProfile::Optimized,
1246 BuildProfile::Release,
1247 BuildProfile::Profiling,
1248 ] {
1249 assert!(
1250 BuildOptions::packaging(profile).cargo_envs().is_empty(),
1251 "packaging {profile:?} must ship the declared profile"
1252 );
1253 }
1254 }
1255
1256 #[test]
1257 fn resolves_target_standard_library_without_guessing_hash() {
1258 let directory = tempdir().expect("temporary target libdir");
1259 let android_triple = triple("aarch64-linux-android");
1260 let expected = directory.path().join("libstd-1234567890abcdef.so");
1261 std::fs::write(&expected, []).expect("write test std library");
1262 std::fs::write(directory.path().join("libcore.rlib"), []).expect("write unrelated library");
1263
1264 assert_eq!(
1265 resolve_rust_standard_library_in(directory.path(), &android_triple)
1266 .expect("resolve dynamic std"),
1267 expected
1268 );
1269 assert_eq!(
1270 dynamic_library_file_name("waterui_dylib", &android_triple),
1271 "libwaterui_dylib.so"
1272 );
1273 assert_eq!(
1274 dynamic_library_file_name("waterui_dylib", &triple("x86_64-pc-windows-msvc")),
1275 "waterui_dylib.dll"
1276 );
1277 }
1278
1279 #[test]
1280 fn static_packaging_removes_only_staged_android_runtime_libraries() {
1281 smol::block_on(async {
1282 let directory = tempdir().expect("temporary Android runtime directory");
1283 let android_triple = triple("aarch64-linux-android");
1284 for file_name in [
1285 "libwaterui_dylib.so",
1286 "libstd-old.so",
1287 "libwaterui_app.so",
1288 "libc++_shared.so",
1289 ] {
1290 std::fs::write(directory.path().join(file_name), [])
1291 .expect("write staged runtime test file");
1292 }
1293
1294 RustDynamicLibraries::remove_staged(directory.path(), &android_triple)
1295 .await
1296 .expect("remove shared Rust runtime libraries");
1297
1298 assert!(!directory.path().join("libwaterui_dylib.so").exists());
1299 assert!(!directory.path().join("libstd-old.so").exists());
1300 assert!(directory.path().join("libwaterui_app.so").exists());
1301 assert!(directory.path().join("libc++_shared.so").exists());
1302 });
1303 }
1304}