waterui_cli/winui/
platform.rs1use std::ffi::OsString;
7use std::path::Path;
8
9use eyre::bail;
10use futures_util::StreamExt as _;
11use smol::fs;
12use tracing::info;
13
14use crate::{
15 assets,
16 build::{
17 BuildOptions, BuildProgress, BuiltTarget, RustBuild, RustDynamicLibraries, RustLinkage,
18 },
19 device::Artifact,
20 platform::{PackageOptions, TargetPlatform},
21 project::Project,
22 utils::run_command_os,
23 winui::backend::WinUiBackend,
24};
25
26#[cfg(target_os = "windows")]
27const WINUI_INIT_HINT: &str = "water run --platform windows --backend winui";
28#[cfg(not(target_os = "windows"))]
29const WINUI_INIT_HINT: &str = "initialize WinUI backend on Windows";
30
31pub async fn build_winui(project: &Project, options: BuildOptions) -> eyre::Result<BuiltTarget> {
40 ensure_windows_host()?;
41
42 let backend_path = project.backend_path::<WinUiBackend>();
43 let cargo_toml = backend_path.join("Cargo.toml");
44
45 if !cargo_toml.exists() {
46 bail!(
47 "WinUI backend not found at {}. Run `{WINUI_INIT_HINT}` to initialize it.",
48 backend_path.display(),
49 );
50 }
51
52 copy_assets_and_fonts(
55 project,
56 &backend_path,
57 options.sccache_path(),
58 options.uses_dev_server(),
59 options.progress(),
60 )
61 .await?;
62
63 let mut build = RustBuild::new(&backend_path, TargetPlatform::Windows.triple())
64 .with_project(project)
65 .with_target_dir(project.water_target_dir(options.linkage()).await?)
66 .with_linkage(
67 options.linkage(),
68 &format!("{}/dev", project.crate_name()),
69 &[],
70 )
71 .with_envs(options.cargo_envs().iter().cloned());
72 if let Some(sccache_path) = options.sccache_path() {
73 build = build.with_sccache(sccache_path.to_path_buf());
74 }
75 if let Some(progress) = options.progress() {
76 build = build.with_progress(progress.clone());
77 }
78 let built_target = build
79 .build_binary(
80 project.winui_backend_crate_name().as_str(),
81 options.is_release(),
82 )
83 .await
84 .map_err(|error| eyre::eyre!("Failed to build WinUI backend with cargo: {error}"))?;
85 Ok(built_target)
86}
87
88pub async fn clean_winui(project: &Project) -> eyre::Result<()> {
97 ensure_windows_host()?;
98
99 let backend_path = project.backend_path::<WinUiBackend>();
100 let cargo_toml = backend_path.join("Cargo.toml");
101 if !cargo_toml.exists() {
102 return Ok(()); }
104
105 for linkage in [RustLinkage::SharedRuntime, RustLinkage::Static] {
109 let backend_target_dir = project.water_target_dir(linkage).await?;
110 if !backend_target_dir.exists() {
111 continue;
112 }
113 let args: Vec<OsString> = vec![
114 "clean".into(),
115 "--manifest-path".into(),
116 cargo_toml.as_os_str().to_owned(),
117 "--target-dir".into(),
118 backend_target_dir.as_os_str().to_owned(),
119 "--package".into(),
120 project.winui_backend_crate_name().as_str().into(),
121 ];
122 run_command_os("cargo", args).await?;
123 }
124
125 Ok(())
126}
127
128pub async fn package_winui(
137 project: &Project,
138 options: PackageOptions,
139 built: &BuiltTarget,
140) -> eyre::Result<Artifact> {
141 ensure_windows_host()?;
142
143 let backend_path = project.backend_path::<WinUiBackend>();
144
145 copy_assets_and_fonts(
147 project,
148 &backend_path,
149 None,
150 options.uses_dev_server(),
151 options.progress(),
152 )
153 .await?;
154
155 let final_binary_path = &built.artifact;
156 let profile = if options.is_debug() {
157 "debug"
158 } else {
159 "release"
160 };
161
162 let runtime_dir =
167 crate::platforming::packaging::dist_dir(&backend_path, "windows", Some(profile));
168 fs::create_dir_all(&runtime_dir).await?;
169
170 let staged_resources = backend_path.join("resources");
173 if staged_resources.is_dir() {
174 copy_dir(&staged_resources, &runtime_dir.join("resources")).await?;
175 }
176
177 if options.uses_shared_rust_runtime() {
178 RustDynamicLibraries::resolve(built, &TargetPlatform::Windows.triple(), project)
179 .await?
180 .stage(&runtime_dir)
181 .await?;
182 } else {
183 RustDynamicLibraries::remove_staged(&runtime_dir, &TargetPlatform::Windows.triple())
184 .await?;
185 }
186
187 let packaged_binary = crate::platforming::packaging::stage_binary_as(
190 final_binary_path,
191 &runtime_dir,
192 &format!("{}.exe", project.winui_binary_name()),
193 )
194 .await?;
195
196 Ok(Artifact::new(project.bundle_identifier(), packaged_binary))
197}
198
199#[must_use]
205pub const fn is_winui_platform(platform: TargetPlatform) -> bool {
206 matches!(platform, TargetPlatform::Windows)
207}
208
209fn ensure_windows_host() -> eyre::Result<()> {
210 if cfg!(target_os = "windows") {
211 Ok(())
212 } else {
213 bail!("WinUI backend is only supported on Windows hosts");
214 }
215}
216
217async fn copy_dir(source: &Path, destination: &Path) -> eyre::Result<()> {
219 let mut stack = vec![(source.to_path_buf(), destination.to_path_buf())];
220 while let Some((source, destination)) = stack.pop() {
221 fs::create_dir_all(&destination).await?;
222 let mut entries = fs::read_dir(&source).await?;
223 while let Some(entry) = entries.next().await {
224 let entry = entry?;
225 let target = destination.join(entry.file_name());
226 if entry.path().is_dir() {
227 stack.push((entry.path(), target));
228 } else {
229 fs::copy(entry.path(), &target).await?;
230 }
231 }
232 }
233 Ok(())
234}
235
236async fn copy_assets_and_fonts(
246 project: &Project,
247 backend_path: &Path,
248 sccache_path: Option<&Path>,
249 dev_server: bool,
250 progress: Option<&BuildProgress>,
251) -> eyre::Result<()> {
252 let resources_dir = backend_path.join("resources");
253 fs::create_dir_all(&resources_dir).await?;
254
255 let manifest = assets::stage_project_assets_for_gtk(
257 project,
258 &resources_dir,
259 sccache_path,
260 dev_server,
261 progress,
262 )
263 .await?;
264
265 fs::write(
268 backend_path.join("app-icon.ico"),
269 assets::project_windows_ico(project)?,
270 )
271 .await?;
272
273 let font_declarations = assets::scan_fonts(project, &backend_path.join("Cargo.toml")).await?;
275 let mut resolved_fonts = assets::resolve_fonts(font_declarations).await?;
276 resolved_fonts.extend(assets::scan_project_font_assets(&manifest)?);
277
278 if !resolved_fonts.is_empty() {
279 let fonts_dest = resources_dir.join("fonts");
281 assets::copy_fonts(&resolved_fonts, &fonts_dest).await?;
282
283 info!("Copied {} fonts to WinUI resources", resolved_fonts.len());
284 }
285
286 Ok(())
287}