v0.2.3 : execution shell des commandes avec pipe (rustup, bun, uv) + rafraichissement du PATH + python3 satisfait python

This commit is contained in:
2026-08-15 16:42:47 -04:00
parent e513dd485d
commit 2be5f8e8b2
5 changed files with 119 additions and 32 deletions
+95 -1
View File
@@ -5,6 +5,10 @@
//! already present (scoop/winget on Windows, apt/dnf/pacman/apk on Linux,
//! brew on macOS) — and proposes the exact command to install it.
use crate::runner::Runner;
use anyhow::Result;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Family {
Windows,
@@ -130,6 +134,66 @@ pub fn install_commands(tool: &str, os: &OsInfo) -> Vec<String> {
out
}
/// Run an installer command line. Commands containing shell syntax
/// (pipes, redirections, ...) are executed through the shell; plain
/// commands are spawned directly.
pub fn run_install_command(runner: &dyn Runner, cmd: &str) -> Result<i32> {
let needs_shell = cmd.contains('|')
|| cmd.contains('>')
|| cmd.contains('<')
|| cmd.contains('&')
|| cmd.contains(';');
let (prog, args) = if needs_shell {
#[cfg(windows)]
{
("cmd".to_string(), vec!["/C".to_string(), cmd.to_string()])
}
#[cfg(not(windows))]
{
("sh".to_string(), vec!["-c".to_string(), cmd.to_string()])
}
} else {
let tokens = shell_words::split(cmd)
.unwrap_or_else(|_| cmd.split_whitespace().map(String::from).collect());
if tokens.is_empty() {
return Ok(0);
}
(tokens[0].clone(), tokens[1..].to_vec())
};
let empty = std::collections::BTreeMap::new();
let out = runner.run(&prog, &args, &empty, None, false)?;
Ok(out.code)
}
/// After installers that modify ~/.profile or ~/.zshrc (rustup, bun, uv),
/// make the newly installed tools visible to the current process so the
/// dependency re-check can find them.
pub fn refresh_path() {
let mut extra: Vec<PathBuf> = Vec::new();
if let Some(home) = crate::config::home_dir() {
extra.push(home.join(".cargo").join("bin"));
extra.push(home.join(".bun").join("bin"));
extra.push(home.join(".local").join("bin"));
}
let mut parts: Vec<PathBuf> = extra.into_iter().filter(|p| p.is_dir()).collect();
if let Some(current) = std::env::var_os("PATH") {
parts.extend(std::env::split_paths(&current));
}
if let Ok(joined) = std::env::join_paths(parts) {
std::env::set_var("PATH", joined);
}
}
/// Is a tool considered present? Python is satisfied by python3 too
/// (Debian ships python3 without a python symlink).
pub fn tool_is_present(tool: &str, runner: &dyn Runner) -> bool {
if tool == "python" {
runner.which("python3").is_some() || runner.which("python").is_some()
} else {
runner.which(tool).is_some()
}
}
fn scoop_command(tool: &str) -> Option<String> {
let bucket = match tool {
"git" => "git",
@@ -208,7 +272,7 @@ fn linux_command(tool: &str, pkg: &str) -> Option<String> {
("pip", "apk") => "sudo apk add py3-pip",
("pip", "zypper") => "sudo zypper install -y python3-pip",
("cargo" | "rust", "apt") => {
"curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
"curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y"
}
("cargo" | "rust", "dnf") => "sudo dnf install -y rust cargo",
("cargo" | "rust", "pacman") => "sudo pacman -S --noconfirm rust",
@@ -278,6 +342,36 @@ mod tests {
assert!(brew_command("docker").unwrap().contains("--cask"));
}
#[test]
fn pipe_commands_run_through_shell() {
let runner = crate::runner::MockRunner::new();
let code = run_install_command(&runner, "curl -sSf https://x | sh").unwrap();
assert_eq!(code, 0);
let calls = runner.snapshot();
assert_eq!(calls.len(), 1);
if cfg!(windows) {
assert!(calls[0].starts_with("cmd /C "));
} else {
assert!(calls[0].starts_with("sh -c "));
}
}
#[test]
fn plain_commands_spawn_directly() {
let runner = crate::runner::MockRunner::new();
run_install_command(&runner, "apt install -y git").unwrap();
let calls = runner.snapshot();
assert!(calls[0].starts_with("apt install -y git"));
}
#[test]
fn python3_satisfies_python() {
let runner = crate::runner::MockRunner::new().with_found("python3");
assert!(tool_is_present("python", &runner));
let runner = crate::runner::MockRunner::new();
assert!(!tool_is_present("python", &runner));
}
#[test]
fn os_aware_selection() {
// Windows without scoop falls back to winget.