waterui_cli/winui/
platform.rs1use std::ffi::OsString;
7use std::path::{Path, PathBuf};
8
9use eyre::bail;
10use futures_util::StreamExt as _;
11use smol::fs;
12use tracing::info;
13
14use crate::{
15 assets,
16 build::{BuildOptions, BuildProgress, RustBuild, RustDynamicLibraries, RustLinkage},
17 device::Artifact,
18 platform::{PackageOptions, TargetPlatform},
19 project::Project,
20 utils::run_command_os,
21 winui::backend::WinUiBackend,
22};
23
24#[cfg(target_os = "windows")]
25const WINUI_INIT_HINT: &str = "water run --platform windows --backend winui";
26#[cfg(not(target_os = "windows"))]
27const WINUI_INIT_HINT: &str = "initialize WinUI backend on Windows";
28
29async fn winui_profile_dir(
37 project: &Project,
38 profile: &str,
39 linkage: RustLinkage,
40) -> eyre::Result<PathBuf> {
41 Ok(project
42 .water_target_dir(linkage)
43 .await?
44 .join(TargetPlatform::Windows.triple().to_string())
45 .join(profile))
46}
47
48pub async fn build_winui(project: &Project, options: BuildOptions) -> eyre::Result<PathBuf> {
53 ensure_windows_host()?;
54
55 let backend_path = project.backend_path::<WinUiBackend>();
56 let cargo_toml = backend_path.join("Cargo.toml");
57
58 if !cargo_toml.exists() {
59 bail!(
60 "WinUI backend not found at {}. Run `{WINUI_INIT_HINT}` to initialize it.",
61 backend_path.display(),
62 );
63 }
64
65 copy_assets_and_fonts(
68 project,
69 &backend_path,
70 options.sccache_path(),
71 options.uses_dev_server(),
72 options.progress(),
73 )
74 .await?;
75
76 let mut build = RustBuild::new(&backend_path, TargetPlatform::Windows.triple())
77 .with_project(project)
78 .with_target_dir(project.water_target_dir(options.linkage()).await?)
79 .with_linkage(
80 options.linkage(),
81 &format!("{}/dev", project.crate_name()),
82 None,
83 )
84 .with_envs(options.cargo_envs().iter().cloned());
85 if let Some(sccache_path) = options.sccache_path() {
86 build = build.with_sccache(sccache_path.to_path_buf());
87 }
88 if let Some(progress) = options.progress() {
89 build = build.with_progress(progress.clone());
90 }
91 build
92 .build_binary(
93 project.winui_backend_crate_name().as_str(),
94 options.is_release(),
95 )
96 .await
97 .map_err(|error| eyre::eyre!("Failed to build WinUI backend with cargo: {error}"))?;
98
99 build
100 .lib_output_dir(options.is_release())
101 .await
102 .map_err(Into::into)
103}
104
105pub async fn clean_winui(project: &Project) -> eyre::Result<()> {
114 ensure_windows_host()?;
115
116 let backend_path = project.backend_path::<WinUiBackend>();
117 let cargo_toml = backend_path.join("Cargo.toml");
118 if !cargo_toml.exists() {
119 return Ok(()); }
121
122 for linkage in [RustLinkage::SharedRuntime, RustLinkage::Static] {
126 let backend_target_dir = project.water_target_dir(linkage).await?;
127 if !backend_target_dir.exists() {
128 continue;
129 }
130 let args: Vec<OsString> = vec![
131 "clean".into(),
132 "--manifest-path".into(),
133 cargo_toml.as_os_str().to_owned(),
134 "--target-dir".into(),
135 backend_target_dir.as_os_str().to_owned(),
136 "--package".into(),
137 project.winui_backend_crate_name().as_str().into(),
138 ];
139 run_command_os("cargo", args).await?;
140 }
141
142 Ok(())
143}
144
145pub async fn package_winui(project: &Project, options: PackageOptions) -> eyre::Result<Artifact> {
154 ensure_windows_host()?;
155
156 let profile = if options.is_debug() {
157 "debug"
158 } else {
159 "release"
160 };
161 let backend_path = project.backend_path::<WinUiBackend>();
162
163 copy_assets_and_fonts(
165 project,
166 &backend_path,
167 None,
168 options.uses_dev_server(),
169 options.progress(),
170 )
171 .await?;
172
173 let linkage = if options.uses_shared_rust_runtime() {
174 RustLinkage::SharedRuntime
175 } else {
176 RustLinkage::Static
177 };
178 let target_dir = winui_profile_dir(project, profile, linkage).await?;
179
180 let binary_name = project.winui_backend_crate_name();
182
183 let binary_path = target_dir.join(format!("{binary_name}.exe"));
184
185 let final_binary_path = if binary_path.exists() {
186 binary_path
187 } else {
188 let alt_binary_name = binary_name.replace('-', "_");
189 let alt_binary_path = target_dir.join(format!("{alt_binary_name}.exe"));
190
191 if alt_binary_path.exists() {
192 alt_binary_path
193 } else {
194 bail!(
195 "Built WinUI binary not found at {}. Did you run build first?",
196 binary_path.display()
197 );
198 }
199 };
200
201 let runtime_dir =
206 crate::platforming::packaging::dist_dir(&backend_path, "windows", Some(profile));
207 fs::create_dir_all(&runtime_dir).await?;
208
209 let staged_resources = backend_path.join("resources");
212 if staged_resources.is_dir() {
213 copy_dir(&staged_resources, &runtime_dir.join("resources")).await?;
214 }
215
216 if options.uses_shared_rust_runtime() {
217 RustDynamicLibraries::resolve(&target_dir, &TargetPlatform::Windows.triple(), project)
218 .await?
219 .stage(&runtime_dir)
220 .await?;
221 } else {
222 RustDynamicLibraries::remove_staged(&runtime_dir, &TargetPlatform::Windows.triple())
223 .await?;
224 }
225
226 let packaged_binary = crate::platforming::packaging::stage_binary_as(
229 &final_binary_path,
230 &runtime_dir,
231 &format!("{}.exe", project.winui_binary_name()),
232 )
233 .await?;
234
235 Ok(Artifact::new(project.bundle_identifier(), packaged_binary))
236}
237
238#[must_use]
244pub const fn is_winui_platform(platform: TargetPlatform) -> bool {
245 matches!(platform, TargetPlatform::Windows)
246}
247
248fn ensure_windows_host() -> eyre::Result<()> {
249 if cfg!(target_os = "windows") {
250 Ok(())
251 } else {
252 bail!("WinUI backend is only supported on Windows hosts");
253 }
254}
255
256async fn copy_dir(source: &Path, destination: &Path) -> eyre::Result<()> {
258 let mut stack = vec![(source.to_path_buf(), destination.to_path_buf())];
259 while let Some((source, destination)) = stack.pop() {
260 fs::create_dir_all(&destination).await?;
261 let mut entries = fs::read_dir(&source).await?;
262 while let Some(entry) = entries.next().await {
263 let entry = entry?;
264 let target = destination.join(entry.file_name());
265 if entry.path().is_dir() {
266 stack.push((entry.path(), target));
267 } else {
268 fs::copy(entry.path(), &target).await?;
269 }
270 }
271 }
272 Ok(())
273}
274
275async fn copy_assets_and_fonts(
285 project: &Project,
286 backend_path: &Path,
287 sccache_path: Option<&Path>,
288 dev_server: bool,
289 progress: Option<&BuildProgress>,
290) -> eyre::Result<()> {
291 let resources_dir = backend_path.join("resources");
292 fs::create_dir_all(&resources_dir).await?;
293
294 let manifest = assets::stage_project_assets_for_gtk(
296 project,
297 &resources_dir,
298 sccache_path,
299 dev_server,
300 progress,
301 )
302 .await?;
303
304 fs::write(
307 backend_path.join("app-icon.ico"),
308 assets::project_windows_ico(project)?,
309 )
310 .await?;
311
312 let font_declarations = assets::scan_fonts(project).await?;
314 let mut resolved_fonts = assets::resolve_fonts(font_declarations).await?;
315 resolved_fonts.extend(assets::scan_project_font_assets(&manifest)?);
316
317 if !resolved_fonts.is_empty() {
318 let fonts_dest = resources_dir.join("fonts");
320 assets::copy_fonts(&resolved_fonts, &fonts_dest).await?;
321
322 info!("Copied {} fonts to WinUI resources", resolved_fonts.len());
323 }
324
325 Ok(())
326}