waterui_cli/
build.rs

1//! Build system
2
3use 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/// Represents a Rust build for a specific target triple.
11#[derive(Debug, Clone)]
12pub struct RustBuild {
13    path: PathBuf,
14    triple: Triple,
15    hot_reload: bool,
16}
17
18/// Options for building Rust libraries.
19#[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    /// Create new build options
28    #[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    /// Whether to enable hot-reload support
38    #[must_use]
39    pub const fn is_hot_reload(&self) -> bool {
40        self.hot_reload
41    }
42
43    /// Whether to build in release mode
44    #[must_use]
45    pub const fn is_release(&self) -> bool {
46        self.release
47    }
48
49    /// Get the output directory, if specified
50    #[must_use]
51    pub fn output_dir(&self) -> Option<&std::path::Path> {
52        self.output_dir.as_deref()
53    }
54
55    /// Set the output directory where built libraries should be copied
56    #[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/// Errors that can occur during the Rust build process.
64#[derive(Debug, thiserror::Error)]
65pub enum RustBuildError {
66    /// Failed to execute cargo build.
67    #[error("Failed to execute cargo build: {0}")]
68    FailToExecuteCargoBuild(std::io::Error),
69
70    /// Cargo executed but failed to build the Rust library.
71    #[error("Failed to build Rust library: {0}")]
72    FailToBuildRustLibrary(std::io::Error),
73}
74
75impl RustBuild {
76    /// Create a new rust build for the given path and target triple.
77    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    /// Build rust library in development mode.
86    ///
87    /// Will produce debug symbols and less optimizations for faster builds.
88    ///
89    /// Return the path to the built library.
90    ///
91    /// # Errors
92    /// - `RustBuildError::FailToExecuteCargoBuild`: If there was an error executing the cargo build command.
93    /// - `RustBuildError::FailToBuildRustLibrary`: If there was an error building the Rust library.
94    pub async fn dev_build(&self) -> Result<PathBuf, RustBuildError> {
95        self.build_lib(false).await
96    }
97
98    /// Build rust library in release mode.
99    ///
100    /// Return the directory path containing the built library.
101    ///
102    /// # Errors
103    /// - `RustBuildError::FailToExecuteCargoBuild`: If there was an error executing the cargo build command.
104    /// - `RustBuildError::FailToBuildRustLibrary`: If there was an error building the Rust library.
105    pub async fn release_build(&self) -> Result<PathBuf, RustBuildError> {
106        self.build_lib(true).await
107    }
108
109    /// Build a library with the specified crate type.
110    ///
111    /// Return the directory path containing the built library.
112    ///
113    /// # Errors
114    /// - `RustBuildError::FailToExecuteCargoBuild`: If there was an error executing the cargo build command.
115    /// - `RustBuildError::FailToBuildRustLibrary`: If there was an error building the Rust library.
116    pub async fn build_lib(&self, release: bool) -> Result<PathBuf, RustBuildError> {
117        self.build_inner(release).await
118    }
119
120    /// Return target directory path
121    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            // Preserve existing RUSTFLAGS and append our cfg flag
131            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        // Set BINDGEN_EXTRA_CLANG_ARGS for iOS/tvOS/watchOS/visionOS simulator builds
140        // This fixes bindgen issues with the *-apple-*-sim target triples
141        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        // use `cargo metadata` to get the target directory
163
164        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    /// Generate `BINDGEN_EXTRA_CLANG_ARGS` for simulator builds.
189    ///
190    /// Bindgen has issues with the `*-apple-*-sim` target triples, so we need to
191    /// provide explicit clang arguments with a proper target and SDK path.
192    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        // Get SDK path using xcrun
208        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        // Use a reasonable minimum deployment target
214        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}