1use crate::{
4 toolchain::{Host, Installation, Toolchain, ToolchainError, UnfixableToolchain},
5 utils::CommandError,
6};
7
8#[derive(Debug, Clone, Copy, Default)]
10pub struct LinuxSystemToolchain;
11
12#[derive(Debug, Clone)]
14pub struct LinuxSystemPackagesInstallation {
15 manager: LinuxPackageManager,
16 missing_packages: Vec<String>,
17}
18
19impl LinuxSystemPackagesInstallation {
20 const fn new(manager: LinuxPackageManager, missing_packages: Vec<String>) -> Self {
21 Self {
22 manager,
23 missing_packages,
24 }
25 }
26
27 #[must_use]
29 pub const fn package_manager_name(&self) -> &'static str {
30 self.manager.name()
31 }
32
33 #[must_use]
35 pub fn missing_packages(&self) -> &[String] {
36 &self.missing_packages
37 }
38
39 #[must_use]
41 pub fn install_command_hint(&self) -> String {
42 self.manager.install_hint(&self.missing_packages)
43 }
44
45 pub async fn from_packages(
54 host: &Host,
55 packages: Vec<String>,
56 ) -> Result<Self, UnfixableToolchain> {
57 let Some(manager) = LinuxPackageManager::detect(host).await else {
58 return Err(UnfixableToolchain::new(
59 "Unable to detect Linux package manager",
60 unsupported_manager_hint(),
61 ));
62 };
63 if packages.is_empty() {
64 return Err(UnfixableToolchain::new(
65 "No packages were provided for automatic installation",
66 "Re-run `water doctor` and inspect diagnostics.",
67 ));
68 }
69 Ok(Self::new(manager, packages))
70 }
71}
72
73#[derive(Debug, thiserror::Error)]
75pub enum FailToInstallLinuxSystemPackages {
76 #[error("Automatic Linux package installation is only supported on Linux hosts.")]
78 UnsupportedPlatform,
79 #[error("No supported Linux package manager found (apt-get, dnf, pacman, zypper, apk).")]
81 UnsupportedPackageManager,
82 #[error("Failed to install Linux system packages: {0}")]
84 CommandFailed(#[from] CommandError),
85}
86
87#[derive(Debug, thiserror::Error)]
89pub enum LinuxPackageManagerError {
90 #[error("No supported Linux package manager found (apt-get, dnf, pacman, zypper, apk).")]
92 UnsupportedPackageManager,
93 #[error(transparent)]
95 Command(#[from] CommandError),
96}
97
98#[derive(Debug, thiserror::Error)]
100#[error("`{version}` is not a dotted-numeric version: {source}")]
101struct DottedVersionError {
102 version: String,
103 #[source]
104 source: std::num::ParseIntError,
105}
106
107#[derive(Debug, thiserror::Error)]
109enum NativeProbeError {
110 #[error(transparent)]
112 Command(#[from] CommandError),
113 #[error("`pkg-config --modversion {module}` printed nothing")]
115 EmptyModVersion {
116 module: &'static str,
118 },
119 #[error(transparent)]
121 Version(#[from] DottedVersionError),
122}
123
124impl Installation for LinuxSystemPackagesInstallation {
125 type Error = FailToInstallLinuxSystemPackages;
126
127 async fn install(&self, host: &Host) -> Result<(), Self::Error> {
128 if !cfg!(target_os = "linux") {
129 return Err(FailToInstallLinuxSystemPackages::UnsupportedPlatform);
130 }
131
132 let Some(manager) = LinuxPackageManager::detect(host).await else {
133 return Err(FailToInstallLinuxSystemPackages::UnsupportedPackageManager);
134 };
135
136 install_missing_packages(host, manager, &self.missing_packages).await?;
137 Ok(())
138 }
139}
140
141impl Toolchain for LinuxSystemToolchain {
142 type Installation = LinuxSystemPackagesInstallation;
143
144 async fn check(&self, host: &Host) -> Result<(), ToolchainError<Self::Installation>> {
145 if !cfg!(target_os = "linux") {
146 return Ok(());
147 }
148
149 let Some(manager) = LinuxPackageManager::detect(host).await else {
150 return Err(ToolchainError::unfixable(
151 "Unable to detect Linux package manager",
152 unsupported_manager_hint(),
153 ));
154 };
155
156 let required_packages = manager.required_packages();
157 let mut missing_packages = Vec::new();
158 for &package in required_packages {
159 let installed = manager
160 .check_installed(host, package)
161 .await
162 .map_err(|error| {
163 ToolchainError::unfixable(
164 format!("Failed checking Linux package `{package}`: {error}"),
165 manager.install_hint(&required_packages_to_owned(required_packages)),
166 )
167 })?;
168 if !installed {
169 missing_packages.push(package.to_string());
170 }
171 }
172
173 if !missing_packages.is_empty() {
174 return Err(ToolchainError::fixable(
175 LinuxSystemPackagesInstallation::new(manager, missing_packages),
176 ));
177 }
178
179 check_versioned_native_libraries(host, manager).await
180 }
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184enum LinuxPackageManager {
185 Apt,
186 Dnf,
187 Pacman,
188 Zypper,
189 Apk,
190}
191
192impl LinuxPackageManager {
193 async fn detect(host: &Host) -> Option<Self> {
194 if host.which("apt-get").await.is_ok() {
195 Some(Self::Apt)
196 } else if host.which("dnf").await.is_ok() {
197 Some(Self::Dnf)
198 } else if host.which("pacman").await.is_ok() {
199 Some(Self::Pacman)
200 } else if host.which("zypper").await.is_ok() {
201 Some(Self::Zypper)
202 } else if host.which("apk").await.is_ok() {
203 Some(Self::Apk)
204 } else {
205 None
206 }
207 }
208
209 const fn name(self) -> &'static str {
210 match self {
211 Self::Apt => "apt-get",
212 Self::Dnf => "dnf",
213 Self::Pacman => "pacman",
214 Self::Zypper => "zypper",
215 Self::Apk => "apk",
216 }
217 }
218
219 const fn required_packages(self) -> &'static [&'static str] {
220 match self {
221 Self::Apt => &[
222 "pkg-config",
223 "libgtk-4-dev",
224 "libpango1.0-dev",
225 "libwayland-dev",
226 "wayland-protocols",
227 "libasound2-dev",
228 "libva-dev",
229 "libgbm-dev",
230 "libxcb1-dev",
231 "libclang-dev",
232 "libfontconfig-dev",
233 ],
234 Self::Dnf => &[
235 "pkgconf-pkg-config",
236 "gtk4-devel",
237 "pango-devel",
238 "wayland-devel",
239 "wayland-protocols-devel",
240 "alsa-lib-devel",
241 "libva-devel",
242 "mesa-libgbm-devel",
243 "libxcb-devel",
244 "clang-devel",
245 "fontconfig-devel",
246 ],
247 Self::Pacman => &[
248 "pkgconf",
249 "gtk4",
250 "pango",
251 "wayland",
252 "wayland-protocols",
253 "alsa-lib",
254 "libva",
255 "mesa",
256 "libxcb",
257 "clang",
258 "fontconfig",
259 ],
260 Self::Zypper => &[
261 "pkg-config",
262 "gtk4-devel",
263 "pango-devel",
264 "wayland-devel",
265 "wayland-protocols-devel",
266 "alsa-devel",
267 "libva-devel",
268 "Mesa-libgbm-devel",
269 "libxcb-devel",
270 "clang-devel",
271 "fontconfig-devel",
272 ],
273 Self::Apk => &[
274 "pkgconf",
275 "gtk4.0-dev",
276 "pango-dev",
277 "wayland-dev",
278 "wayland-protocols",
279 "alsa-lib-dev",
280 "libva-dev",
281 "mesa-dev",
282 "libxcb-dev",
283 "clang-dev",
284 "fontconfig-dev",
285 ],
286 }
287 }
288
289 fn install_hint(self, packages: &[String]) -> String {
290 let package_list = packages.join(" ");
291 match self {
292 Self::Apt => format!("sudo apt-get install -y {package_list}"),
293 Self::Dnf => format!("sudo dnf install -y {package_list}"),
294 Self::Pacman => format!("sudo pacman -S --noconfirm --needed {package_list}"),
295 Self::Zypper => {
296 format!(
297 "sudo zypper --non-interactive install --auto-agree-with-licenses {package_list}"
298 )
299 }
300 Self::Apk => format!("sudo apk add {package_list}"),
301 }
302 }
303
304 async fn check_installed(self, host: &Host, package: &str) -> Result<bool, CommandError> {
305 let output = match self {
306 Self::Apt => host.output("dpkg-query", ["-W", package]).await?,
307 Self::Dnf | Self::Zypper => host.output("rpm", ["-q", package]).await?,
308 Self::Pacman => host.output("pacman", ["-Q", package]).await?,
309 Self::Apk => host.output("apk", ["info", "-e", package]).await?,
310 };
311 Ok(output.status.success())
312 }
313
314 fn package_for_gtk_pkg_config_probe(self, probe: &str) -> Option<&'static str> {
315 if probe.starts_with("gtk4") {
316 return Some(match self {
317 Self::Apt => "libgtk-4-dev",
318 Self::Dnf | Self::Zypper => "gtk4-devel",
319 Self::Pacman => "gtk4",
320 Self::Apk => "gtk4.0-dev",
321 });
322 }
323 if probe.starts_with("pango") {
324 return Some(match self {
325 Self::Apt => "libpango1.0-dev",
326 Self::Dnf | Self::Zypper => "pango-devel",
327 Self::Pacman => "pango",
328 Self::Apk => "pango-dev",
329 });
330 }
331 None
332 }
333
334 const fn package_for_native_library(self, module: &str) -> Option<&'static str> {
337 match module.as_bytes() {
338 b"libva" => Some(match self {
339 Self::Apt | Self::Apk => "libva-dev",
340 Self::Dnf | Self::Zypper => "libva-devel",
341 Self::Pacman => "libva",
342 }),
343 b"libpipewire-0.3" => Some(match self {
344 Self::Apt => "libpipewire-0.3-dev",
345 Self::Dnf | Self::Zypper => "pipewire-devel",
346 Self::Pacman => "libpipewire",
347 Self::Apk => "pipewire-dev",
348 }),
349 _ => None,
350 }
351 }
352}
353
354#[derive(Debug, Clone, Copy)]
362struct VersionedNativeLibrary {
363 module: &'static str,
365 version_axis: &'static str,
367 minimum_version: &'static str,
369 required_by: &'static str,
371 required_by_version: &'static str,
374 requirement_source: &'static str,
376 release_version_variable: Option<&'static str>,
379 distribution_hint: &'static str,
381}
382
383const VERSIONED_NATIVE_LIBRARIES: &[VersionedNativeLibrary] = &[
418 VersionedNativeLibrary {
419 module: "libva",
420 version_axis: "VA-API",
421 minimum_version: "1.19",
422 required_by: "cros-libva",
423 required_by_version: "0.0.12",
424 requirement_source: "its build.rs only emits `libva_1_19_or_higher`, which src/buffer/av1.rs requires, from VA-API 1.19 up",
425 release_version_variable: Some("libva_version"),
426 distribution_hint: "VA-API 1.19 first ships in libva 2.19. Ubuntu 24.04 (libva 2.20) and Debian 13 trixie (libva 2.22) are new enough; Ubuntu 22.04 (libva 2.14) and Debian 12 bookworm (libva 2.17) are not, and neither has a backport in its updates or backports pocket. Upgrade the distribution, or build libva 2.19 or newer from https://github.com/intel/libva and put its prefix on PKG_CONFIG_PATH.",
427 },
428 VersionedNativeLibrary {
429 module: "libpipewire-0.3",
430 version_axis: "PipeWire",
431 minimum_version: "0.3.65",
432 required_by: "libspa",
433 required_by_version: "0.10.1",
434 requirement_source: "it uses spa_video_info_raw::flags, added in PipeWire 0.3.65, and spa_meta_first, a static inline function only since 0.3.59, without a feature gate",
435 release_version_variable: None,
436 distribution_hint: "Ubuntu 24.04 (PipeWire 1.0.5) and Debian 12 bookworm (PipeWire 0.3.65) are new enough; Ubuntu 22.04 (PipeWire 0.3.48) is not, and has no backport in its updates or backports pocket. Upgrade the distribution, or build PipeWire 0.3.65 or newer from https://gitlab.freedesktop.org/pipewire/pipewire and put its prefix on PKG_CONFIG_PATH.",
437 },
438];
439
440#[derive(Debug, Clone, PartialEq, Eq)]
442enum NativeLibraryStatus {
443 Satisfied,
445 ModuleMissing,
447 TooOld {
449 installed: String,
451 release: Option<String>,
453 },
454}
455
456impl VersionedNativeLibrary {
457 fn outdated_message(self, installed: &str, release: Option<&str>) -> String {
459 let Self {
460 module,
461 version_axis,
462 minimum_version,
463 required_by,
464 required_by_version,
465 ..
466 } = self;
467 let release = release.map_or_else(String::new, |release| format!(" (release {release})"));
468 format!(
469 "`{module}` is too old: pkg-config reports {version_axis} {installed}{release}, but `{required_by} {required_by_version}` needs {version_axis} {minimum_version} or newer"
470 )
471 }
472
473 fn outdated_suggestion(self, package: Option<&str>) -> String {
478 let Self {
479 module,
480 minimum_version,
481 required_by,
482 requirement_source,
483 distribution_hint,
484 ..
485 } = self;
486 let already_installed = package.map_or_else(
487 || format!("The package providing `{module}` is already installed"),
488 |package| format!("`{package}` is already installed"),
489 );
490 format!(
491 "{already_installed}, so installing it again will not help. `{required_by}` needs the newer headers because {requirement_source}. {distribution_hint} `pkg-config --modversion {module}` has to report {minimum_version} or newer."
492 )
493 }
494}
495
496async fn check_versioned_native_libraries(
498 host: &Host,
499 manager: LinuxPackageManager,
500) -> Result<(), ToolchainError<LinuxSystemPackagesInstallation>> {
501 if !pkg_config_available(host).await {
502 return Err(ToolchainError::unfixable(
503 "pkg-config not found",
504 "Install pkg-config and ensure it is in PATH, then re-run `water doctor`.",
505 ));
506 }
507
508 let mut missing_packages = Vec::new();
509 for &library in VERSIONED_NATIVE_LIBRARIES {
510 let package = manager.package_for_native_library(library.module);
511 let status = probe_native_library(host, library).await.map_err(|error| {
512 ToolchainError::unfixable(
513 format!(
514 "Failed checking `{}` with pkg-config: {error}",
515 library.module
516 ),
517 format!(
518 "Ensure `pkg-config --modversion {}` works, then re-run `water doctor`.",
519 library.module
520 ),
521 )
522 })?;
523
524 match status {
525 NativeLibraryStatus::Satisfied => {}
526 NativeLibraryStatus::ModuleMissing => {
527 let package = package.ok_or_else(|| {
528 ToolchainError::unfixable(
529 format!("`{}` is not known to pkg-config", library.module),
530 format!(
531 "Install the development package providing `{}` for this distribution, then re-run `water doctor`.",
532 library.module
533 ),
534 )
535 })?;
536 if !missing_packages.iter().any(|existing| existing == package) {
537 missing_packages.push(package.to_owned());
538 }
539 }
540 NativeLibraryStatus::TooOld { installed, release } => {
541 return Err(ToolchainError::unfixable(
542 library.outdated_message(&installed, release.as_deref()),
543 library.outdated_suggestion(package),
544 ));
545 }
546 }
547 }
548
549 if missing_packages.is_empty() {
550 Ok(())
551 } else {
552 Err(ToolchainError::fixable(
553 LinuxSystemPackagesInstallation::new(manager, missing_packages),
554 ))
555 }
556}
557
558async fn pkg_config_available(host: &Host) -> bool {
560 host.output("pkg-config", ["--version"])
561 .await
562 .is_ok_and(|output| output.status.success())
563}
564
565async fn probe_native_library(
567 host: &Host,
568 library: VersionedNativeLibrary,
569) -> Result<NativeLibraryStatus, NativeProbeError> {
570 let output = host
571 .output("pkg-config", ["--modversion", library.module])
572 .await?;
573 if !output.status.success() {
574 return Ok(NativeLibraryStatus::ModuleMissing);
575 }
576 let installed = String::from_utf8_lossy(&output.stdout).trim().to_owned();
577 if installed.is_empty() {
578 return Err(NativeProbeError::EmptyModVersion {
579 module: library.module,
580 });
581 }
582
583 if version_at_least(&installed, library.minimum_version)? {
584 return Ok(NativeLibraryStatus::Satisfied);
585 }
586
587 let release = match library.release_version_variable {
588 Some(variable) => {
589 let output = host
590 .output(
591 "pkg-config",
592 [format!("--variable={variable}").as_str(), library.module],
593 )
594 .await?;
595 let value = String::from_utf8_lossy(&output.stdout).trim().to_owned();
596 (output.status.success() && !value.is_empty()).then_some(value)
597 }
598 None => None,
599 };
600
601 Ok(NativeLibraryStatus::TooOld { installed, release })
602}
603
604fn version_at_least(installed: &str, minimum: &str) -> Result<bool, DottedVersionError> {
615 let installed = parse_version(installed)?;
616 let minimum = parse_version(minimum)?;
617 let len = installed.len().max(minimum.len());
618 for index in 0..len {
619 let left = installed.get(index).copied().unwrap_or(0);
620 let right = minimum.get(index).copied().unwrap_or(0);
621 if left != right {
622 return Ok(left > right);
623 }
624 }
625 Ok(true)
626}
627
628fn parse_version(version: &str) -> Result<Vec<u64>, DottedVersionError> {
630 version
631 .split('.')
632 .map(|component| {
633 component
634 .parse::<u64>()
635 .map_err(|source| DottedVersionError {
636 version: version.to_owned(),
637 source,
638 })
639 })
640 .collect()
641}
642
643async fn run_with_optional_sudo(
644 host: &Host,
645 command: &str,
646 args: &[String],
647) -> Result<(), CommandError> {
648 if host.which("sudo").await.is_ok() {
649 let mut sudo_args = Vec::with_capacity(args.len() + 1);
650 sudo_args.push(command.to_string());
651 sudo_args.extend(args.iter().cloned());
652 host.run("sudo", sudo_args.iter().map(String::as_str))
653 .await?;
654 } else {
655 host.run(command, args.iter().map(String::as_str)).await?;
656 }
657 Ok(())
658}
659
660async fn install_missing_packages(
661 host: &Host,
662 manager: LinuxPackageManager,
663 packages: &[String],
664) -> Result<(), CommandError> {
665 if packages.is_empty() {
666 return Ok(());
667 }
668
669 match manager {
670 LinuxPackageManager::Apt => {
671 if packages.iter().any(|package| package.ends_with(":amd64")) {
672 ensure_apt_foreign_architecture(host, "amd64").await?;
673 }
674 run_with_optional_sudo(host, "apt-get", &[String::from("update")]).await?;
675 let mut args = vec![String::from("install"), String::from("-y")];
676 args.extend(packages.iter().cloned());
677 run_with_optional_sudo(host, "apt-get", &args).await?;
678 }
679 LinuxPackageManager::Dnf => {
680 let mut args = vec![String::from("install"), String::from("-y")];
681 args.extend(packages.iter().cloned());
682 run_with_optional_sudo(host, "dnf", &args).await?;
683 }
684 LinuxPackageManager::Pacman => {
685 let mut args = vec![
686 String::from("-S"),
687 String::from("--noconfirm"),
688 String::from("--needed"),
689 ];
690 args.extend(packages.iter().cloned());
691 run_with_optional_sudo(host, "pacman", &args).await?;
692 }
693 LinuxPackageManager::Zypper => {
694 let mut args = vec![
695 String::from("--non-interactive"),
696 String::from("install"),
697 String::from("--auto-agree-with-licenses"),
698 ];
699 args.extend(packages.iter().cloned());
700 run_with_optional_sudo(host, "zypper", &args).await?;
701 }
702 LinuxPackageManager::Apk => {
703 let mut args = vec![String::from("add")];
704 args.extend(packages.iter().cloned());
705 run_with_optional_sudo(host, "apk", &args).await?;
706 }
707 }
708
709 Ok(())
710}
711
712async fn ensure_apt_foreign_architecture(
713 host: &Host,
714 architecture: &str,
715) -> Result<(), CommandError> {
716 let output = host.run("dpkg", ["--print-foreign-architectures"]).await?;
717 if output.lines().any(|line| line.trim() == architecture) {
718 return Ok(());
719 }
720 run_with_optional_sudo(
721 host,
722 "dpkg",
723 &[String::from("--add-architecture"), architecture.to_string()],
724 )
725 .await
726}
727
728fn required_packages_to_owned(packages: &[&str]) -> Vec<String> {
729 packages
730 .iter()
731 .map(|package| (*package).to_string())
732 .collect()
733}
734
735fn unsupported_manager_hint() -> String {
736 let apt_hint = LinuxPackageManager::Apt.install_hint(&required_packages_to_owned(
737 LinuxPackageManager::Apt.required_packages(),
738 ));
739 let dnf_hint = LinuxPackageManager::Dnf.install_hint(&required_packages_to_owned(
740 LinuxPackageManager::Dnf.required_packages(),
741 ));
742 let pacman_hint = LinuxPackageManager::Pacman.install_hint(&required_packages_to_owned(
743 LinuxPackageManager::Pacman.required_packages(),
744 ));
745 let zypper_hint = LinuxPackageManager::Zypper.install_hint(&required_packages_to_owned(
746 LinuxPackageManager::Zypper.required_packages(),
747 ));
748 let alpine_hint = LinuxPackageManager::Apk.install_hint(&required_packages_to_owned(
749 LinuxPackageManager::Apk.required_packages(),
750 ));
751 format!(
752 "Install required packages manually. Debian/Ubuntu: `{apt_hint}`; Fedora/RHEL: `{dnf_hint}`; Arch: `{pacman_hint}`; openSUSE: `{zypper_hint}`; Alpine: `{alpine_hint}`."
753 )
754}
755
756pub async fn has_supported_package_manager(host: &Host) -> bool {
758 LinuxPackageManager::detect(host).await.is_some()
759}
760
761pub async fn gtk4_pkg_config_repair_installation(
769 host: &Host,
770 missing_modules: &[String],
771) -> Result<LinuxSystemPackagesInstallation, UnfixableToolchain> {
772 let Some(manager) = LinuxPackageManager::detect(host).await else {
773 return Err(UnfixableToolchain::new(
774 "Unable to detect Linux package manager",
775 unsupported_manager_hint(),
776 ));
777 };
778
779 let mut packages = Vec::new();
780 for module in missing_modules {
781 let package = manager
782 .package_for_gtk_pkg_config_probe(module)
783 .ok_or_else(|| {
784 UnfixableToolchain::new(
785 format!("No package mapping is defined for GTK probe `{module}`"),
786 "Install a package that provides the missing module via pkg-config, then re-run `water doctor`.",
787 )
788 })?;
789 if !packages.iter().any(|existing| existing == package) {
790 packages.push(package.to_owned());
791 }
792 }
793
794 LinuxSystemPackagesInstallation::from_packages(host, packages).await
795}
796
797pub async fn install_named_packages(
803 host: &Host,
804 packages: &[&'static str],
805) -> Result<(), LinuxPackageManagerError> {
806 let Some(manager) = LinuxPackageManager::detect(host).await else {
807 return Err(LinuxPackageManagerError::UnsupportedPackageManager);
808 };
809
810 let packages: Vec<String> = packages
811 .iter()
812 .map(|package| (*package).to_string())
813 .collect();
814 install_missing_packages(host, manager, &packages).await?;
815 Ok(())
816}
817
818pub async fn install_java_jdk(host: &Host) -> Result<(), LinuxPackageManagerError> {
824 let Some(manager) = LinuxPackageManager::detect(host).await else {
825 return Err(LinuxPackageManagerError::UnsupportedPackageManager);
826 };
827
828 let packages: Vec<String> = match manager {
829 LinuxPackageManager::Apt => vec![String::from("openjdk-21-jdk")],
830 LinuxPackageManager::Dnf | LinuxPackageManager::Zypper => {
831 vec![String::from("java-21-openjdk-devel")]
832 }
833 LinuxPackageManager::Pacman => vec![String::from("jdk-openjdk")],
834 LinuxPackageManager::Apk => vec![String::from("openjdk21-jdk")],
835 };
836
837 install_missing_packages(host, manager, &packages).await?;
838 Ok(())
839}
840
841#[cfg(test)]
842mod tests {
843 use super::{
844 LinuxPackageManager, NativeLibraryStatus, VERSIONED_NATIVE_LIBRARIES,
845 VersionedNativeLibrary, version_at_least,
846 };
847
848 fn interpret_pkg_config(
851 library: VersionedNativeLibrary,
852 modversion: Option<&str>,
853 release: Option<&str>,
854 ) -> NativeLibraryStatus {
855 let Some(modversion) = modversion else {
856 return NativeLibraryStatus::ModuleMissing;
857 };
858 if version_at_least(modversion.trim(), library.minimum_version).unwrap() {
859 NativeLibraryStatus::Satisfied
860 } else {
861 NativeLibraryStatus::TooOld {
862 installed: modversion.trim().to_owned(),
863 release: release.map(str::to_owned),
864 }
865 }
866 }
867
868 fn library(module: &str) -> VersionedNativeLibrary {
869 *VERSIONED_NATIVE_LIBRARIES
870 .iter()
871 .find(|library| library.module == module)
872 .expect("library is checked by the doctor")
873 }
874
875 #[test]
876 fn version_comparison_pads_missing_components() {
877 assert!(version_at_least("1.19.0", "1.19").unwrap());
878 assert!(version_at_least("1.19", "1.19.0").unwrap());
879 assert!(version_at_least("1.20.0", "1.19").unwrap());
880 assert!(!version_at_least("1.14.0", "1.19").unwrap());
881 assert!(!version_at_least("1.2.0", "1.19").unwrap());
882 assert!(version_at_least("1.4.7", "0.3.65").unwrap());
883 assert!(!version_at_least("0.3.48", "0.3.65").unwrap());
884 assert!(version_at_least("0.3.65", "0.3.65").unwrap());
885 }
886
887 #[test]
888 fn version_comparison_rejects_unreadable_versions() {
889 let error = version_at_least("1.19.0-rc1", "1.19").unwrap_err();
890 assert!(error.to_string().contains("dotted-numeric"));
891 }
892
893 #[test]
894 fn ubuntu_2204_libva_is_reported_as_too_old() {
895 let libva = library("libva");
896 let status = interpret_pkg_config(libva, Some("1.14.0\n"), Some("2.14.0"));
898 let NativeLibraryStatus::TooOld { installed, release } = status else {
899 panic!("VA-API 1.14 must not satisfy the cros-libva floor");
900 };
901 let message = libva.outdated_message(&installed, release.as_deref());
902 assert_eq!(
903 message,
904 "`libva` is too old: pkg-config reports VA-API 1.14.0 (release 2.14.0), but `cros-libva 0.0.12` needs VA-API 1.19 or newer"
905 );
906
907 let suggestion = libva.outdated_suggestion(Some("libva-dev"));
908 assert!(suggestion.starts_with(
909 "`libva-dev` is already installed, so installing it again will not help."
910 ));
911 assert!(suggestion.contains("Ubuntu 22.04 (libva 2.14)"));
912 assert!(suggestion.contains("build libva 2.19 or newer"));
913 assert!(suggestion.contains("pkg-config --modversion libva"));
914 }
915
916 #[test]
917 fn ubuntu_2404_libva_satisfies_the_floor() {
918 let status = interpret_pkg_config(library("libva"), Some("1.20.0\n"), Some("2.20.0"));
920 assert_eq!(status, NativeLibraryStatus::Satisfied);
921 }
922
923 #[test]
924 fn ubuntu_2204_pipewire_is_reported_as_too_old() {
925 let pipewire = library("libpipewire-0.3");
926 let status = interpret_pkg_config(pipewire, Some("0.3.48\n"), None);
927 let NativeLibraryStatus::TooOld { installed, release } = status else {
928 panic!("PipeWire 0.3.48 must not satisfy the libspa floor");
929 };
930 assert_eq!(release, None);
931 let message = pipewire.outdated_message(&installed, release.as_deref());
932 assert_eq!(
933 message,
934 "`libpipewire-0.3` is too old: pkg-config reports PipeWire 0.3.48, but `libspa 0.10.1` needs PipeWire 0.3.65 or newer"
935 );
936
937 let suggestion = pipewire.outdated_suggestion(Some("libpipewire-0.3-dev"));
938 assert!(suggestion.contains("spa_video_info_raw::flags"));
939 assert!(suggestion.contains("Ubuntu 22.04 (PipeWire 0.3.48)"));
940 }
941
942 #[test]
943 fn missing_module_is_reported_as_missing_not_outdated() {
944 assert_eq!(
945 interpret_pkg_config(library("libva"), None, None),
946 NativeLibraryStatus::ModuleMissing
947 );
948 }
949
950 #[test]
951 fn outdated_suggestion_without_a_package_mapping_still_reads() {
952 let suggestion = library("libva").outdated_suggestion(None);
953 assert!(suggestion.starts_with("The package providing `libva` is already installed,"));
954 }
955
956 #[test]
957 fn every_manager_maps_the_version_checked_modules() {
958 for manager in [
959 LinuxPackageManager::Apt,
960 LinuxPackageManager::Dnf,
961 LinuxPackageManager::Pacman,
962 LinuxPackageManager::Zypper,
963 LinuxPackageManager::Apk,
964 ] {
965 for library in VERSIONED_NATIVE_LIBRARIES {
966 assert!(
967 manager.package_for_native_library(library.module).is_some(),
968 "{} has no package mapping for {}",
969 manager.name(),
970 library.module
971 );
972 }
973 }
974 }
975
976 #[test]
981 #[ignore = "fetches the pinned framework revision's lockfile over the network"]
982 fn crate_versions_match_lockfile() {
983 let (framework, revision) = crate::pinned_framework::source();
984 let lockfile = String::from_utf8(crate::pinned_framework::fetch(
985 &crate::pinned_framework::raw_url(&framework, &revision, "Cargo.lock"),
986 ))
987 .expect("the framework lockfile is UTF-8");
988 for library in VERSIONED_NATIVE_LIBRARIES {
989 let entry = format!(
990 "name = \"{}\"\nversion = \"{}\"\n",
991 library.required_by, library.required_by_version
992 );
993 assert!(
994 lockfile.contains(&entry),
995 "{} {} is no longer the resolved version; re-read its version floor before changing this constant",
996 library.required_by,
997 library.required_by_version
998 );
999 }
1000 }
1001
1002 #[test]
1003 fn dnf_packages_include_validated_core_deps() {
1004 let required = LinuxPackageManager::Dnf.required_packages();
1005 assert!(required.contains(&"gtk4-devel"));
1006 assert!(required.contains(&"pango-devel"));
1007 assert!(required.contains(&"wayland-devel"));
1008 assert!(required.contains(&"libva-devel"));
1009 assert!(required.contains(&"mesa-libgbm-devel"));
1010 assert!(required.contains(&"libxcb-devel"));
1011 assert!(required.contains(&"alsa-lib-devel"));
1012 assert!(required.contains(&"clang-devel"));
1013 assert!(required.contains(&"fontconfig-devel"));
1014 }
1015
1016 #[test]
1017 fn apt_hint_uses_apt_get_install() {
1018 let hint = LinuxPackageManager::Apt
1019 .install_hint(&[String::from("libwayland-dev"), String::from("libva-dev")]);
1020 assert_eq!(hint, "sudo apt-get install -y libwayland-dev libva-dev");
1021 }
1022
1023 #[test]
1024 fn apt_required_packages_include_gtk4_dev() {
1025 let required = LinuxPackageManager::Apt.required_packages();
1026 assert!(required.contains(&"libgtk-4-dev"));
1027 assert!(required.contains(&"libpango1.0-dev"));
1028 }
1029
1030 #[test]
1031 fn pacman_hint_uses_needed_flag() {
1032 let hint = LinuxPackageManager::Pacman
1033 .install_hint(&[String::from("wayland"), String::from("libva")]);
1034 assert_eq!(hint, "sudo pacman -S --noconfirm --needed wayland libva");
1035 }
1036
1037 #[test]
1038 fn dnf_probe_mapping_covers_pango_and_gtk4() {
1039 assert_eq!(
1040 LinuxPackageManager::Dnf.package_for_gtk_pkg_config_probe("gtk4"),
1041 Some("gtk4-devel")
1042 );
1043 assert_eq!(
1044 LinuxPackageManager::Dnf.package_for_gtk_pkg_config_probe("pango>=1.50"),
1045 Some("pango-devel")
1046 );
1047 }
1048}
1049
1050#[cfg(all(test, target_os = "linux"))]
1054mod host_tests {
1055 use super::LinuxSystemToolchain;
1056 use crate::toolchain::testing::TestMachine;
1057 use crate::toolchain::{Toolchain, ToolchainError};
1058
1059 const APT_PACKAGES: &str = "pkg-config libgtk-4-dev libpango1.0-dev libwayland-dev \
1060 wayland-protocols libasound2-dev libva-dev libgbm-dev libxcb1-dev \
1061 libclang-dev libfontconfig-dev";
1062
1063 fn complete_apt_machine() -> TestMachine {
1066 let machine = TestMachine::new();
1067 for tool in ["apt-get", "dpkg-query", "pkg-config"] {
1068 machine.install(tool);
1069 }
1070 machine.respond_pkg_config_module("libva", "1.20.0");
1071 machine.respond_pkg_config_var("libva_version", "2.20.0");
1072 machine.respond_pkg_config_module("libpipewire-0.3", "0.3.65");
1073 machine
1074 }
1075
1076 #[test]
1077 fn unfixable_without_package_manager() {
1078 let machine = TestMachine::new();
1079 let host = machine.host(Vec::<(String, String)>::new());
1080 let result = smol::block_on(LinuxSystemToolchain.check(&host));
1081 assert!(
1082 matches!(result, Err(ToolchainError::Unfixable(_))),
1083 "no package manager must be unfixable: {result:?}"
1084 );
1085 }
1086
1087 #[test]
1088 fn ok_when_apt_packages_and_library_versions_satisfy_floors() {
1089 let machine = complete_apt_machine();
1090 let host = machine.host([(
1091 String::from("WATERUI_FAKE_DPKG_INSTALLED"),
1092 APT_PACKAGES.to_string(),
1093 )]);
1094 smol::block_on(LinuxSystemToolchain.check(&host))
1095 .expect("complete apt package set must satisfy the check");
1096 }
1097
1098 #[test]
1099 fn fixable_installation_lists_only_missing_packages() {
1100 let machine = complete_apt_machine();
1101 let host = machine.host([(
1102 String::from("WATERUI_FAKE_DPKG_INSTALLED"),
1103 String::from("pkg-config libgtk-4-dev"),
1104 )]);
1105 let Err(ToolchainError::Fixable(installation)) =
1106 smol::block_on(LinuxSystemToolchain.check(&host))
1107 else {
1108 panic!("missing apt packages must produce a fixable installation");
1109 };
1110 assert_eq!(installation.package_manager_name(), "apt-get");
1111 let missing = installation.missing_packages();
1112 assert!(missing.iter().any(|package| package == "libva-dev"));
1113 assert!(!missing.iter().any(|package| package == "libgtk-4-dev"));
1114 }
1115
1116 #[test]
1117 fn unfixable_when_pkg_config_missing() {
1118 let machine = TestMachine::new();
1119 machine.install("apt-get");
1120 machine.install("dpkg-query");
1121 let host = machine.host([(
1122 String::from("WATERUI_FAKE_DPKG_INSTALLED"),
1123 APT_PACKAGES.to_string(),
1124 )]);
1125 let result = smol::block_on(LinuxSystemToolchain.check(&host));
1126 assert!(
1127 matches!(result, Err(ToolchainError::Unfixable(_))),
1128 "absent pkg-config must be an unfixable diagnostic: {result:?}"
1129 );
1130 }
1131
1132 #[test]
1133 fn unfixable_when_libva_below_floor() {
1134 let machine = complete_apt_machine();
1135 machine.respond_pkg_config_module("libva", "1.14.0");
1138 machine.respond_pkg_config_var("libva_version", "2.14.0");
1139 let host = machine.host([(
1140 String::from("WATERUI_FAKE_DPKG_INSTALLED"),
1141 APT_PACKAGES.to_string(),
1142 )]);
1143 let result = smol::block_on(LinuxSystemToolchain.check(&host));
1144 assert!(
1145 matches!(result, Err(ToolchainError::Unfixable(_))),
1146 "an outdated libva must be unfixable (reinstalling changes nothing): {result:?}"
1147 );
1148 }
1149
1150 #[test]
1151 fn fixable_when_versioned_module_absent_from_pkg_config() {
1152 let machine = TestMachine::new();
1153 for tool in ["apt-get", "dpkg-query", "pkg-config"] {
1154 machine.install(tool);
1155 }
1156 machine.respond_pkg_config_module("libva", "1.20.0");
1158 machine.respond_pkg_config_var("libva_version", "2.20.0");
1159 let host = machine.host([(
1160 String::from("WATERUI_FAKE_DPKG_INSTALLED"),
1161 APT_PACKAGES.to_string(),
1162 )]);
1163 let Err(ToolchainError::Fixable(installation)) =
1164 smol::block_on(LinuxSystemToolchain.check(&host))
1165 else {
1166 panic!("an absent libpipewire module must map to an installable package");
1167 };
1168 assert_eq!(
1169 installation.missing_packages(),
1170 &[String::from("libpipewire-0.3-dev")]
1171 );
1172 }
1173}