waterui_cli/gtk4/
platform.rs1use std::ffi::OsString;
7use std::path::Path;
8
9use eyre::bail;
10use smol::fs;
11use tracing::info;
12
13use crate::{
14 assets, browser_runtime,
15 build::{
16 BuildOptions, BuildProgress, BuiltTarget, RustBuild, RustDynamicLibraries, RustLinkage,
17 },
18 device::Artifact,
19 gtk4::backend::Gtk4Backend,
20 platform::{PackageOptions, TargetPlatform},
21 project::Project,
22 utils::run_command_os,
23};
24
25#[cfg(target_os = "linux")]
26const GTK4_INIT_HINT: &str = "water run --platform linux --backend gtk4";
27#[cfg(not(target_os = "linux"))]
28const GTK4_INIT_HINT: &str = "initialize GTK4 backend on Linux";
29
30pub async fn build_gtk4(project: &Project, options: BuildOptions) -> eyre::Result<BuiltTarget> {
39 ensure_linux_host()?;
40
41 let backend_path = project.backend_path::<Gtk4Backend>();
42 let cargo_toml = backend_path.join("Cargo.toml");
43
44 if !cargo_toml.exists() {
45 bail!(
46 "GTK4 backend not found at {}. Run `{GTK4_INIT_HINT}` to initialize it.",
47 backend_path.display(),
48 );
49 }
50
51 let mut build = RustBuild::new(&backend_path, TargetPlatform::Linux.triple())
52 .with_project(project)
53 .with_target_dir(project.water_target_dir(options.linkage()).await?)
54 .with_linkage(
55 options.linkage(),
56 &format!("{}/dev", project.crate_name()),
57 &["$ORIGIN"],
58 )
59 .with_envs(options.cargo_envs().iter().cloned());
60 if let Some(sccache_path) = options.sccache_path() {
61 build = build.with_sccache(sccache_path.to_path_buf());
62 }
63 if let Some(progress) = options.progress() {
64 build = build.with_progress(progress.clone());
65 }
66 let built_target = build
67 .build_binary(
68 project.gtk_backend_crate_name().as_str(),
69 options.is_release(),
70 )
71 .await
72 .map_err(|error| eyre::eyre!("Failed to build GTK4 backend with cargo: {error}"))?;
73 Ok(built_target)
74}
75
76pub async fn clean_gtk4(project: &Project) -> eyre::Result<()> {
85 ensure_linux_host()?;
86
87 let backend_path = project.backend_path::<Gtk4Backend>();
88 let cargo_toml = backend_path.join("Cargo.toml");
89 if !cargo_toml.exists() {
90 return Ok(()); }
92
93 for linkage in [RustLinkage::SharedRuntime, RustLinkage::Static] {
97 let backend_target_dir = project.water_target_dir(linkage).await?;
98 if !backend_target_dir.exists() {
99 continue;
100 }
101 let args: Vec<OsString> = vec![
102 "clean".into(),
103 "--manifest-path".into(),
104 cargo_toml.as_os_str().to_owned(),
105 "--target-dir".into(),
106 backend_target_dir.as_os_str().to_owned(),
107 "--package".into(),
108 project.gtk_backend_crate_name().as_str().into(),
109 ];
110 run_command_os("cargo", args).await?;
111 }
112
113 Ok(())
114}
115
116pub async fn package_gtk4(
125 project: &Project,
126 options: PackageOptions,
127 built: &BuiltTarget,
128) -> eyre::Result<Artifact> {
129 ensure_linux_host()?;
130
131 let backend_path = project.backend_path::<Gtk4Backend>();
134
135 copy_assets_and_fonts(
137 project,
138 &backend_path,
139 None,
140 options.uses_dev_server(),
141 options.progress(),
142 )
143 .await?;
144
145 let target_dir = &built.profile_dir;
146 let profile = if options.is_debug() {
147 "debug"
148 } else {
149 "release"
150 };
151
152 let final_binary_path = &built.artifact;
154 let runtime_plan = project
155 .browser_runtime_plan(TargetPlatform::Linux, crate::platform::TargetBackend::Gtk4)
156 .await?;
157
158 let runtime_dir =
163 crate::platforming::packaging::dist_dir(&backend_path, "linux", Some(profile));
164 fs::create_dir_all(&runtime_dir).await?;
165 browser_runtime::stage(
166 runtime_plan,
167 TargetPlatform::Linux,
168 target_dir,
169 &runtime_dir,
170 )
171 .await?;
172
173 if options.uses_shared_rust_runtime() {
174 RustDynamicLibraries::resolve(built, &TargetPlatform::Linux.triple(), project)
175 .await?
176 .stage(&runtime_dir)
177 .await?;
178 } else {
179 RustDynamicLibraries::remove_staged(&runtime_dir, &TargetPlatform::Linux.triple()).await?;
180 }
181
182 let packaged_binary = crate::platforming::packaging::stage_binary_as(
185 final_binary_path,
186 &runtime_dir,
187 project.gtk4_binary_name().as_str(),
188 )
189 .await?;
190
191 Ok(Artifact::new(project.bundle_identifier(), packaged_binary))
192}
193
194#[must_use]
200pub const fn is_gtk4_platform(platform: TargetPlatform) -> bool {
201 matches!(platform, TargetPlatform::Linux)
202}
203
204fn ensure_linux_host() -> eyre::Result<()> {
205 if cfg!(target_os = "linux") {
206 Ok(())
207 } else {
208 bail!("GTK4 backend is only supported on Linux hosts");
209 }
210}
211
212async fn copy_assets_and_fonts(
221 project: &Project,
222 backend_path: &Path,
223 sccache_path: Option<&Path>,
224 dev_server: bool,
225 progress: Option<&BuildProgress>,
226) -> eyre::Result<()> {
227 let resources_dir = backend_path.join("resources");
228 fs::create_dir_all(&resources_dir).await?;
229
230 let manifest = assets::stage_project_assets_for_gtk(
232 project,
233 &resources_dir,
234 sccache_path,
235 dev_server,
236 progress,
237 )
238 .await?;
239 assets::stage_hicolor_icons(project, &resources_dir.join("icons")).await?;
240
241 let font_declarations = assets::scan_fonts(project, &backend_path.join("Cargo.toml")).await?;
243 let mut resolved_fonts = assets::resolve_fonts(font_declarations).await?;
244 resolved_fonts.extend(assets::scan_project_font_assets(&manifest)?);
245
246 if !resolved_fonts.is_empty() {
247 let fonts_dest = resources_dir.join("fonts");
249 assets::copy_fonts(&resolved_fonts, &fonts_dest).await?;
250
251 info!("Copied {} fonts to GTK4 resources", resolved_fonts.len());
252
253 }
257
258 Ok(())
259}