1use std::path::{Path, PathBuf};
4
5use smol::{process::Command, unblock};
6use target_lexicon::{Environment, OperatingSystem, Triple};
7
8use crate::utils::{command, run_command};
9
10#[derive(Debug, Clone)]
12pub struct RustBuild {
13 path: PathBuf,
14 triple: Triple,
15 hot_reload: bool,
16}
17
18#[derive(Debug, Clone, Default)]
20pub struct BuildOptions {
21 release: bool,
22 hot_reload: bool,
23 output_dir: Option<std::path::PathBuf>,
24}
25
26impl BuildOptions {
27 #[must_use]
29 pub const fn new(release: bool, hot_reload: bool) -> Self {
30 Self {
31 release,
32 output_dir: None,
33 hot_reload,
34 }
35 }
36
37 #[must_use]
39 pub const fn is_hot_reload(&self) -> bool {
40 self.hot_reload
41 }
42
43 #[must_use]
45 pub const fn is_release(&self) -> bool {
46 self.release
47 }
48
49 #[must_use]
51 pub fn output_dir(&self) -> Option<&std::path::Path> {
52 self.output_dir.as_deref()
53 }
54
55 #[must_use]
57 pub fn with_output_dir(mut self, output_dir: impl Into<std::path::PathBuf>) -> Self {
58 self.output_dir = Some(output_dir.into());
59 self
60 }
61}
62
63#[derive(Debug, thiserror::Error)]
65pub enum RustBuildError {
66 #[error("Failed to execute cargo build: {0}")]
68 FailToExecuteCargoBuild(std::io::Error),
69
70 #[error("Failed to build Rust library: {0}")]
72 FailToBuildRustLibrary(std::io::Error),
73}
74
75impl RustBuild {
76 pub fn new(path: impl AsRef<Path>, triple: Triple, hot_reload: bool) -> Self {
78 Self {
79 path: path.as_ref().to_path_buf(),
80 triple,
81 hot_reload,
82 }
83 }
84
85 pub async fn dev_build(&self) -> Result<PathBuf, RustBuildError> {
95 self.build_lib(false).await
96 }
97
98 pub async fn release_build(&self) -> Result<PathBuf, RustBuildError> {
106 self.build_lib(true).await
107 }
108
109 pub async fn build_lib(&self, release: bool) -> Result<PathBuf, RustBuildError> {
117 self.build_inner(release).await
118 }
119
120 async fn build_inner(&self, release: bool) -> Result<PathBuf, RustBuildError> {
122 let mut cmd = Command::new("cargo");
123 let mut cmd = command(&mut cmd)
124 .arg("build")
125 .arg("--lib")
126 .args(["--target", self.triple.to_string().as_str()])
127 .current_dir(&self.path);
128
129 if self.hot_reload {
130 let mut rustflags = std::env::var("RUSTFLAGS").unwrap_or_default();
132 if !rustflags.is_empty() {
133 rustflags.push(' ');
134 }
135 rustflags.push_str("--cfg waterui_hot_reload_lib");
136 cmd.env("RUSTFLAGS", rustflags);
137 }
138
139 if self.triple.environment == Environment::Sim {
142 if let Some(clang_args) = self.bindgen_clang_args_for_simulator().await {
143 cmd = cmd.env("BINDGEN_EXTRA_CLANG_ARGS", clang_args);
144 }
145 }
146
147 if release {
148 cmd = cmd.arg("--release");
149 }
150
151 let status = cmd
152 .status()
153 .await
154 .map_err(RustBuildError::FailToExecuteCargoBuild)?;
155
156 if !status.success() {
157 return Err(RustBuildError::FailToBuildRustLibrary(
158 std::io::Error::other("Cargo build failed"),
159 ));
160 }
161
162 let build_path = self.path.clone();
165 let metadata = unblock(move || {
166 cargo_metadata::MetadataCommand::new()
167 .no_deps()
168 .current_dir(build_path)
169 .exec()
170 .map_err(|e| {
171 RustBuildError::FailToBuildRustLibrary(std::io::Error::new(
172 std::io::ErrorKind::InvalidData,
173 e,
174 ))
175 })
176 })
177 .await?;
178
179 let target_directory = metadata.target_directory.as_std_path();
180
181 let dir = target_directory
182 .join(self.triple.to_string())
183 .join(if release { "release" } else { "debug" });
184
185 Ok(dir)
186 }
187
188 async fn bindgen_clang_args_for_simulator(&self) -> Option<String> {
193 let (sdk_name, target_os) = match self.triple.operating_system {
194 OperatingSystem::IOS(_) => ("iphonesimulator", "ios"),
195 OperatingSystem::TvOS(_) => ("appletvsimulator", "tvos"),
196 OperatingSystem::WatchOS(_) => ("watchsimulator", "watchos"),
197 OperatingSystem::VisionOS(_) => ("xrsimulator", "xros"),
198 _ => return None,
199 };
200
201 let arch = match self.triple.architecture {
202 target_lexicon::Architecture::Aarch64(_) => "arm64",
203 target_lexicon::Architecture::X86_64 => "x86_64",
204 _ => return None,
205 };
206
207 let sdk_path = run_command("xcrun", ["--sdk", sdk_name, "--show-sdk-path"])
209 .await
210 .ok()
211 .map(|s| s.trim().to_string())?;
212
213 let min_version = match target_os {
215 "ios" | "tvos" => "17.0",
216 "watchos" => "10.0",
217 "xros" => "1.0",
218 _ => unimplemented!(),
219 };
220
221 Some(format!(
222 "--target={arch}-apple-{target_os}{min_version}-simulator -isysroot {sdk_path}"
223 ))
224 }
225}