Files
honey-biscuit-workshop/workshop-baker/src/engine/pm/pacman.rs
T
Catty Steve 246c2b22f1 feat(bake): Add streaming execution output and Repology endpoint config
- 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
2026-04-04 16:59:39 +08:00

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());
}
}