pacsea/install/
command.rs1use crate::state::{PackageItem, Source};
4
5use super::utils::{shell_single_quote, validate_package_names};
6
7#[must_use]
19pub const fn aur_install_helper_flags(reinstall: bool) -> &'static str {
20 if reinstall {
21 "-S --aur --noconfirm"
22 } else {
23 "-S --aur --needed --noconfirm"
24 }
25}
26
27#[must_use]
40pub fn aur_install_body(flags: &str, n: &str) -> String {
41 format!(
42 "(if command -v paru >/dev/null 2>&1; then \
43 paru {flags} {n}; \
44 elif command -v yay >/dev/null 2>&1; then \
45 yay {flags} {n}; \
46 else \
47 echo 'No AUR helper (paru/yay) found.'; \
48 fi)"
49 )
50}
51
52pub fn build_install_command(
70 item: &PackageItem,
71 password: Option<&str>,
72 dry_run: bool,
73) -> Result<(String, bool), String> {
74 validate_package_names(
75 std::slice::from_ref(&item.name),
76 "install command construction",
77 )?;
78 let quoted_name = shell_single_quote(&item.name);
79 match &item.source {
80 Source::Official { .. } => {
81 let tool = crate::logic::privilege::active_tool()?;
82 let reinstall = crate::index::is_installed(&item.name);
83 let base_cmd = if reinstall {
84 format!("pacman -S --noconfirm {quoted_name}")
85 } else {
86 format!("pacman -S --needed --noconfirm {quoted_name}")
87 };
88 let hold_tail = "; echo; echo 'Finished.'; echo 'Press any key to close...'; read -rn1 -s _ || (echo; echo 'Press Ctrl+C to close'; sleep infinity)";
89 if dry_run {
90 let cmd = format!(
91 "{}{hold_tail}",
92 crate::logic::privilege::build_privilege_command(tool, &base_cmd)
93 );
94 let quoted = shell_single_quote(&cmd);
95 let bash = format!("echo DRY RUN: {quoted}");
96 return Ok((bash, true));
97 }
98 let pass = password.unwrap_or("");
99 if pass.is_empty() {
100 let bash = format!(
101 "{}{hold_tail}",
102 crate::logic::privilege::build_privilege_command(tool, &base_cmd)
103 );
104 Ok((bash, true))
105 } else {
106 let piped = crate::logic::privilege::build_password_pipe(tool, pass, &base_cmd);
107 let priv_cmd = piped.unwrap_or_else(|| {
108 crate::logic::privilege::build_privilege_command(tool, &base_cmd)
109 });
110 let bash = format!("{priv_cmd}{hold_tail}");
111 Ok((bash, true))
112 }
113 }
114 Source::Aur => {
115 let hold_tail = "; echo; echo 'Press any key to close...'; read -rn1 -s _ || (echo; echo 'Press Ctrl+C to close'; sleep infinity)";
116 let reinstall = crate::index::is_installed(&item.name);
117 let flags = aur_install_helper_flags(reinstall);
118 let aur_cmd = if dry_run {
119 let cmd =
120 format!("paru {flags} {quoted_name} || yay {flags} {quoted_name}{hold_tail}");
121 let quoted = shell_single_quote(&cmd);
122 format!("echo DRY RUN: {quoted}")
123 } else {
124 format!(
125 "{body}{hold}",
126 body = aur_install_body(flags, "ed_name),
127 hold = hold_tail
128 )
129 };
130 Ok((aur_cmd, false))
131 }
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn install_build_install_command_official_variants() {
156 let tool = crate::logic::privilege::active_tool().expect("privilege tool");
157 let bin = tool.binary_name();
158
159 let pkg = PackageItem {
160 name: "ripgrep".into(),
161 version: "14".into(),
162 description: String::new(),
163 source: Source::Official {
164 repo: "extra".into(),
165 arch: "x86_64".into(),
166 },
167 popularity: None,
168 out_of_date: None,
169 orphaned: false,
170 };
171
172 let (cmd1, uses_sudo1) = build_install_command(&pkg, None, false).expect("build");
173 assert!(uses_sudo1);
174 let quoted_name = crate::install::shell_single_quote("ripgrep");
175 assert!(
176 cmd1.contains(&format!(
177 "{bin} pacman -S --needed --noconfirm {quoted_name}"
178 )),
179 "expected quoted package name in: {cmd1}"
180 );
181 assert!(cmd1.contains("Press any key to close"));
182
183 let (cmd2, uses_sudo2) = build_install_command(&pkg, Some("pa's"), false).expect("build");
184 assert!(uses_sudo2);
185 if tool.capabilities().supports_stdin_password {
186 assert!(
187 cmd2.contains(&format!(
188 "{bin} -S pacman -S --needed --noconfirm {quoted_name}"
189 )),
190 "expected quoted package name in password pipe command: {cmd2}"
191 );
192 } else {
193 assert!(
194 cmd2.contains(&format!(
195 "{bin} pacman -S --needed --noconfirm {quoted_name}"
196 )),
197 "doas fallback should use plain command: {cmd2}"
198 );
199 }
200
201 let (cmd3, uses_sudo3) = build_install_command(&pkg, None, true).expect("build");
202 assert!(uses_sudo3);
203 assert!(cmd3.starts_with("echo DRY RUN: '"));
204 assert!(
205 cmd3.contains(&format!("{bin} pacman -S --needed --noconfirm"))
206 && cmd3.contains("ripgrep"),
207 "expected dry-run output to include command and package name: {cmd3}"
208 );
209 }
210
211 #[test]
212 fn install_build_install_command_aur_variants() {
224 let pkg = PackageItem {
225 name: "yay-bin".into(),
226 version: "1".into(),
227 description: String::new(),
228 source: Source::Aur,
229 popularity: None,
230 out_of_date: None,
231 orphaned: false,
232 };
233
234 let (cmd1, uses_sudo1) = build_install_command(&pkg, None, false).expect("build");
235 assert!(!uses_sudo1);
236 assert!(cmd1.contains("command -v paru"));
237 assert!(cmd1.contains("paru -S --aur --needed --noconfirm 'yay-bin'"));
238 assert!(cmd1.contains("yay -S --aur --needed --noconfirm 'yay-bin'"));
239 assert!(cmd1.contains("elif command -v yay"));
240 assert!(cmd1.contains("No AUR helper"));
241 assert!(cmd1.contains("Press any key to close"));
242
243 let (cmd2, uses_sudo2) = build_install_command(&pkg, None, true).expect("build");
244 assert!(!uses_sudo2);
245 assert!(cmd2.starts_with("echo DRY RUN: '"));
247 assert!(cmd2.contains("paru -S --aur --needed --noconfirm"));
248 assert!(cmd2.contains("yay-bin"));
249 }
250}