1#![allow(dead_code)] use std::process::{Command, Output};
4use std::sync::OnceLock;
5
6use crate::network::config::NetworkConfig;
7use crate::network::propagate::apply_network_config;
8
9#[derive(thiserror::Error, Debug)]
10pub enum InstallError {
11 #[error("unsupported platform")]
12 Unsupported,
13 #[error("prerequisite missing: {0}")]
14 Prereq(&'static str),
15 #[error("invalid permissions: {0}")]
16 InvalidPermissions(&'static str),
17 #[error("command failed: {cmd} (code: {code:?})\n{stderr}")]
18 CommandFailed {
19 cmd: String,
20 code: Option<i32>,
21 stderr: String,
22 },
23 #[error("io error: {0}")]
24 Io(#[from] std::io::Error),
25 #[error("parse error: {0}")]
26 Parse(&'static str),
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)]
31#[serde(rename_all = "lowercase")]
32pub enum Os {
33 Linux,
34 Macos,
35 Windows,
36 Bsd,
37}
38
39pub fn current_os() -> Os {
41 #[cfg(target_os = "linux")]
42 {
43 Os::Linux
44 }
45 #[cfg(target_os = "macos")]
46 {
47 Os::Macos
48 }
49 #[cfg(target_os = "windows")]
50 {
51 Os::Windows
52 }
53 #[cfg(target_os = "freebsd")]
54 {
55 Os::Bsd
56 }
57}
58
59static USE_SUDO_DEFAULT: OnceLock<Option<bool>> = OnceLock::new();
62
63pub fn set_default_use_sudo(val: Option<bool>) {
64 let _ = USE_SUDO_DEFAULT.set(val);
65}
66
67pub fn default_use_sudo() -> Option<bool> {
68 if let Some(v) = USE_SUDO_DEFAULT.get() {
69 *v
70 } else {
71 None
73 }
74}
75
76pub fn run_capture(cmd: &str, args: &[&str], stage: &str, context: &str) -> Option<Output> {
89 match Command::new(cmd).args(args).output() {
90 Ok(out) => Some(out),
91 Err(e) => {
92 tracing::warn!(
93 event = "setup.subprocess.failed",
94 stage = %stage,
95 command = %cmd,
96 context = %context,
97 error = %e,
98 );
99 eprintln!("{context}: {e}");
100 None
101 }
102 }
103}
104
105#[must_use = "this Result may contain an error that should be handled"]
106pub fn run(cmd: &str, args: &[&str]) -> Result<Output, InstallError> {
107 if std::env::var_os("JARVY_FAST_TEST").is_some() {
110 return Err(InstallError::Prereq(
111 "skipped external command in fast test mode",
112 ));
113 }
114 #[cfg(test)]
115 {
116 if std::env::var_os("JARVY_RUN_EXTERNAL_CMDS_IN_TEST").is_none() {
117 return Err(InstallError::Prereq(
118 "external commands disabled during unit tests",
119 ));
120 }
121 }
122
123 let out = Command::new(cmd).args(args).output().map_err(|e| {
124 use std::io::ErrorKind::*;
125 match e.kind() {
126 NotFound => InstallError::Prereq("required command not found on PATH"),
127 PermissionDenied => {
128 InstallError::InvalidPermissions("operation requires elevated privileges")
129 }
130 _ => InstallError::Io(e),
131 }
132 })?;
133
134 if !out.status.success() {
135 return Err(InstallError::CommandFailed {
137 cmd: cmd.to_string(),
138 code: out.status.code(),
139 stderr: String::from_utf8_lossy(&out.stderr).into(),
140 });
141 }
142 Ok(out)
143}
144
145#[must_use = "this Result may contain an error that should be handled"]
150pub fn run_with_network(
151 cmd: &str,
152 args: &[&str],
153 network: Option<&NetworkConfig>,
154 tool_name: &str,
155) -> Result<Output, InstallError> {
156 if std::env::var_os("JARVY_FAST_TEST").is_some() {
158 return Err(InstallError::Prereq(
159 "skipped external command in fast test mode",
160 ));
161 }
162 #[cfg(test)]
163 {
164 if std::env::var_os("JARVY_RUN_EXTERNAL_CMDS_IN_TEST").is_none() {
165 return Err(InstallError::Prereq(
166 "external commands disabled during unit tests",
167 ));
168 }
169 }
170
171 let mut command = Command::new(cmd);
172 command.args(args);
173
174 if let Some(net_config) = network {
176 apply_network_config(&mut command, net_config, tool_name);
177 }
178
179 let out = command.output().map_err(|e| {
180 use std::io::ErrorKind::*;
181 match e.kind() {
182 NotFound => InstallError::Prereq("required command not found on PATH"),
183 PermissionDenied => {
184 InstallError::InvalidPermissions("operation requires elevated privileges")
185 }
186 _ => InstallError::Io(e),
187 }
188 })?;
189
190 if !out.status.success() {
191 return Err(InstallError::CommandFailed {
192 cmd: cmd.to_string(),
193 code: out.status.code(),
194 stderr: String::from_utf8_lossy(&out.stderr).into(),
195 });
196 }
197 Ok(out)
198}
199
200#[must_use = "this Result may contain an error that should be handled"]
202pub fn run_maybe_sudo(use_sudo: bool, cmd: &str, args: &[&str]) -> Result<Output, InstallError> {
203 match current_os() {
204 Os::Windows => run(cmd, args),
205 Os::Linux | Os::Macos | Os::Bsd => {
206 if use_sudo {
207 let mut all = Vec::with_capacity(1 + args.len());
209 all.push(cmd);
210 all.extend_from_slice(args);
211 run("sudo", &all)
212 } else {
213 run(cmd, args)
214 }
215 }
216 }
217}
218
219#[must_use = "this Result may contain an error that should be handled"]
223pub fn run_maybe_sudo_with_network(
224 use_sudo: bool,
225 cmd: &str,
226 args: &[&str],
227 network: Option<&NetworkConfig>,
228 tool_name: &str,
229) -> Result<Output, InstallError> {
230 match current_os() {
231 Os::Windows => run_with_network(cmd, args, network, tool_name),
232 Os::Linux | Os::Macos | Os::Bsd => {
233 if use_sudo {
234 let mut all = Vec::with_capacity(2 + args.len());
237 all.push("-E"); all.push(cmd);
239 all.extend_from_slice(args);
240 run_with_network("sudo", &all, network, tool_name)
241 } else {
242 run_with_network(cmd, args, network, tool_name)
243 }
244 }
245 }
246}
247
248pub fn has(cmd: &str) -> bool {
249 Command::new(cmd)
250 .arg("--version")
251 .output()
252 .map(|o| o.status.success())
253 .unwrap_or(false)
254}
255
256pub fn require(cmd: &str, remediation: &'static str) -> Result<(), InstallError> {
258 if has(cmd) {
259 Ok(())
260 } else {
261 Err(InstallError::Prereq(remediation))
262 }
263}
264
265pub fn require_any<'a>(
267 candidates: &[&'a str],
268 remediation: &'static str,
269) -> Result<&'a str, InstallError> {
270 for c in candidates {
271 if has(c) {
272 return Ok(*c);
273 }
274 }
275 Err(InstallError::Prereq(remediation))
276}
277
278pub fn cmd_satisfies(cmd: &str, requirement: &str) -> bool {
288 if let Ok(out) = Command::new(cmd).arg("--version").output() {
289 let version_output = String::from_utf8_lossy(&out.stdout);
290 return super::version::version_satisfies(&version_output, requirement);
291 }
292 false
293}
294
295pub fn plan_sudo_attempts(use_sudo: Option<bool>, sudo_available: bool) -> Vec<bool> {
296 match use_sudo {
297 Some(flag) => vec![flag],
298 None => {
299 if sudo_available {
300 vec![false, true]
301 } else {
302 vec![false]
303 }
304 }
305 }
306}
307
308#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
309pub enum PackageManager {
310 Apt,
311 Dnf,
312 Yum,
313 Zypper,
314 Pacman,
315 Apk,
316 Brew,
317 BrewCask, Winget,
319 Choco,
320 Pkg, }
322
323#[cfg(target_os = "linux")]
324pub fn detect_linux_pm() -> Option<PackageManager> {
325 use std::{fs, process::Command};
326 let has = |c| {
327 Command::new(c)
328 .arg("--version")
329 .output()
330 .map(|o| o.status.success())
331 .unwrap_or(false)
332 };
333
334 let _os_release = fs::read_to_string("/etc/os-release").unwrap_or_default();
337
338 if has("apt-get") || has("apt") {
339 return Some(PackageManager::Apt);
340 }
341 if has("dnf") {
342 return Some(PackageManager::Dnf);
343 }
344 if has("yum") {
345 return Some(PackageManager::Yum);
346 }
347 if has("zypper") {
348 return Some(PackageManager::Zypper);
349 }
350 if has("pacman") {
351 return Some(PackageManager::Pacman);
352 }
353 if has("apk") {
354 return Some(PackageManager::Apk);
355 }
356 None
357}
358
359#[cfg(target_os = "freebsd")]
360pub fn detect_bsd_pm() -> Option<PackageManager> {
361 use std::process::Command;
362 let has = |c| {
363 Command::new(c)
364 .arg("--version")
365 .output()
366 .map(|o| o.status.success())
367 .unwrap_or(false)
368 };
369
370 if has("pkg") {
371 return Some(PackageManager::Pkg);
372 }
373 None
374}
375
376#[cfg(test)]
377mod sudo_plan_tests {
378 use super::plan_sudo_attempts;
379
380 #[test]
381 fn plan_some_true_only_true() {
382 let v = plan_sudo_attempts(Some(true), true);
383 assert_eq!(v, vec![true]);
384 }
385
386 #[test]
387 fn plan_some_false_only_false() {
388 let v = plan_sudo_attempts(Some(false), true);
389 assert_eq!(v, vec![false]);
390 }
391
392 #[test]
393 fn plan_none_with_sudo_available() {
394 let v = plan_sudo_attempts(None, true);
395 assert_eq!(v, vec![false, true]);
396 }
397
398 #[test]
399 fn plan_none_without_sudo_available() {
400 let v = plan_sudo_attempts(None, false);
401 assert_eq!(v, vec![false]);
402 }
403}
404
405#[cfg(test)]
406mod batch_install_tests {
407 use super::*;
408
409 #[test]
410 fn empty_list_returns_empty_result() {
411 let result = PkgOps::batch_install(PackageManager::Brew, &[], None);
412 assert!(result.is_ok());
413 let result = result.unwrap();
414 assert!(result.succeeded.is_empty());
415 assert!(result.failed.is_empty());
416 }
417
418 #[test]
419 fn batch_install_result_default() {
420 let result = BatchInstallResult {
421 succeeded: vec!["foo".to_string()],
422 failed: vec![("bar".to_string(), "error".to_string())],
423 };
424 assert_eq!(result.succeeded.len(), 1);
425 assert_eq!(result.failed.len(), 1);
426 assert_eq!(result.succeeded[0], "foo");
427 assert_eq!(result.failed[0].0, "bar");
428 }
429
430 #[test]
431 fn package_manager_has_required_traits() {
432 let mut map = std::collections::HashMap::new();
434 map.insert(PackageManager::Brew, vec!["jq", "ripgrep"]);
435 map.insert(PackageManager::Apt, vec!["git", "curl"]);
436 assert_eq!(map.len(), 2);
437 assert!(map.contains_key(&PackageManager::Brew));
438 assert!(map.contains_key(&PackageManager::Apt));
439 }
440
441 #[test]
442 fn package_manager_equality() {
443 assert_eq!(PackageManager::Brew, PackageManager::Brew);
444 assert_ne!(PackageManager::Brew, PackageManager::Apt);
445 assert_eq!(PackageManager::BrewCask, PackageManager::BrewCask);
446 assert_ne!(PackageManager::Winget, PackageManager::Choco);
447 }
448}
449
450#[allow(dead_code)]
451pub struct PkgOps {
452 name: &'static str,
453}
454
455#[derive(Debug)]
457pub struct BatchInstallResult {
458 pub succeeded: Vec<String>,
460 pub failed: Vec<(String, String)>,
462}
463
464impl PkgOps {
465 pub fn batch_install(
475 pm: PackageManager,
476 packages: &[&str],
477 use_sudo: Option<bool>,
478 ) -> Result<BatchInstallResult, InstallError> {
479 if packages.is_empty() {
480 return Ok(BatchInstallResult {
481 succeeded: vec![],
482 failed: vec![],
483 });
484 }
485
486 if packages.len() == 1 {
488 match Self::install(pm, packages[0], use_sudo) {
489 Ok(()) => {
490 return Ok(BatchInstallResult {
491 succeeded: vec![packages[0].to_string()],
492 failed: vec![],
493 });
494 }
495 Err(e) => {
496 return Ok(BatchInstallResult {
497 succeeded: vec![],
498 failed: vec![(packages[0].to_string(), format!("{}", e))],
499 });
500 }
501 }
502 }
503
504 let batch_result = Self::try_batch_install(pm, packages, use_sudo);
506
507 match batch_result {
508 Ok(()) => {
509 Ok(BatchInstallResult {
511 succeeded: packages.iter().map(|s| s.to_string()).collect(),
512 failed: vec![],
513 })
514 }
515 Err(_) => {
516 let mut succeeded = Vec::with_capacity(packages.len());
518 let mut failed = Vec::with_capacity(packages.len());
519
520 for pkg in packages {
521 match Self::install(pm, pkg, use_sudo) {
522 Ok(()) => succeeded.push(pkg.to_string()),
523 Err(e) => failed.push((pkg.to_string(), format!("{}", e))),
524 }
525 }
526
527 Ok(BatchInstallResult { succeeded, failed })
528 }
529 }
530 }
531
532 fn try_batch_install(
535 pm: PackageManager,
536 packages: &[&str],
537 use_sudo: Option<bool>,
538 ) -> Result<(), InstallError> {
539 match pm {
540 PackageManager::Apt => {
541 let apt = require_any(&["apt-get", "apt"], "apt is required to install packages")?;
542 let mut args = vec!["install", "-y"];
543 args.extend(packages);
544 Self::run_with_sudo_strategy(use_sudo, apt, &args)
545 }
546 PackageManager::Dnf => {
547 require("dnf", "dnf is required to install packages")?;
548 let mut args = vec!["install", "-y"];
549 args.extend(packages);
550 Self::run_with_sudo_strategy(use_sudo, "dnf", &args)
551 }
552 PackageManager::Yum => {
553 require("yum", "yum is required to install packages")?;
554 let mut args = vec!["install", "-y"];
555 args.extend(packages);
556 Self::run_with_sudo_strategy(use_sudo, "yum", &args)
557 }
558 PackageManager::Zypper => {
559 require("zypper", "zypper is required to install packages")?;
560 let mut args = vec!["--non-interactive", "install", "--no-confirm"];
561 args.extend(packages);
562 Self::run_with_sudo_strategy(use_sudo, "zypper", &args)
563 }
564 PackageManager::Pacman => {
565 require("pacman", "pacman is required to install packages")?;
566 let mut args = vec!["--noconfirm", "-S"];
567 args.extend(packages);
568 Self::run_with_sudo_strategy(use_sudo, "pacman", &args)
569 }
570 PackageManager::Apk => {
571 require("apk", "apk is required to install packages")?;
572 let mut args = vec!["add"];
573 args.extend(packages);
574 Self::run_with_sudo_strategy(use_sudo, "apk", &args)
575 }
576 PackageManager::Brew => {
577 require("brew", "Homebrew is required to install packages")?;
578 let mut args = vec!["install"];
579 args.extend(packages);
580 run("brew", &args)?;
581 Ok(())
582 }
583 PackageManager::Winget => {
584 require("winget", "Winget is required to install packages")?;
587 for pkg in packages {
588 run("winget", &["install", "-e", "--id", pkg])?;
589 }
590 Ok(())
591 }
592 PackageManager::BrewCask => {
593 require("brew", "Homebrew is required to install casks")?;
594 let mut args = vec!["install", "--cask"];
595 args.extend(packages);
596 run("brew", &args)?;
597 Ok(())
598 }
599 PackageManager::Choco => {
600 require("choco", "Chocolatey is required to install packages")?;
601 let mut args = vec!["install", "-y"];
602 args.extend(packages);
603 run("choco", &args)?;
604 Ok(())
605 }
606 PackageManager::Pkg => {
607 require("pkg", "FreeBSD pkg is required to install packages")?;
608 let mut args = vec!["install", "-y"];
609 args.extend(packages);
610 Self::run_with_sudo_strategy(use_sudo, "pkg", &args)
611 }
612 }
613 }
614
615 fn run_with_sudo_strategy(
617 use_sudo: Option<bool>,
618 cmd: &str,
619 args: &[&str],
620 ) -> Result<(), InstallError> {
621 match use_sudo {
622 Some(flag) => {
623 if flag {
624 require("sudo", "sudo is required to install packages")?;
625 }
626 run_maybe_sudo(flag, cmd, args)?;
627 Ok(())
628 }
629 None => {
630 if let Err(e) = run_maybe_sudo(false, cmd, args) {
631 if has("sudo") {
632 run_maybe_sudo(true, cmd, args)?;
633 Ok(())
634 } else {
635 Err(e)
636 }
637 } else {
638 Ok(())
639 }
640 }
641 }
642 }
643
644 pub fn batch_install_cask(packages: &[&str]) -> Result<BatchInstallResult, InstallError> {
646 if packages.is_empty() {
647 return Ok(BatchInstallResult {
648 succeeded: vec![],
649 failed: vec![],
650 });
651 }
652
653 require("brew", "Homebrew is required to install casks")?;
654
655 let mut args = vec!["install", "--cask"];
656 args.extend(packages);
657
658 match run("brew", &args) {
659 Ok(_) => Ok(BatchInstallResult {
660 succeeded: packages.iter().map(|s| s.to_string()).collect(),
661 failed: vec![],
662 }),
663 Err(_) => {
664 let mut succeeded = Vec::with_capacity(packages.len());
666 let mut failed = Vec::with_capacity(packages.len());
667 for pkg in packages {
668 match run("brew", &["install", "--cask", pkg]) {
669 Ok(_) => succeeded.push(pkg.to_string()),
670 Err(e) => failed.push((pkg.to_string(), format!("{}", e))),
671 }
672 }
673 Ok(BatchInstallResult { succeeded, failed })
674 }
675 }
676 }
677
678 pub fn batch_install_choco(packages: &[&str]) -> Result<BatchInstallResult, InstallError> {
680 if packages.is_empty() {
681 return Ok(BatchInstallResult {
682 succeeded: vec![],
683 failed: vec![],
684 });
685 }
686
687 require("choco", "Chocolatey is required to install packages")?;
688
689 let mut args = vec!["install", "-y"];
690 args.extend(packages);
691
692 match run("choco", &args) {
693 Ok(_) => Ok(BatchInstallResult {
694 succeeded: packages.iter().map(|s| s.to_string()).collect(),
695 failed: vec![],
696 }),
697 Err(_) => {
698 let mut succeeded = Vec::with_capacity(packages.len());
700 let mut failed = Vec::with_capacity(packages.len());
701 for pkg in packages {
702 match run("choco", &["install", "-y", pkg]) {
703 Ok(_) => succeeded.push(pkg.to_string()),
704 Err(e) => failed.push((pkg.to_string(), format!("{}", e))),
705 }
706 }
707 Ok(BatchInstallResult { succeeded, failed })
708 }
709 }
710 }
711
712 pub fn update(pm: PackageManager, use_sudo: Option<bool>) -> Result<(), InstallError> {
713 match pm {
714 PackageManager::Apt => {
715 let apt = require_any(&["apt-get", "apt"], "apt is required to update packages")?;
717 match use_sudo {
719 Some(flag) => {
720 if flag {
721 require("sudo", "sudo is required to update packages")?;
722 }
723 run_maybe_sudo(flag, apt, &["update"])?;
724 }
725 None => {
726 if let Err(e) = run_maybe_sudo(false, apt, &["update"]) {
728 if has("sudo") {
730 run_maybe_sudo(true, apt, &["update"])?;
731 } else {
732 return Err(e);
733 }
734 }
735 }
736 }
737 }
738 PackageManager::Dnf => { }
739 PackageManager::Yum => { }
740 PackageManager::Zypper => {
741 require("zypper", "zypper is required to update packages")?;
742 match use_sudo {
743 Some(flag) => {
744 if flag {
745 require("sudo", "sudo is required to update packages")?;
746 }
747 run_maybe_sudo(flag, "zypper", &["--non-interactive", "refresh"])?;
748 }
749 None => {
750 if let Err(e) =
751 run_maybe_sudo(false, "zypper", &["--non-interactive", "refresh"])
752 {
753 if has("sudo") {
754 run_maybe_sudo(true, "zypper", &["--non-interactive", "refresh"])?;
755 } else {
756 return Err(e);
757 }
758 }
759 }
760 }
761 }
762 PackageManager::Pacman => {
763 require("pacman", "pacman is required to update packages")?;
764 match use_sudo {
765 Some(flag) => {
766 if flag {
767 require("sudo", "sudo is required to update packages")?;
768 }
769 run_maybe_sudo(flag, "pacman", &["-Sy"])?;
770 }
771 None => {
772 if let Err(e) = run_maybe_sudo(false, "pacman", &["-Sy"]) {
773 if has("sudo") {
774 run_maybe_sudo(true, "pacman", &["-Sy"])?;
775 } else {
776 return Err(e);
777 }
778 }
779 }
780 }
781 }
782 PackageManager::Apk => { }
783 PackageManager::Pkg => {
784 require("pkg", "FreeBSD pkg is required to update packages")?;
785 match use_sudo {
786 Some(flag) => {
787 if flag {
788 require("sudo", "sudo is required to update packages")?;
789 }
790 run_maybe_sudo(flag, "pkg", &["update"])?;
791 }
792 None => {
793 if let Err(e) = run_maybe_sudo(false, "pkg", &["update"]) {
794 if has("sudo") {
795 run_maybe_sudo(true, "pkg", &["update"])?;
796 } else {
797 return Err(e);
798 }
799 }
800 }
801 }
802 }
803 _ => {}
804 }
805 Ok(())
806 }
807
808 pub fn install(
809 pm: PackageManager,
810 pkg: &str,
811 use_sudo: Option<bool>,
812 ) -> Result<(), InstallError> {
813 match pm {
814 PackageManager::Apt => {
815 let apt = require_any(&["apt-get", "apt"], "apt is required to install packages")?;
816 match use_sudo {
817 Some(flag) => {
818 if flag {
819 require("sudo", "sudo is required to install packages")?;
820 }
821 run_maybe_sudo(flag, apt, &["install", "-y", pkg])?;
822 }
823 None => {
824 if let Err(e) = run_maybe_sudo(false, apt, &["install", "-y", pkg]) {
825 if has("sudo") {
826 run_maybe_sudo(true, apt, &["install", "-y", pkg])?;
827 } else {
828 return Err(e);
829 }
830 }
831 }
832 }
833 }
834 PackageManager::Dnf => {
835 require("dnf", "dnf is required to install packages")?;
836 match use_sudo {
837 Some(flag) => {
838 if flag {
839 require("sudo", "sudo is required to install packages")?;
840 }
841 run_maybe_sudo(flag, "dnf", &["install", "-y", pkg])?;
842 }
843 None => {
844 if let Err(e) = run_maybe_sudo(false, "dnf", &["install", "-y", pkg]) {
845 if has("sudo") {
846 run_maybe_sudo(true, "dnf", &["install", "-y", pkg])?;
847 } else {
848 return Err(e);
849 }
850 }
851 }
852 }
853 }
854 PackageManager::Yum => {
855 require("yum", "yum is required to install packages")?;
856 match use_sudo {
857 Some(flag) => {
858 if flag {
859 require("sudo", "sudo is required to install packages")?;
860 }
861 run_maybe_sudo(flag, "yum", &["install", "-y", pkg])?;
862 }
863 None => {
864 if let Err(e) = run_maybe_sudo(false, "yum", &["install", "-y", pkg]) {
865 if has("sudo") {
866 run_maybe_sudo(true, "yum", &["install", "-y", pkg])?;
867 } else {
868 return Err(e);
869 }
870 }
871 }
872 }
873 }
874 PackageManager::Zypper => {
875 require("zypper", "zypper is required to install packages")?;
876 match use_sudo {
877 Some(flag) => {
878 if flag {
879 require("sudo", "sudo is required to install packages")?;
880 }
881 run_maybe_sudo(
882 flag,
883 "zypper",
884 &["--non-interactive", "install", "--no-confirm", pkg],
885 )?;
886 }
887 None => {
888 if let Err(e) = run_maybe_sudo(
889 false,
890 "zypper",
891 &["--non-interactive", "install", "--no-confirm", pkg],
892 ) {
893 if has("sudo") {
894 run_maybe_sudo(
895 true,
896 "zypper",
897 &["--non-interactive", "install", "--no-confirm", pkg],
898 )?;
899 } else {
900 return Err(e);
901 }
902 }
903 }
904 }
905 }
906 PackageManager::Pacman => {
907 require("pacman", "pacman is required to install packages")?;
908 match use_sudo {
909 Some(flag) => {
910 if flag {
911 require("sudo", "sudo is required to install packages")?;
912 }
913 run_maybe_sudo(flag, "pacman", &["--noconfirm", "-S", pkg])?;
914 }
915 None => {
916 if let Err(e) = run_maybe_sudo(false, "pacman", &["--noconfirm", "-S", pkg])
917 {
918 if has("sudo") {
919 run_maybe_sudo(true, "pacman", &["--noconfirm", "-S", pkg])?;
920 } else {
921 return Err(e);
922 }
923 }
924 }
925 }
926 }
927 PackageManager::Apk => {
928 require("apk", "apk is required to install packages")?;
929 match use_sudo {
930 Some(flag) => {
931 if flag {
932 require("sudo", "sudo is required to install packages")?;
933 }
934 run_maybe_sudo(flag, "apk", &["add", pkg])?;
935 }
936 None => {
937 if let Err(e) = run_maybe_sudo(false, "apk", &["add", pkg]) {
938 if has("sudo") {
939 run_maybe_sudo(true, "apk", &["add", pkg])?;
940 } else {
941 return Err(e);
942 }
943 }
944 }
945 }
946 }
947 PackageManager::Brew => {
949 require("brew", "Homebrew is required to install packages")?;
950 run("brew", &["install", pkg])?;
951 }
952 PackageManager::BrewCask => {
953 require("brew", "Homebrew is required to install casks")?;
954 run("brew", &["install", "--cask", pkg])?;
955 }
956 PackageManager::Winget => {
957 require("winget", "Winget is required to install packages")?;
958 run("winget", &["install", "-e", "--id", pkg])?;
959 }
960 PackageManager::Choco => {
961 require("choco", "Chocolatey is required to install packages")?;
962 run("choco", &["install", "-y", pkg])?;
963 }
964 PackageManager::Pkg => {
965 require("pkg", "FreeBSD pkg is required to install packages")?;
966 match use_sudo {
967 Some(flag) => {
968 if flag {
969 require("sudo", "sudo is required to install packages")?;
970 }
971 run_maybe_sudo(flag, "pkg", &["install", "-y", pkg])?;
972 }
973 None => {
974 if let Err(e) = run_maybe_sudo(false, "pkg", &["install", "-y", pkg]) {
975 if has("sudo") {
976 run_maybe_sudo(true, "pkg", &["install", "-y", pkg])?;
977 } else {
978 return Err(e);
979 }
980 }
981 }
982 }
983 }
984 };
985 Ok(())
986 }
987}