246c2b22f1
- Implement async stdout/stderr streaming with event channel - Add Repology endpoint configuration in dependencies config - Refactor PackageManager trait to return Vec<Command> - Add bootstrap configuration example - Remove temp/privdrop.rs
620 lines
20 KiB
Rust
620 lines
20 KiB
Rust
use super::PackageManager;
|
|
use os_info::{Info, Type};
|
|
use serde::Deserialize;
|
|
use serde_yaml::Value;
|
|
use tokio::process::Command;
|
|
|
|
#[derive(Clone)]
|
|
pub struct Pacman;
|
|
|
|
#[derive(Deserialize)]
|
|
struct PacmanRepo {
|
|
name: String,
|
|
server: String,
|
|
cacheserver: Option<String>,
|
|
siglevel: Option<SigLevel>,
|
|
keyid: Option<String>,
|
|
keypackage: Option<String>,
|
|
}
|
|
|
|
struct SigLevel {
|
|
raw: String,
|
|
}
|
|
|
|
impl SigLevel {
|
|
pub fn new(raw: String) -> Self {
|
|
for token in raw.split_whitespace() {
|
|
if !VALID_SIGLEVEL_TOKENS.contains(&token) {
|
|
log::warn!("Invalid siglevel token: {}", token);
|
|
}
|
|
}
|
|
Self { raw }
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for SigLevel {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{}", self.raw)
|
|
}
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for SigLevel {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
let s = String::deserialize(deserializer)?;
|
|
Ok(SigLevel::new(s))
|
|
}
|
|
}
|
|
|
|
const VALID_SIGLEVEL_TOKENS: &[&str] = &[
|
|
// Token
|
|
"Never",
|
|
"Optional",
|
|
"Required",
|
|
"TrustedOnly",
|
|
"TrustAll",
|
|
// Package prefix
|
|
"PackageNever",
|
|
"PackageOptional",
|
|
"PackageRequired",
|
|
"PackageTrustedOnly",
|
|
"PackageTrustAll",
|
|
// Database prefix
|
|
"DatabaseNever",
|
|
"DatabaseOptional",
|
|
"DatabaseRequired",
|
|
"DatabaseTrustedOnly",
|
|
"DatabaseTrustAll",
|
|
];
|
|
|
|
impl PackageManager for Pacman {
|
|
fn name(&self) -> &'static str {
|
|
"pacman"
|
|
}
|
|
|
|
fn adopt(&self, distro: &Info) -> bool {
|
|
matches!(
|
|
distro.os_type(),
|
|
Type::Arch
|
|
| Type::Manjaro
|
|
| Type::EndeavourOS
|
|
| Type::Garuda
|
|
| Type::CachyOS
|
|
| Type::Artix
|
|
| Type::Mabox
|
|
| Type::Nobara
|
|
| Type::InstantOS
|
|
)
|
|
}
|
|
|
|
fn binary(&self) -> &'static str {
|
|
"pacman"
|
|
}
|
|
|
|
fn update(&self) -> Vec<Command> {
|
|
let mut cmd = Command::new("pacman");
|
|
cmd.arg("-Sy");
|
|
vec![cmd]
|
|
}
|
|
|
|
fn install(&self, package: &[String]) -> Vec<Command> {
|
|
let mut cmd = Command::new("pacman");
|
|
cmd.arg("-S");
|
|
cmd.arg("--noconfirm");
|
|
cmd.arg("--needed");
|
|
cmd.args(package);
|
|
vec![cmd]
|
|
}
|
|
|
|
fn change_mirror(&self, repo: &str, _osinfo: Option<&Info>) -> Vec<Command> {
|
|
let mirrorlist_path = "/etc/pacman.d/mirrorlist";
|
|
|
|
let content = format!(
|
|
"# Generated by WSExecutor-PM\n\
|
|
# Date: $(date)\n\
|
|
# Original content overwritten.\n\
|
|
\n\
|
|
Server = {}\n",
|
|
repo
|
|
);
|
|
|
|
let mut cmd = Command::new("sh");
|
|
cmd.arg("-c").arg(format!(
|
|
"cat > {} << 'PACMAN_EOF'\n{}\nPACMAN_EOF",
|
|
mirrorlist_path, content
|
|
));
|
|
|
|
let mut keyring_cmd = Command::new("sh");
|
|
keyring_cmd.arg("-c").arg("pacman-key --init");
|
|
vec![cmd, keyring_cmd]
|
|
}
|
|
|
|
fn add_repository(&self, repo: &Value, _osinfo: Option<&Info>) -> Vec<Command> {
|
|
let repos: Vec<PacmanRepo> = match serde_yaml::from_value(repo.clone()) {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
log::error!("Failed to parse pacman repository: {}", e);
|
|
return Vec::new();
|
|
}
|
|
};
|
|
let pacman_conf = "/etc/pacman.conf";
|
|
let mut content = String::with_capacity(512);
|
|
content.push_str("# Modified by HBW baker\n");
|
|
let mut cmds = vec![];
|
|
for repo in &repos {
|
|
// Install keyring
|
|
if repo.keyid.is_some() && repo.keypackage.is_some() {
|
|
log::error!(
|
|
"Both keyid and keypackage are set for repository {}",
|
|
repo.name
|
|
);
|
|
log::warn!("Ignoring...");
|
|
continue;
|
|
}
|
|
if let Some(keyid) = &repo.keyid {
|
|
let mut cmd = Command::new("sh");
|
|
cmd.arg("-c")
|
|
.arg(format!("pacman-key --recv-keys {}", keyid));
|
|
cmds.push(cmd);
|
|
let mut cmd = Command::new("sh");
|
|
cmd.arg("-c")
|
|
.arg(format!("pacman-key --lsign-key {}", keyid));
|
|
cmds.push(cmd);
|
|
} else if let Some(keypackage) = &repo.keypackage {
|
|
let mut cmd = Command::new("sh");
|
|
cmd.arg("-c")
|
|
.arg(format!("pacman -Sy {} --needed --noconfirm", keypackage));
|
|
cmds.push(cmd);
|
|
}
|
|
|
|
// Configure repository
|
|
let mut block = format!("[{}]\n", &repo.name);
|
|
block.push_str(&format!("Server = {}\n", &repo.server));
|
|
|
|
if let Some(cacheserver) = &repo.cacheserver {
|
|
block.push_str(&format!("CacheServer = {}\n", cacheserver));
|
|
}
|
|
if let Some(siglevel) = &repo.siglevel {
|
|
block.push_str(&format!("SigLevel = {}\n", siglevel));
|
|
}
|
|
content.push_str(&block);
|
|
}
|
|
|
|
let mut cmd = Command::new("sh");
|
|
cmd.arg("-c").arg(format!(
|
|
"cat >> {} << 'PACMAN_EOF'\n{}\nPACMAN_EOF",
|
|
pacman_conf, content
|
|
));
|
|
cmds.insert(0, cmd);
|
|
|
|
cmds
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_name() {
|
|
let pacman = Pacman;
|
|
assert_eq!(pacman.name(), "pacman");
|
|
}
|
|
|
|
#[test]
|
|
fn test_binary() {
|
|
let pacman = Pacman;
|
|
assert_eq!(pacman.binary(), "pacman");
|
|
}
|
|
|
|
#[test]
|
|
fn test_adopt_true_for_arch_based_distros() {
|
|
let pacman = Pacman;
|
|
let arch_distros = [
|
|
Type::Arch,
|
|
Type::Manjaro,
|
|
Type::EndeavourOS,
|
|
Type::Garuda,
|
|
Type::CachyOS,
|
|
Type::Artix,
|
|
Type::Mabox,
|
|
Type::Nobara,
|
|
Type::InstantOS,
|
|
];
|
|
for distro in arch_distros {
|
|
let info = Info::with_type(distro);
|
|
assert!(
|
|
pacman.adopt(&info),
|
|
"Pacman should adopt distro: {:?}",
|
|
distro
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_adopt_false_for_non_arch_distros() {
|
|
let pacman = Pacman;
|
|
let non_arch_distros = [
|
|
Type::Ubuntu,
|
|
Type::Debian,
|
|
Type::Fedora,
|
|
Type::CentOS,
|
|
Type::RedHatEnterprise,
|
|
Type::openSUSE,
|
|
Type::Gentoo,
|
|
Type::FreeBSD,
|
|
Type::Macos,
|
|
Type::Windows,
|
|
];
|
|
for distro in non_arch_distros {
|
|
let info = Info::with_type(distro);
|
|
assert!(
|
|
!pacman.adopt(&info),
|
|
"Pacman should NOT adopt distro: {:?}",
|
|
distro
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_update_command_contents() {
|
|
let cmds = Pacman.update();
|
|
let cmd = cmds[0].as_std();
|
|
assert_eq!(cmd.get_program().to_string_lossy(), "pacman");
|
|
let args: Vec<String> = cmd
|
|
.get_args()
|
|
.map(|a| a.to_string_lossy().into_owned())
|
|
.collect();
|
|
assert_eq!(args, vec!["-Sy"]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_install_command_contents() {
|
|
let cmds = Pacman.install(&["curl".to_string(), "wget".to_string()]);
|
|
let cmd = cmds[0].as_std();
|
|
assert_eq!(cmd.get_program().to_string_lossy(), "pacman");
|
|
let args: Vec<String> = cmd
|
|
.get_args()
|
|
.map(|a| a.to_string_lossy().into_owned())
|
|
.collect();
|
|
assert_eq!(args, vec!["-S", "--noconfirm", "--needed", "curl", "wget"]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_install_single_package() {
|
|
let cmds = Pacman.install(&["package".to_string()]);
|
|
let cmd = cmds[0].as_std();
|
|
let args: Vec<String> = cmd
|
|
.get_args()
|
|
.map(|a| a.to_string_lossy().into_owned())
|
|
.collect();
|
|
assert_eq!(args, vec!["-S", "--noconfirm", "--needed", "package"]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_siglevel_valid_tokens() {
|
|
let siglevel = SigLevel::new("Never".to_string());
|
|
assert_eq!(siglevel.to_string(), "Never");
|
|
|
|
let siglevel = SigLevel::new("Optional Required".to_string());
|
|
assert_eq!(siglevel.to_string(), "Optional Required");
|
|
|
|
let siglevel = SigLevel::new("PackageOptional DatabaseRequired".to_string());
|
|
assert_eq!(siglevel.to_string(), "PackageOptional DatabaseRequired");
|
|
}
|
|
|
|
#[test]
|
|
fn test_siglevel_with_invalid_tokens() {
|
|
// Invalid tokens are logged but still accepted
|
|
let siglevel = SigLevel::new("Never InvalidToken".to_string());
|
|
assert_eq!(siglevel.to_string(), "Never InvalidToken");
|
|
}
|
|
|
|
#[test]
|
|
fn test_siglevel_deserialize() {
|
|
let yaml = "Never";
|
|
let siglevel: SigLevel = serde_yaml::from_str(yaml).unwrap();
|
|
assert_eq!(siglevel.to_string(), "Never");
|
|
|
|
let yaml = "PackageOptional DatabaseRequired";
|
|
let siglevel: SigLevel = serde_yaml::from_str(yaml).unwrap();
|
|
assert_eq!(siglevel.to_string(), "PackageOptional DatabaseRequired");
|
|
}
|
|
|
|
#[test]
|
|
fn test_siglevel_all_valid_tokens() {
|
|
let all_tokens = VALID_SIGLEVEL_TOKENS.join(" ");
|
|
let siglevel = SigLevel::new(all_tokens.clone());
|
|
assert_eq!(siglevel.to_string(), all_tokens);
|
|
}
|
|
|
|
#[test]
|
|
fn test_change_mirror_command() {
|
|
let cmds = Pacman.change_mirror("https://mirror.example.com/archlinux", None);
|
|
let cmd = cmds[0].as_std();
|
|
assert_eq!(cmd.get_program().to_string_lossy(), "sh");
|
|
let args: Vec<String> = cmd
|
|
.get_args()
|
|
.map(|a| a.to_string_lossy().into_owned())
|
|
.collect();
|
|
let full_arg = args.join(" ");
|
|
assert!(full_arg.contains("cat > /etc/pacman.d/mirrorlist"));
|
|
assert!(full_arg.contains("https://mirror.example.com/archlinux"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_add_repository_single_repo() {
|
|
let repo_yaml = serde_yaml::from_value(serde_yaml::Value::Sequence(vec![
|
|
serde_yaml::Value::Mapping(
|
|
[
|
|
(
|
|
serde_yaml::Value::String("name".to_string()),
|
|
serde_yaml::Value::String("customrepo".to_string()),
|
|
),
|
|
(
|
|
serde_yaml::Value::String("server".to_string()),
|
|
serde_yaml::Value::String("https://repo.example.com".to_string()),
|
|
),
|
|
]
|
|
.into_iter()
|
|
.collect(),
|
|
),
|
|
]))
|
|
.unwrap();
|
|
|
|
let cmds = Pacman.add_repository(&repo_yaml, None);
|
|
// Should have at least one command (the one to append to pacman.conf)
|
|
assert!(!cmds.is_empty());
|
|
let last_cmd = cmds.last().unwrap().as_std();
|
|
let args: Vec<String> = last_cmd
|
|
.get_args()
|
|
.map(|a| a.to_string_lossy().into_owned())
|
|
.collect();
|
|
let full_arg = args.join(" ");
|
|
assert!(full_arg.contains("cat >> /etc/pacman.conf"));
|
|
assert!(full_arg.contains("[customrepo]"));
|
|
assert!(full_arg.contains("https://repo.example.com"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_add_repository_with_siglevel() {
|
|
let repo_yaml = serde_yaml::from_value(serde_yaml::Value::Sequence(vec![
|
|
serde_yaml::Value::Mapping(
|
|
[
|
|
(
|
|
serde_yaml::Value::String("name".to_string()),
|
|
serde_yaml::Value::String("customrepo".to_string()),
|
|
),
|
|
(
|
|
serde_yaml::Value::String("server".to_string()),
|
|
serde_yaml::Value::String("https://repo.example.com".to_string()),
|
|
),
|
|
(
|
|
serde_yaml::Value::String("siglevel".to_string()),
|
|
serde_yaml::Value::String("PackageOptional".to_string()),
|
|
),
|
|
]
|
|
.into_iter()
|
|
.collect(),
|
|
),
|
|
]))
|
|
.unwrap();
|
|
|
|
let cmds = Pacman.add_repository(&repo_yaml, None);
|
|
let last_cmd = cmds.last().unwrap().as_std();
|
|
let args: Vec<String> = last_cmd
|
|
.get_args()
|
|
.map(|a| a.to_string_lossy().into_owned())
|
|
.collect();
|
|
let full_arg = args.join(" ");
|
|
assert!(full_arg.contains("SigLevel = PackageOptional"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_add_repository_with_keyid() {
|
|
let repo_yaml = serde_yaml::from_value(serde_yaml::Value::Sequence(vec![
|
|
serde_yaml::Value::Mapping(
|
|
[
|
|
(
|
|
serde_yaml::Value::String("name".to_string()),
|
|
serde_yaml::Value::String("customrepo".to_string()),
|
|
),
|
|
(
|
|
serde_yaml::Value::String("server".to_string()),
|
|
serde_yaml::Value::String("https://repo.example.com".to_string()),
|
|
),
|
|
(
|
|
serde_yaml::Value::String("keyid".to_string()),
|
|
serde_yaml::Value::String("ABCDEF123456".to_string()),
|
|
),
|
|
]
|
|
.into_iter()
|
|
.collect(),
|
|
),
|
|
]))
|
|
.unwrap();
|
|
|
|
let cmds = Pacman.add_repository(&repo_yaml, None);
|
|
// Should have 3 commands: recv-keys, lsign-key, and append to conf
|
|
assert_eq!(cmds.len(), 3);
|
|
|
|
let recv_cmd = cmds[1].as_std();
|
|
let recv_args: Vec<String> = recv_cmd
|
|
.get_args()
|
|
.map(|a| a.to_string_lossy().into_owned())
|
|
.collect();
|
|
|
|
assert!(recv_args
|
|
.join(" ")
|
|
.contains("pacman-key --recv-keys ABCDEF123456"));
|
|
|
|
let lsign_cmd = cmds[2].as_std();
|
|
let lsign_args: Vec<String> = lsign_cmd
|
|
.get_args()
|
|
.map(|a| a.to_string_lossy().into_owned())
|
|
.collect();
|
|
assert!(lsign_args
|
|
.join(" ")
|
|
.contains("pacman-key --lsign-key ABCDEF123456"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_add_repository_with_keypackage() {
|
|
let repo_yaml = serde_yaml::from_value(serde_yaml::Value::Sequence(vec![
|
|
serde_yaml::Value::Mapping(
|
|
[
|
|
(
|
|
serde_yaml::Value::String("name".to_string()),
|
|
serde_yaml::Value::String("customrepo".to_string()),
|
|
),
|
|
(
|
|
serde_yaml::Value::String("server".to_string()),
|
|
serde_yaml::Value::String("https://repo.example.com".to_string()),
|
|
),
|
|
(
|
|
serde_yaml::Value::String("keypackage".to_string()),
|
|
serde_yaml::Value::String("gnupg".to_string()),
|
|
),
|
|
]
|
|
.into_iter()
|
|
.collect(),
|
|
),
|
|
]))
|
|
.unwrap();
|
|
|
|
let cmds = Pacman.add_repository(&repo_yaml, None);
|
|
// Should have 2 commands: install keypackage and append to conf
|
|
assert_eq!(cmds.len(), 2);
|
|
|
|
let install_cmd = cmds[1].as_std();
|
|
let install_args: Vec<String> = install_cmd
|
|
.get_args()
|
|
.map(|a| a.to_string_lossy().into_owned())
|
|
.collect();
|
|
assert!(install_args
|
|
.join(" ")
|
|
.contains("pacman -Sy gnupg --needed --noconfirm"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_add_repository_with_cacheserver() {
|
|
let repo_yaml = serde_yaml::from_value(serde_yaml::Value::Sequence(vec![
|
|
serde_yaml::Value::Mapping(
|
|
[
|
|
(
|
|
serde_yaml::Value::String("name".to_string()),
|
|
serde_yaml::Value::String("customrepo".to_string()),
|
|
),
|
|
(
|
|
serde_yaml::Value::String("server".to_string()),
|
|
serde_yaml::Value::String("https://repo.example.com".to_string()),
|
|
),
|
|
(
|
|
serde_yaml::Value::String("cacheserver".to_string()),
|
|
serde_yaml::Value::String("https://cache.example.com".to_string()),
|
|
),
|
|
]
|
|
.into_iter()
|
|
.collect(),
|
|
),
|
|
]))
|
|
.unwrap();
|
|
|
|
let cmds = Pacman.add_repository(&repo_yaml, None);
|
|
let last_cmd = cmds.last().unwrap().as_std();
|
|
let args: Vec<String> = last_cmd
|
|
.get_args()
|
|
.map(|a| a.to_string_lossy().into_owned())
|
|
.collect();
|
|
let full_arg = args.join(" ");
|
|
assert!(full_arg.contains("CacheServer = https://cache.example.com"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_add_repository_multiple_repos() {
|
|
let repos_yaml = serde_yaml::from_value(serde_yaml::Value::Sequence(vec![
|
|
serde_yaml::Value::Mapping(
|
|
[
|
|
(
|
|
serde_yaml::Value::String("name".to_string()),
|
|
serde_yaml::Value::String("repo1".to_string()),
|
|
),
|
|
(
|
|
serde_yaml::Value::String("server".to_string()),
|
|
serde_yaml::Value::String("https://repo1.example.com".to_string()),
|
|
),
|
|
]
|
|
.into_iter()
|
|
.collect(),
|
|
),
|
|
serde_yaml::Value::Mapping(
|
|
[
|
|
(
|
|
serde_yaml::Value::String("name".to_string()),
|
|
serde_yaml::Value::String("repo2".to_string()),
|
|
),
|
|
(
|
|
serde_yaml::Value::String("server".to_string()),
|
|
serde_yaml::Value::String("https://repo2.example.com".to_string()),
|
|
),
|
|
]
|
|
.into_iter()
|
|
.collect(),
|
|
),
|
|
]))
|
|
.unwrap();
|
|
|
|
let cmds = Pacman.add_repository(&repos_yaml, None);
|
|
let last_cmd = cmds.last().unwrap().as_std();
|
|
let args: Vec<String> = last_cmd
|
|
.get_args()
|
|
.map(|a| a.to_string_lossy().into_owned())
|
|
.collect();
|
|
let full_arg = args.join(" ");
|
|
assert!(full_arg.contains("[repo1]"));
|
|
assert!(full_arg.contains("[repo2]"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_add_repository_both_keyid_and_keypackage_logs_error() {
|
|
let repo_yaml = serde_yaml::from_value(serde_yaml::Value::Sequence(vec![
|
|
serde_yaml::Value::Mapping(
|
|
[
|
|
(
|
|
serde_yaml::Value::String("name".to_string()),
|
|
serde_yaml::Value::String("customrepo".to_string()),
|
|
),
|
|
(
|
|
serde_yaml::Value::String("server".to_string()),
|
|
serde_yaml::Value::String("https://repo.example.com".to_string()),
|
|
),
|
|
(
|
|
serde_yaml::Value::String("keyid".to_string()),
|
|
serde_yaml::Value::String("ABCDEF123456".to_string()),
|
|
),
|
|
(
|
|
serde_yaml::Value::String("keypackage".to_string()),
|
|
serde_yaml::Value::String("gnupg".to_string()),
|
|
),
|
|
]
|
|
.into_iter()
|
|
.collect(),
|
|
),
|
|
]))
|
|
.unwrap();
|
|
|
|
let cmds = Pacman.add_repository(&repo_yaml, None);
|
|
// Should have no key-related commands when both are set (error case)
|
|
assert!(cmds.is_empty() || cmds.len() == 1); // Only the conf append cmd if any
|
|
}
|
|
|
|
#[test]
|
|
fn test_add_repository_invalid_yaml_returns_empty() {
|
|
let invalid_yaml = serde_yaml::Value::String("invalid: [yaml".to_string());
|
|
let cmds = Pacman.add_repository(&invalid_yaml, None);
|
|
assert!(cmds.is_empty());
|
|
}
|
|
}
|