Skip to main content

waterui_cli/gtk4/
platform.rs

1//! GTK4 platform build and package utilities.
2//!
3//! This module provides utility functions for building and packaging GTK4 apps.
4//! These functions are used by `Gtk4Backend` to implement the `Backend` trait.
5
6use 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
30// ============================================================================
31// Build Utilities
32// ============================================================================
33
34/// Build GTK4 binary for the host platform.
35///
36/// # Errors
37/// Returns an error if the backend manifest is missing, the host is unsupported, or Cargo fails.
38pub 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
76// ============================================================================
77// Clean
78// ============================================================================
79
80/// Clean Cargo build artifacts for GTK4.
81///
82/// # Errors
83/// Returns an error if the host is unsupported or `cargo clean` fails.
84pub 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(()); // Nothing to clean
91    }
92
93    // The target directories are shared with every other generated backend, so only
94    // this backend's own package is cleaned — its dependency artifacts stay for
95    // the other backends that resolve them identically.
96    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
116// ============================================================================
117// Package
118// ============================================================================
119
120/// Package a GTK4 app (locate the built binary).
121///
122/// # Errors
123/// Returns an error if the host is unsupported, assets cannot be staged, or the built binary is missing.
124pub async fn package_gtk4(
125    project: &Project,
126    options: PackageOptions,
127    built: &BuiltTarget,
128) -> eyre::Result<Artifact> {
129    ensure_linux_host()?;
130
131    // For GTK4, "packaging" just means locating the built binary
132    // GTK4 uses its own target directory since it's a standalone project
133    let backend_path = project.backend_path::<Gtk4Backend>();
134
135    // Copy project assets and dependency fonts
136    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    // The binary name is the GTK4 crate name (project-gtk4)
153    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    // The shipped binary and everything `$ORIGIN` resolves beside it stage
159    // into the project's own managed backend directory — the shared Cargo
160    // profile directory would collide two same-named projects on
161    // `<profile>/<product>`.
162    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    // Ship the binary under the product name; the tagged Cargo artifact name
183    // is internal to the shared target directory.
184    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// ============================================================================
195// Platform Support Check
196// ============================================================================
197
198/// Check if a platform is supported by the GTK4 backend.
199#[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
212// ============================================================================
213// Asset and Font Handling
214// ============================================================================
215
216/// Copy project assets and dependency fonts to the GTK4 resources directory.
217///
218/// For GTK4, assets and fonts are placed alongside the binary in a `resources/`
219/// directory. The binary should load fonts via fontconfig or Pango at runtime.
220async 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    // Stage project assets using platform-native conventions.
231    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    // Scan and resolve dependency fonts
242    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        // Copy fonts to resources/fonts/
248        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        // Note: GTK4 font registration happens at runtime via fontconfig/pango.
254        // The hydrolysis backend should register fonts from the resources/fonts directory
255        // when initializing.
256    }
257
258    Ok(())
259}