use crate::toolchain::{
Host, Toolchain, ToolchainError, UnfixableToolchain,
linux::{
LinuxSystemPackagesInstallation, LinuxSystemToolchain, gtk4_pkg_config_repair_installation,
},
};
#[derive(Debug, Clone, Copy, Default)]
pub struct Gtk4Toolchain;
#[derive(Debug, Clone, Copy)]
struct PkgConfigProbe {
module: &'static str,
min_version: Option<&'static str>,
}
impl PkgConfigProbe {
fn display(self) -> String {
self.min_version.map_or_else(
|| self.module.to_owned(),
|min| format!("{}>={min}", self.module),
)
}
}
const REQUIRED_PROBES: &[PkgConfigProbe] = &[
PkgConfigProbe {
module: "gtk4",
min_version: Some("4.14"),
},
PkgConfigProbe {
module: "pango",
min_version: Some("1.50"),
},
];
impl Toolchain for Gtk4Toolchain {
type Installation = LinuxSystemPackagesInstallation;
async fn check(&self, host: &Host) -> Result<(), ToolchainError<Self::Installation>> {
if !cfg!(target_os = "linux") {
return Err(ToolchainError::Unfixable(UnfixableToolchain::new(
"GTK4 backend is only supported on Linux",
"Run GTK4 targets on Linux with `--platform linux --backend gtk4`.",
)));
}
if !check_pkg_config_exists(host).await {
let linux_toolchain = LinuxSystemToolchain;
return match linux_toolchain.check(host).await {
Ok(()) => Err(ToolchainError::Unfixable(UnfixableToolchain::new(
"pkg-config not found",
"Install pkg-config and ensure it is in PATH, then re-run `water doctor`.",
))),
Err(ToolchainError::Fixable(installation)) => {
Err(ToolchainError::Fixable(installation))
}
Err(ToolchainError::Unfixable(e)) => Err(ToolchainError::Unfixable(e)),
};
}
let missing = missing_pkg_config_probes(host).await;
if missing.is_empty() {
return Ok(());
}
let linux_toolchain = LinuxSystemToolchain;
return match linux_toolchain.check(host).await {
Err(ToolchainError::Fixable(installation)) => {
Err(ToolchainError::Fixable(installation))
}
Err(ToolchainError::Unfixable(e)) => Err(ToolchainError::Unfixable(e)),
Ok(()) => match gtk4_pkg_config_repair_installation(host, &missing).await {
Ok(installation) => Err(ToolchainError::Fixable(installation)),
Err(error) => {
let missing = missing.join(", ");
let base_hint = install_gtk4_suggestion();
Err(ToolchainError::Unfixable(UnfixableToolchain::new(
format!("GTK4 pkg-config probe failed: missing {missing}"),
format!(
"{base_hint} Also ensure these probes pass: `pkg-config --exists gtk4 && pkg-config --atleast-version=4.14 gtk4` and `pkg-config --exists pango && pkg-config --atleast-version=1.50 pango`. Repair planner error: {}",
error.message()
),
)))
}
},
};
}
}
async fn check_pkg_config_exists(host: &Host) -> bool {
host.output("pkg-config", ["--version"])
.await
.is_ok_and(|o| o.status.success())
}
async fn check_module_exists(host: &Host, module: &str) -> bool {
host.output("pkg-config", ["--exists", module])
.await
.is_ok_and(|o| o.status.success())
}
async fn check_module_min_version(host: &Host, module: &str, min_version: &str) -> bool {
host.output(
"pkg-config",
[
format!("--atleast-version={min_version}"),
module.to_owned(),
],
)
.await
.is_ok_and(|o| o.status.success())
}
async fn missing_pkg_config_probes(host: &Host) -> Vec<String> {
let mut missing = Vec::new();
for probe in REQUIRED_PROBES {
if !check_module_exists(host, probe.module).await {
missing.push(probe.display());
continue;
}
if let Some(min_version) = probe.min_version
&& !check_module_min_version(host, probe.module, min_version).await
{
missing.push(probe.display());
}
}
missing
}
const fn install_gtk4_suggestion() -> &'static str {
"GTK4 was not discoverable via pkg-config. Ensure GTK4 development packages are installed for your distribution and `pkg-config --exists gtk4` succeeds."
}
#[cfg(test)]
mod tests {
use super::{PkgConfigProbe, install_gtk4_suggestion};
#[test]
fn gtk4_suggestion_mentions_pkg_config_probe() {
let gtk4 = install_gtk4_suggestion();
assert!(gtk4.contains("pkg-config"));
assert!(gtk4.contains("gtk4"));
}
#[test]
fn pkg_config_probe_display_formats_min_version() {
let probe = PkgConfigProbe {
module: "pango",
min_version: Some("1.50"),
};
assert_eq!(probe.display(), "pango>=1.50");
}
#[test]
fn pkg_config_probe_display_without_version() {
let probe = PkgConfigProbe {
module: "gtk4",
min_version: None,
};
assert_eq!(probe.display(), "gtk4");
}
}
#[cfg(test)]
mod host_tests {
use super::Gtk4Toolchain;
use crate::toolchain::testing::TestMachine;
use crate::toolchain::{Toolchain, ToolchainError};
#[test]
#[cfg(not(target_os = "linux"))]
fn off_linux_is_unfixable() {
let machine = TestMachine::new();
let host = machine.host(Vec::<(String, String)>::new());
let result = smol::block_on(Gtk4Toolchain.check(&host));
assert!(
matches!(result, Err(ToolchainError::Unfixable(_))),
"GTK4 outside Linux must be unfixable: {result:?}"
);
}
#[cfg(target_os = "linux")]
mod linux {
use super::*;
const APT_PACKAGES: &str = "pkg-config libgtk-4-dev libpango1.0-dev libwayland-dev \
wayland-protocols libasound2-dev libva-dev libgbm-dev libxcb1-dev \
libclang-dev libfontconfig-dev";
fn complete_machine() -> TestMachine {
let machine = TestMachine::new();
for tool in ["apt-get", "dpkg-query", "pkg-config"] {
machine.install(tool);
}
machine.respond_pkg_config_module("gtk4", "4.18.0");
machine.respond_pkg_config_module("pango", "1.56.0");
machine.respond_pkg_config_module("libva", "1.20.0");
machine.respond_pkg_config_var("libva_version", "2.20.0");
machine.respond_pkg_config_module("libpipewire-0.3", "0.3.65");
machine
}
#[test]
fn ok_when_probes_and_packages_satisfied() {
let machine = complete_machine();
let host = machine.host([(
String::from("WATERUI_FAKE_DPKG_INSTALLED"),
APT_PACKAGES.to_string(),
)]);
smol::block_on(Gtk4Toolchain.check(&host))
.expect("satisfied gtk4/pango probes plus complete apt set must be ok");
}
#[test]
fn missing_probe_with_apt_is_fixable() {
let machine = TestMachine::new();
for tool in ["apt-get", "dpkg-query", "pkg-config"] {
machine.install(tool);
}
let host = machine.host([(
String::from("WATERUI_FAKE_DPKG_INSTALLED"),
String::from("pkg-config"),
)]);
let result = smol::block_on(Gtk4Toolchain.check(&host));
assert!(
matches!(result, Err(ToolchainError::Fixable(_))),
"missing gtk4 probes under apt must produce a fixable install: {result:?}"
);
}
#[test]
fn missing_probes_fixable_via_repair_when_packages_complete() {
let machine = complete_machine();
std::fs::remove_file(machine.responses().join("PKG_CONFIG_gtk4"))
.expect("remove staged gtk4 module response");
let host = machine.host([(
String::from("WATERUI_FAKE_DPKG_INSTALLED"),
APT_PACKAGES.to_string(),
)]);
let result = smol::block_on(Gtk4Toolchain.check(&host));
assert!(
matches!(result, Err(ToolchainError::Fixable(_))),
"complete packages + missing gtk4 probe must repair via package mapping: {result:?}"
);
}
#[test]
fn unfixable_without_package_manager() {
let machine = TestMachine::new();
machine.install("pkg-config");
let host = machine.host(Vec::<(String, String)>::new());
let result = smol::block_on(Gtk4Toolchain.check(&host));
assert!(
matches!(result, Err(ToolchainError::Unfixable(_))),
"no package manager must be unfixable: {result:?}"
);
}
#[test]
fn unfixable_when_pkg_config_missing_but_packages_installed() {
let machine = TestMachine::new();
machine.install("apt-get");
machine.install("dpkg-query");
let host = machine.host([(
String::from("WATERUI_FAKE_DPKG_INSTALLED"),
APT_PACKAGES.to_string(),
)]);
let result = smol::block_on(Gtk4Toolchain.check(&host));
assert!(
matches!(result, Err(ToolchainError::Unfixable(_))),
"installed packages without pkg-config must be unfixable: {result:?}"
);
}
}
}