1use crate::toolchain::{
4 Host, Toolchain, ToolchainError, UnfixableToolchain,
5 linux::{
6 LinuxSystemPackagesInstallation, LinuxSystemToolchain, gtk4_pkg_config_repair_installation,
7 },
8};
9
10#[derive(Debug, Clone, Copy, Default)]
14pub struct Gtk4Toolchain;
15
16#[derive(Debug, Clone, Copy)]
17struct PkgConfigProbe {
18 module: &'static str,
19 min_version: Option<&'static str>,
20}
21
22impl PkgConfigProbe {
23 fn display(self) -> String {
24 self.min_version.map_or_else(
25 || self.module.to_owned(),
26 |min| format!("{}>={min}", self.module),
27 )
28 }
29}
30
31const REQUIRED_PROBES: &[PkgConfigProbe] = &[
32 PkgConfigProbe {
35 module: "gtk4",
36 min_version: Some("4.14"),
37 },
38 PkgConfigProbe {
39 module: "pango",
40 min_version: Some("1.50"),
41 },
42];
43
44impl Toolchain for Gtk4Toolchain {
45 type Installation = LinuxSystemPackagesInstallation;
46
47 async fn check(&self, host: &Host) -> Result<(), ToolchainError<Self::Installation>> {
48 if !cfg!(target_os = "linux") {
49 return Err(ToolchainError::Unfixable(UnfixableToolchain::new(
50 "GTK4 backend is only supported on Linux",
51 "Run GTK4 targets on Linux with `--platform linux --backend gtk4`.",
52 )));
53 }
54
55 if !check_pkg_config_exists(host).await {
56 let linux_toolchain = LinuxSystemToolchain;
57 return match linux_toolchain.check(host).await {
58 Ok(()) => Err(ToolchainError::Unfixable(UnfixableToolchain::new(
59 "pkg-config not found",
60 "Install pkg-config and ensure it is in PATH, then re-run `water doctor`.",
61 ))),
62 Err(ToolchainError::Fixable(installation)) => {
63 Err(ToolchainError::Fixable(installation))
64 }
65 Err(ToolchainError::Unfixable(e)) => Err(ToolchainError::Unfixable(e)),
66 };
67 }
68
69 let missing = missing_pkg_config_probes(host).await;
70 if missing.is_empty() {
71 return Ok(());
72 }
73
74 let linux_toolchain = LinuxSystemToolchain;
75 return match linux_toolchain.check(host).await {
76 Err(ToolchainError::Fixable(installation)) => {
77 Err(ToolchainError::Fixable(installation))
78 }
79 Err(ToolchainError::Unfixable(e)) => Err(ToolchainError::Unfixable(e)),
80 Ok(()) => match gtk4_pkg_config_repair_installation(host, &missing).await {
81 Ok(installation) => Err(ToolchainError::Fixable(installation)),
82 Err(error) => {
83 let missing = missing.join(", ");
84 let base_hint = install_gtk4_suggestion();
85 Err(ToolchainError::Unfixable(UnfixableToolchain::new(
86 format!("GTK4 pkg-config probe failed: missing {missing}"),
87 format!(
88 "{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: {}",
89 error.message()
90 ),
91 )))
92 }
93 },
94 };
95 }
96}
97
98async fn check_pkg_config_exists(host: &Host) -> bool {
100 host.output("pkg-config", ["--version"])
101 .await
102 .is_ok_and(|o| o.status.success())
103}
104
105async fn check_module_exists(host: &Host, module: &str) -> bool {
106 host.output("pkg-config", ["--exists", module])
107 .await
108 .is_ok_and(|o| o.status.success())
109}
110
111async fn check_module_min_version(host: &Host, module: &str, min_version: &str) -> bool {
112 host.output(
113 "pkg-config",
114 [
115 format!("--atleast-version={min_version}"),
116 module.to_owned(),
117 ],
118 )
119 .await
120 .is_ok_and(|o| o.status.success())
121}
122
123async fn missing_pkg_config_probes(host: &Host) -> Vec<String> {
124 let mut missing = Vec::new();
125
126 for probe in REQUIRED_PROBES {
127 if !check_module_exists(host, probe.module).await {
128 missing.push(probe.display());
129 continue;
130 }
131 if let Some(min_version) = probe.min_version
132 && !check_module_min_version(host, probe.module, min_version).await
133 {
134 missing.push(probe.display());
135 }
136 }
137
138 missing
139}
140
141const fn install_gtk4_suggestion() -> &'static str {
143 "GTK4 was not discoverable via pkg-config. Ensure GTK4 development packages are installed for your distribution and `pkg-config --exists gtk4` succeeds."
144}
145
146#[cfg(test)]
147mod tests {
148 use super::{PkgConfigProbe, install_gtk4_suggestion};
149
150 #[test]
151 fn gtk4_suggestion_mentions_pkg_config_probe() {
152 let gtk4 = install_gtk4_suggestion();
153 assert!(gtk4.contains("pkg-config"));
154 assert!(gtk4.contains("gtk4"));
155 }
156
157 #[test]
158 fn pkg_config_probe_display_formats_min_version() {
159 let probe = PkgConfigProbe {
160 module: "pango",
161 min_version: Some("1.50"),
162 };
163 assert_eq!(probe.display(), "pango>=1.50");
164 }
165
166 #[test]
167 fn pkg_config_probe_display_without_version() {
168 let probe = PkgConfigProbe {
169 module: "gtk4",
170 min_version: None,
171 };
172 assert_eq!(probe.display(), "gtk4");
173 }
174}
175
176#[cfg(test)]
177mod host_tests {
178 use super::Gtk4Toolchain;
179 use crate::toolchain::testing::TestMachine;
180 use crate::toolchain::{Toolchain, ToolchainError};
181
182 #[test]
183 #[cfg(not(target_os = "linux"))]
184 fn off_linux_is_unfixable() {
185 let machine = TestMachine::new();
186 let host = machine.host(Vec::<(String, String)>::new());
187 let result = smol::block_on(Gtk4Toolchain.check(&host));
188 assert!(
189 matches!(result, Err(ToolchainError::Unfixable(_))),
190 "GTK4 outside Linux must be unfixable: {result:?}"
191 );
192 }
193
194 #[cfg(target_os = "linux")]
195 mod linux {
196 use super::*;
197
198 const APT_PACKAGES: &str = "pkg-config libgtk-4-dev libpango1.0-dev libwayland-dev \
199 wayland-protocols libasound2-dev libva-dev libgbm-dev libxcb1-dev \
200 libclang-dev libfontconfig-dev";
201
202 fn complete_machine() -> TestMachine {
205 let machine = TestMachine::new();
206 for tool in ["apt-get", "dpkg-query", "pkg-config"] {
207 machine.install(tool);
208 }
209 machine.respond_pkg_config_module("gtk4", "4.18.0");
210 machine.respond_pkg_config_module("pango", "1.56.0");
211 machine.respond_pkg_config_module("libva", "1.20.0");
212 machine.respond_pkg_config_var("libva_version", "2.20.0");
213 machine.respond_pkg_config_module("libpipewire-0.3", "0.3.65");
214 machine
215 }
216
217 #[test]
218 fn ok_when_probes_and_packages_satisfied() {
219 let machine = complete_machine();
220 let host = machine.host([(
221 String::from("WATERUI_FAKE_DPKG_INSTALLED"),
222 APT_PACKAGES.to_string(),
223 )]);
224 smol::block_on(Gtk4Toolchain.check(&host))
225 .expect("satisfied gtk4/pango probes plus complete apt set must be ok");
226 }
227
228 #[test]
229 fn missing_probe_with_apt_is_fixable() {
230 let machine = TestMachine::new();
231 for tool in ["apt-get", "dpkg-query", "pkg-config"] {
232 machine.install(tool);
233 }
234 let host = machine.host([(
235 String::from("WATERUI_FAKE_DPKG_INSTALLED"),
236 String::from("pkg-config"),
237 )]);
238 let result = smol::block_on(Gtk4Toolchain.check(&host));
239 assert!(
240 matches!(result, Err(ToolchainError::Fixable(_))),
241 "missing gtk4 probes under apt must produce a fixable install: {result:?}"
242 );
243 }
244
245 #[test]
246 fn missing_probes_fixable_via_repair_when_packages_complete() {
247 let machine = complete_machine();
248 std::fs::remove_file(machine.responses().join("PKG_CONFIG_gtk4"))
253 .expect("remove staged gtk4 module response");
254 let host = machine.host([(
255 String::from("WATERUI_FAKE_DPKG_INSTALLED"),
256 APT_PACKAGES.to_string(),
257 )]);
258 let result = smol::block_on(Gtk4Toolchain.check(&host));
259 assert!(
260 matches!(result, Err(ToolchainError::Fixable(_))),
261 "complete packages + missing gtk4 probe must repair via package mapping: {result:?}"
262 );
263 }
264
265 #[test]
266 fn unfixable_without_package_manager() {
267 let machine = TestMachine::new();
268 machine.install("pkg-config");
269 let host = machine.host(Vec::<(String, String)>::new());
270 let result = smol::block_on(Gtk4Toolchain.check(&host));
271 assert!(
272 matches!(result, Err(ToolchainError::Unfixable(_))),
273 "no package manager must be unfixable: {result:?}"
274 );
275 }
276
277 #[test]
278 fn unfixable_when_pkg_config_missing_but_packages_installed() {
279 let machine = TestMachine::new();
280 machine.install("apt-get");
281 machine.install("dpkg-query");
282 let host = machine.host([(
283 String::from("WATERUI_FAKE_DPKG_INSTALLED"),
284 APT_PACKAGES.to_string(),
285 )]);
286 let result = smol::block_on(Gtk4Toolchain.check(&host));
287 assert!(
288 matches!(result, Err(ToolchainError::Unfixable(_))),
289 "installed packages without pkg-config must be unfixable: {result:?}"
290 );
291 }
292 }
293}