use std::ffi::OsString;
use std::path::{Path, PathBuf};
use color_eyre::eyre::{self, bail};
use smol::fs;
use tracing::info;
use crate::{
assets, browser_runtime,
build::{BuildOptions, RustBuild, RustDynamicLibraries, RustLinkage},
device::Artifact,
gtk4::backend::Gtk4Backend,
platform::{PackageOptions, TargetPlatform},
project::Project,
utils::run_command_os,
};
#[cfg(target_os = "linux")]
const GTK4_INIT_HINT: &str = "water run --platform linux --backend gtk4";
#[cfg(not(target_os = "linux"))]
const GTK4_INIT_HINT: &str = "initialize GTK4 backend on Linux";
async fn gtk4_profile_dir(
project: &Project,
profile: &str,
linkage: RustLinkage,
) -> eyre::Result<PathBuf> {
Ok(project
.water_target_dir(linkage)
.await?
.join(TargetPlatform::Linux.triple().to_string())
.join(profile))
}
pub async fn build_gtk4(project: &Project, options: BuildOptions) -> eyre::Result<PathBuf> {
ensure_linux_host()?;
let backend_path = project.backend_path::<Gtk4Backend>();
let cargo_toml = backend_path.join("Cargo.toml");
if !cargo_toml.exists() {
bail!(
"GTK4 backend not found at {}. Run `{GTK4_INIT_HINT}` to initialize it.",
backend_path.display(),
);
}
let mut build = RustBuild::new(&backend_path, TargetPlatform::Linux.triple())
.with_target_dir(project.water_target_dir(options.linkage()).await?)
.with_linkage(
options.linkage(),
&format!("{}/dev", project.crate_name()),
Some("$ORIGIN"),
);
if let Some(sccache_path) = options.sccache_path() {
build = build.with_sccache(sccache_path.to_path_buf());
}
build
.build_binary(
project.gtk_backend_crate_name().as_str(),
options.is_release(),
)
.await
.map_err(|error| eyre::eyre!("Failed to build GTK4 backend with cargo: {error}"))?;
build
.lib_output_dir(options.is_release())
.await
.map_err(Into::into)
}
pub async fn clean_gtk4(project: &Project) -> eyre::Result<()> {
ensure_linux_host()?;
let backend_path = project.backend_path::<Gtk4Backend>();
let cargo_toml = backend_path.join("Cargo.toml");
if !cargo_toml.exists() {
return Ok(()); }
for linkage in [RustLinkage::SharedRuntime, RustLinkage::Static] {
let backend_target_dir = project.water_target_dir(linkage).await?;
if !backend_target_dir.exists() {
continue;
}
let args: Vec<OsString> = vec![
"clean".into(),
"--manifest-path".into(),
cargo_toml.as_os_str().to_owned(),
"--target-dir".into(),
backend_target_dir.as_os_str().to_owned(),
"--package".into(),
project.gtk_backend_crate_name().as_str().into(),
];
run_command_os("cargo", args).await?;
}
Ok(())
}
pub async fn package_gtk4(project: &Project, options: PackageOptions) -> eyre::Result<Artifact> {
ensure_linux_host()?;
let profile = if options.is_debug() {
"debug"
} else {
"release"
};
let backend_path = project.backend_path::<Gtk4Backend>();
copy_assets_and_fonts(project, &backend_path).await?;
let linkage = if options.uses_shared_rust_runtime() {
RustLinkage::SharedRuntime
} else {
RustLinkage::Static
};
let target_dir = gtk4_profile_dir(project, profile, linkage).await?;
let binary_name = project.gtk_backend_crate_name();
let binary_path = target_dir.join(binary_name.as_ref());
let final_binary_path = if binary_path.exists() {
binary_path
} else {
let alt_binary_name = binary_name.replace('-', "_");
let alt_binary_path = target_dir.join(&alt_binary_name);
if alt_binary_path.exists() {
alt_binary_path
} else {
bail!(
"Built GTK4 binary not found at {}. Did you run build first?",
binary_path.display()
);
}
};
let runtime_plan = project
.browser_runtime_plan(TargetPlatform::Linux, crate::platform::TargetBackend::Gtk4)
.await?;
let runtime_dir = final_binary_path.parent().ok_or_else(|| {
eyre::eyre!(
"GTK4 binary path has no output directory: {}",
final_binary_path.display()
)
})?;
browser_runtime::stage(
runtime_plan,
TargetPlatform::Linux,
&target_dir,
runtime_dir,
)
.await?;
if options.uses_shared_rust_runtime() {
RustDynamicLibraries::resolve(runtime_dir, &TargetPlatform::Linux.triple())
.await?
.stage(runtime_dir)
.await?;
} else {
RustDynamicLibraries::remove_staged(runtime_dir, &TargetPlatform::Linux.triple()).await?;
}
Ok(Artifact::new(
project.bundle_identifier(),
final_binary_path,
))
}
#[must_use]
pub const fn is_gtk4_platform(platform: TargetPlatform) -> bool {
matches!(platform, TargetPlatform::Linux)
}
fn ensure_linux_host() -> eyre::Result<()> {
if cfg!(target_os = "linux") {
Ok(())
} else {
bail!("GTK4 backend is only supported on Linux hosts");
}
}
async fn copy_assets_and_fonts(project: &Project, backend_path: &Path) -> eyre::Result<()> {
let resources_dir = backend_path.join("resources");
fs::create_dir_all(&resources_dir).await?;
assets::stage_project_assets_for_gtk(project, &resources_dir).await?;
assets::stage_hicolor_icons(project, &resources_dir.join("icons")).await?;
let font_declarations = assets::scan_fonts(project).await?;
let mut resolved_fonts = assets::resolve_fonts(font_declarations).await?;
resolved_fonts.extend(assets::scan_project_font_assets(project)?);
if !resolved_fonts.is_empty() {
let fonts_dest = resources_dir.join("fonts");
assets::copy_fonts(&resolved_fonts, &fonts_dest).await?;
info!("Copied {} fonts to GTK4 resources", resolved_fonts.len());
}
Ok(())
}