Files
honey-biscuit-workshop/workshop-engine/src/pm/dnf.rs
T
Catty Steve cf317b55c6
CI / ${{ matrix.crate }} (workshop-engine) (push) Failing after 28m8s
CI / ${{ matrix.crate }} (workshop-baker) (push) Failing after 33m39s
feat(pm): add apk/dnf implementations and multi-distro test
infrastructure
2026-04-27 23:01:43 +08:00

276 lines
8.7 KiB
Rust

use crate::pm::PackageManager;
use os_info::Info;
use serde::Deserialize;
use serde_yaml::Value;
use tokio::process::Command;
#[derive(Clone)]
pub struct Dnf;
/// Repository configuration for dnf.
#[derive(Deserialize, Clone)]
struct DnfRepo {
/// Repository name (used as section header and filename).
name: String,
/// Base URL for packages.
baseurl: String,
/// Whether the repository is enabled (default: true).
#[serde(default = "default_enabled")]
enabled: bool,
/// GPG key URL.
#[serde(default)]
gpgkey: Option<String>,
/// Additional metadata (name, gpgcheck, etc.).
#[serde(default)]
description: Option<String>,
}
fn default_enabled() -> bool {
true
}
impl PackageManager for Dnf {
fn name(&self) -> &'static str {
"dnf"
}
fn binary(&self) -> &'static str {
"dnf"
}
fn update(&self) -> Vec<Command> {
let mut cmd = Command::new("dnf");
cmd.arg("makecache");
vec![cmd]
}
fn install(&self, package: &[String]) -> Vec<Command> {
let mut cmd = Command::new("dnf");
cmd.arg("install");
cmd.arg("-y");
cmd.args(package);
vec![cmd]
}
/// Changes the primary dnf mirror by replacing baseurl in the main repo file.
///
/// Uses sed to replace `baseurl=` lines in `/etc/yum.repos.d/` with the
/// user-specified mirror URL.
fn change_mirror(&self, repo: &str, _osinfo: Option<&Info>) -> Vec<Command> {
let script = format!(
"sed -i 's|^baseurl=[^ ].*|baseurl={}|' /etc/yum.repos.d/*.repo",
repo
);
let mut cmd = Command::new("sh");
cmd.arg("-c").arg(script);
vec![cmd]
}
/// Adds a custom dnf repository by creating a .repo file in /etc/yum.repos.d/.
fn add_repository(&self, repo: &Value, _osinfo: Option<&Info>) -> Vec<Command> {
let repos: Vec<DnfRepo> = match serde_yaml::from_value(repo.clone()) {
Ok(r) => r,
Err(e) => {
log::error!("Failed to parse dnf repository: {}", e);
return Vec::new();
}
};
let mut cmds: Vec<Command> = vec![];
for repo_cfg in &repos {
// Build the .repo file content
let mut content = format!(
"[{}]\nname={}\nbaseurl={}\nenabled={}\n",
repo_cfg.name,
repo_cfg
.description
.clone()
.unwrap_or_else(|| repo_cfg.name.clone()),
repo_cfg.baseurl,
if repo_cfg.enabled { "1" } else { "0" },
);
if let Some(gpgkey) = &repo_cfg.gpgkey {
content.push_str(&format!("gpgcheck=1\ngpgkey={}\n", gpgkey));
// Download and import the GPG key
let mut key_cmd = Command::new("sh");
key_cmd.arg("-c").arg(format!(
"rpm --import '{}'",
gpgkey
));
cmds.push(key_cmd);
} else {
content.push_str("gpgcheck=0\n");
}
// Write the .repo file
let repo_path = format!("/etc/yum.repos.d/{}.repo", sanitize_repo_name(&repo_cfg.name));
let mut cmd = Command::new("sh");
cmd.arg("-c").arg(format!(
"cat > '{}' << 'DNF_EOF'\n{}DNF_EOF",
repo_path, content
));
cmds.push(cmd);
}
cmds
}
}
/// Sanitizes a string to a safe filename component.
fn sanitize_repo_name(name: &str) -> String {
name.replace(|c: char| !c.is_alphanumeric() && c != '-' && c != '_', "_")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_name() {
assert_eq!(Dnf.name(), "dnf");
}
#[test]
fn test_binary() {
assert_eq!(Dnf.binary(), "dnf");
}
#[test]
fn test_update_command_contents() {
let cmds = Dnf.update();
let cmd = cmds[0].as_std();
assert_eq!(cmd.get_program().to_string_lossy(), "dnf");
let args: Vec<String> = cmd
.get_args()
.map(|a| a.to_string_lossy().into_owned())
.collect();
assert_eq!(args, vec!["makecache"]);
}
#[test]
fn test_install_command_contents() {
let cmds = Dnf.install(&["curl".to_string()]);
let cmd = cmds[0].as_std();
assert_eq!(cmd.get_program().to_string_lossy(), "dnf");
let args: Vec<String> = cmd
.get_args()
.map(|a| a.to_string_lossy().into_owned())
.collect();
assert_eq!(args, vec!["install", "-y", "curl"]);
}
#[test]
fn test_install_multiple_packages() {
let cmds = Dnf.install(&["curl".to_string(), "git".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!["install", "-y", "curl", "git"]);
}
#[test]
fn test_change_mirror_command() {
let cmds = Dnf.change_mirror("http://mirror.tuna.tsinghua.edu.cn/fedora", 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("sed -i"));
assert!(full_arg.contains("baseurl="));
assert!(full_arg.contains("yum.repos.d"));
assert!(full_arg.contains("mirror.tuna.tsinghua.edu.cn"));
}
#[test]
fn test_add_repository_simple() {
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("custom-repo".to_string()),
),
(
serde_yaml::Value::String("baseurl".to_string()),
serde_yaml::Value::String(
"https://repo.example.com/fedora".to_string(),
),
),
]
.into_iter()
.collect(),
),
]))
.unwrap();
let cmds = Dnf.add_repository(&repo_yaml, None);
// Should have one command (write .repo file, no GPG key)
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 >"));
assert!(full_arg.contains("custom-repo.repo"));
assert!(full_arg.contains("repo.example.com"));
}
#[test]
fn test_add_repository_with_gpgkey() {
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("custom-repo".to_string()),
),
(
serde_yaml::Value::String("baseurl".to_string()),
serde_yaml::Value::String(
"https://repo.example.com/fedora".to_string(),
),
),
(
serde_yaml::Value::String("gpgkey".to_string()),
serde_yaml::Value::String(
"https://repo.example.com/gpg".to_string(),
),
),
]
.into_iter()
.collect(),
),
]))
.unwrap();
let cmds = Dnf.add_repository(&repo_yaml, None);
// Should have 2 commands: import GPG key, write .repo file
assert_eq!(cmds.len(), 2);
let key_cmd = cmds[0].as_std();
let args: Vec<String> = key_cmd
.get_args()
.map(|a| a.to_string_lossy().into_owned())
.collect();
let full_arg = args.join(" ");
assert!(full_arg.contains("rpm --import"));
assert!(full_arg.contains("repo.example.com/gpg"));
}
#[test]
fn test_add_repository_invalid_yaml_returns_empty() {
let invalid_yaml = serde_yaml::Value::String("invalid".to_string());
let cmds = Dnf.add_repository(&invalid_yaml, None);
assert!(cmds.is_empty());
}
}