feat(engine): implement UPM/UserPackageManager interface

This commit is contained in:
Catty Steve
2026-03-31 11:39:47 +08:00
parent 960f03269c
commit 0f6aed6f94
14 changed files with 309 additions and 92 deletions
+1
View File
@@ -6,6 +6,7 @@
pub mod cgroups; pub mod cgroups;
pub mod executor; pub mod executor;
pub mod pm; pub mod pm;
pub mod upm;
pub mod types; pub mod types;
// Re-export commonly used types for convenience // Re-export commonly used types for convenience
+32 -3
View File
@@ -6,7 +6,7 @@ use tokio::process::Command;
use super::pm::PackageManager; use super::pm::PackageManager;
use super::types::{ use super::types::{
CgroupManager, EventSender, ExecutionContext, ExecutionError, ExecutionEvent, ExecutionResult, CgroupManager, EventSender, ExecutionContext, ExecutionError, ExecutionEvent, ExecutionResult,
ResourceLimits, ResourceLimits, DeltaExecutionContext
}; };
impl super::types::Engine { impl super::types::Engine {
@@ -82,6 +82,34 @@ impl super::types::Engine {
self.execute(command, ctx, event_tx).await self.execute(command, ctx, event_tx).await
} }
pub async fn execute_command_with_delta(
&self,
cmd: &str,
ctx: &ExecutionContext,
event_tx: &EventSender,
delta_ctx: &DeltaExecutionContext
) -> Result<ExecutionResult,ExecutionError> {
let mut local_ctx = ctx.clone();
if let Some(working_dir) = &delta_ctx.working_dir {
log::debug!("Updating working directory to {}", working_dir.display());
local_ctx.working_dir = working_dir.clone();
}
if let Some(timeout) = &delta_ctx.timeout {
log::debug!("Updating timeout to {}s", timeout.as_secs_f64());
local_ctx.timeout = Some(*timeout);
}
for (key, value) in &delta_ctx.env_vars {
log::debug!("Updating envvar {} with {:?}", key, value);
match value {
Some(v) => local_ctx.env_vars.insert(key.clone(), v.clone()),
None => local_ctx.env_vars.remove(key),
};
}
self.execute_command(cmd, &local_ctx, event_tx).await
}
fn construct_command(&self, command: &mut Command, ctx: &ExecutionContext) { fn construct_command(&self, command: &mut Command, ctx: &ExecutionContext) {
command command
.current_dir(&ctx.working_dir) .current_dir(&ctx.working_dir)
@@ -162,13 +190,14 @@ impl super::types::Engine {
pub async fn install_dependency( pub async fn install_dependency(
&self, &self,
package: &[&str], package: &[String],
pkgman: &dyn PackageManager, pkgman: &dyn PackageManager,
ctx: &ExecutionContext, ctx: &ExecutionContext,
event_tx: &EventSender
) -> Result<ExecutionResult, ExecutionError> { ) -> Result<ExecutionResult, ExecutionError> {
let cmd = pkgman.install(package); let cmd = pkgman.install(package);
self.execute(cmd, ctx, &None).await self.execute(cmd, ctx, event_tx).await
} }
pub async fn cleanup(&self) -> Result<(), String> { pub async fn cleanup(&self) -> Result<(), String> {
+9 -9
View File
@@ -9,7 +9,7 @@ pub trait PackageManager {
fn name(&self) -> &'static str; fn name(&self) -> &'static str;
/// Adopt for a distro /// Adopt for a distro
fn adopt(&self, distro: &str) -> bool; fn adopt(&self, distro: &Info) -> bool;
/// Binary name /// Binary name
fn binary(&self) -> &'static str; fn binary(&self) -> &'static str;
@@ -18,7 +18,7 @@ pub trait PackageManager {
fn update(&self) -> Command; fn update(&self) -> Command;
/// The command of installing packages /// The command of installing packages
fn install(&self, package: &[&str]) -> Command; fn install(&self, package: &[String]) -> Command;
// Check whether a package is installed // Check whether a package is installed
// Currently not planned // Currently not planned
@@ -28,10 +28,10 @@ pub trait PackageManager {
// fn is_installed(&self, _package: &str) -> Option<bool> {unimplemented!()} // fn is_installed(&self, _package: &str) -> Option<bool> {unimplemented!()}
/// Change major mirror of package manager /// Change major mirror of package manager
fn change_mirror(&self, repo: &str, osinfo: Option<Info>) -> Command; fn change_mirror(&self, repo: &str, osinfo: Option<&Info>) -> Command;
/// add a custom repository /// add a custom repository
fn add_repository(&self, repo: &Value, osinfo: Option<Info>) -> Vec<Command>; fn add_repository(&self, repo: &Value, osinfo: Option<&Info>) -> Vec<Command>;
} }
pub fn all_managers() -> Vec<Box<dyn PackageManager>> { pub fn all_managers() -> Vec<Box<dyn PackageManager>> {
@@ -42,13 +42,13 @@ pub fn all_managers() -> Vec<Box<dyn PackageManager>> {
] ]
} }
pub fn detect(distro: os_info::Type) -> Option<Box<dyn PackageManager>> { pub fn detect(distro: &os_info::Info) -> Option<Box<dyn PackageManager>> {
let distro_str = distro.to_string(); all_managers().into_iter().find(|pm| pm.adopt(distro))
all_managers().into_iter().find(|pm| pm.adopt(&distro_str))
} }
pub fn select(manager: &str) -> Option<Box<dyn PackageManager>> { pub fn select(manager: &str) -> Option<Box<dyn PackageManager>> {
log::warn!("Manually select package manager is not recommended."); // TODO: allow specify PM
log::warn!("Use at your own risk."); // log::warn!("Manually select package manager is not recommended.");
// log::warn!("Use at your own risk.");
all_managers().into_iter().find(|pm| pm.name() == manager) all_managers().into_iter().find(|pm| pm.name() == manager)
} }
+6 -31
View File
@@ -17,25 +17,8 @@ impl PackageManager for Apt {
"apt" "apt"
} }
fn adopt(&self, distro: &str) -> bool { fn adopt(&self, distro: &Info) -> bool {
// 匹配所有主要的使用 apt 的发行版 todo!();
// 注意:os_info 返回的 Type 转字符串通常是全小写或特定格式,这里做宽松匹配
let d = distro.to_lowercase();
matches!(
d.as_str(),
"debian"
| "ubuntu"
| "linuxmint"
| "mint"
| "pop"
| "pop!_os"
| "kali"
| "raspbian"
| "aosc"
| "deepin"
| "uos"
| "elementary"
)
} }
fn binary(&self) -> &'static str { fn binary(&self) -> &'static str {
@@ -48,7 +31,7 @@ impl PackageManager for Apt {
cmd cmd
} }
fn install(&self, package: &[&str]) -> Command { fn install(&self, package: &[String]) -> Command {
let mut cmd = Command::new("apt"); let mut cmd = Command::new("apt");
cmd.arg("install"); cmd.arg("install");
cmd.arg("-y"); // 自动确认 cmd.arg("-y"); // 自动确认
@@ -56,12 +39,12 @@ impl PackageManager for Apt {
cmd cmd
} }
fn change_mirror(&self, repo: &str, osinfo: Option<Info>) -> Command { fn change_mirror(&self, repo: &str, osinfo: Option<&Info>) -> Command {
todo!(); todo!();
} }
fn add_repository(&self, repo: &Value, osinfo: Option<Info>) -> Vec<Command> { fn add_repository(&self, repo: &Value, osinfo: Option<&Info>) -> Vec<Command> {
todo!(); todo!();
} }
@@ -71,14 +54,6 @@ impl PackageManager for Apt {
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn test_adopt_true_for_ubuntu_and_debian() {
let apt = Apt;
assert!(apt.adopt("ubuntu"));
assert!(apt.adopt("debian"));
assert!(!apt.adopt("fedora"));
}
#[test] #[test]
fn test_update_command_contents() { fn test_update_command_contents() {
let cmd = Apt.update(); let cmd = Apt.update();
@@ -93,7 +68,7 @@ mod tests {
#[test] #[test]
fn test_install_command_contents() { fn test_install_command_contents() {
let cmd = Apt.install(&["curl"]); let cmd = Apt.install(&["curl".to_string()]);
let cmd = cmd.as_std(); let cmd = cmd.as_std();
assert_eq!(cmd.get_program().to_string_lossy(), "apt"); assert_eq!(cmd.get_program().to_string_lossy(), "apt");
let args: Vec<String> = cmd let args: Vec<String> = cmd
+12 -25
View File
@@ -1,4 +1,4 @@
use crate::engine::pm::PackageManager; use super::PackageManager;
use os_info::Info; use os_info::Info;
use serde::Deserialize; use serde::Deserialize;
use serde_yaml::Value; use serde_yaml::Value;
@@ -74,8 +74,8 @@ impl PackageManager for Pacman {
"pacman" "pacman"
} }
fn adopt(&self, distro: &str) -> bool { fn adopt(&self, distro: &Info) -> bool {
let d = distro.to_lowercase(); let d = distro.os_type().to_string().to_lowercase();
matches!( matches!(
d.as_str(), d.as_str(),
"arch" "arch"
@@ -101,7 +101,7 @@ impl PackageManager for Pacman {
cmd cmd
} }
fn install(&self, package: &[&str]) -> Command { fn install(&self, package: &[String]) -> Command {
let mut cmd = Command::new("pacman"); let mut cmd = Command::new("pacman");
cmd.arg("-S"); cmd.arg("-S");
cmd.arg("--noconfirm"); cmd.arg("--noconfirm");
@@ -110,7 +110,7 @@ impl PackageManager for Pacman {
cmd cmd
} }
fn change_mirror(&self, repo: &str, _osinfo: Option<Info>) -> Command { fn change_mirror(&self, repo: &str, _osinfo: Option<&Info>) -> Command {
let mirrorlist_path = "/etc/pacman.d/mirrorlist"; let mirrorlist_path = "/etc/pacman.d/mirrorlist";
let content = format!( let content = format!(
@@ -131,7 +131,7 @@ impl PackageManager for Pacman {
cmd cmd
} }
fn add_repository(&self, repo: &Value, _osinfo: Option<Info>) -> Vec<Command> { fn add_repository(&self, repo: &Value, _osinfo: Option<&Info>) -> Vec<Command> {
let repos: Vec<PacmanRepo> = match serde_yaml::from_value(repo.clone()) { let repos: Vec<PacmanRepo> = match serde_yaml::from_value(repo.clone()) {
Ok(r) => r, Ok(r) => r,
Err(e) => { Err(e) => {
@@ -212,28 +212,15 @@ mod tests {
#[test] #[test]
fn test_adopt_true_for_arch_based_distros() { fn test_adopt_true_for_arch_based_distros() {
let pacman = Pacman; let pacman = Pacman;
assert!(pacman.adopt("arch")); // TODO: reimplement adopt_test
assert!(pacman.adopt("Arch")); todo!()
assert!(pacman.adopt("ARCH"));
assert!(pacman.adopt("manjaro"));
assert!(pacman.adopt("endeavouros"));
assert!(pacman.adopt("garuda"));
assert!(pacman.adopt("cachyos"));
assert!(pacman.adopt("artix"));
assert!(pacman.adopt("mabox"));
assert!(pacman.adopt("nobara"));
assert!(pacman.adopt("instantos"));
assert!(pacman.adopt("archlinux"));
} }
#[test] #[test]
fn test_adopt_false_for_non_arch_distros() { fn test_adopt_false_for_non_arch_distros() {
let pacman = Pacman; let pacman = Pacman;
assert!(!pacman.adopt("ubuntu")); // TODO: reimplement adopt_test
assert!(!pacman.adopt("debian")); todo!()
assert!(!pacman.adopt("fedora"));
assert!(!pacman.adopt("opensuse"));
assert!(!pacman.adopt("centos"));
} }
#[test] #[test]
@@ -250,7 +237,7 @@ mod tests {
#[test] #[test]
fn test_install_command_contents() { fn test_install_command_contents() {
let cmd = Pacman.install(&["curl", "wget"]); let cmd = Pacman.install(&["curl".to_string(), "wget".to_string()]);
let cmd = cmd.as_std(); let cmd = cmd.as_std();
assert_eq!(cmd.get_program().to_string_lossy(), "pacman"); assert_eq!(cmd.get_program().to_string_lossy(), "pacman");
let args: Vec<String> = cmd let args: Vec<String> = cmd
@@ -262,7 +249,7 @@ mod tests {
#[test] #[test]
fn test_install_single_package() { fn test_install_single_package() {
let cmd = Pacman.install(&["package"]); let cmd = Pacman.install(&["package".to_string()]);
let cmd = cmd.as_std(); let cmd = cmd.as_std();
let args: Vec<String> = cmd let args: Vec<String> = cmd
.get_args() .get_args()
+7
View File
@@ -27,6 +27,13 @@ pub struct ExecutionContext {
pub shell: String, pub shell: String,
} }
#[derive(Debug, Clone)]
pub struct DeltaExecutionContext {
pub working_dir: Option<PathBuf>,
pub env_vars: HashMap<String, Option<String>>,
pub timeout: Option<Duration>,
}
impl Default for ExecutionContext { impl Default for ExecutionContext {
fn default() -> Self { fn default() -> Self {
Self { Self {
+39
View File
@@ -0,0 +1,39 @@
use super::types::EventSender;
use async_trait::async_trait;
use serde_yaml::Value;
mod custom;
use crate::{ExecutionContext, error::DependencyError};
pub struct UPMSysDeps {
/// Packages to be installed
packages: Vec<String>,
/// Whether package name is mapped to distro
mapped: bool,
}
#[async_trait]
pub trait UserPackageManager {
/// Name of package manager
fn name(&self) -> &'static str;
/// Indicates that DepsSystem needs to install these packages
fn system_dependency(&self, config: &Value) -> UPMSysDeps;
/// Main function of User Package Manager
async fn main(
&self,
config: &Value,
ctx: &ExecutionContext,
event_tx: &EventSender,
) -> Result<(), DependencyError>;
}
pub fn all_managers() -> Vec<Box<dyn UserPackageManager>> {
vec![]
}
pub fn select(manager: &str) -> Option<Box<dyn UserPackageManager>> {
all_managers().into_iter().find(|pm| pm.name() == manager)
}
+64
View File
@@ -0,0 +1,64 @@
use std::path::PathBuf;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use crate::Engine;
use crate::engine::types::DeltaExecutionContext;
use crate::engine::upm::UPMSysDeps;
use crate::error::DependencyError;
use crate::types::command::CustomCommand;
use super::UserPackageManager;
#[derive(Clone)]
pub struct Custom;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(transparent)]
struct CustomConfig(Vec<CustomCommand>);
#[async_trait]
impl UserPackageManager for Custom {
fn name(&self) -> &'static str {
"custom"
}
fn system_dependency(&self, _config: &serde_yaml::Value) -> super::UPMSysDeps {
UPMSysDeps {
packages: vec![],
mapped: true,
}
}
async fn main(
&self,
config: &serde_yaml::Value,
ctx: &crate::ExecutionContext,
event_tx: &crate::EventSender,
) -> Result<(), DependencyError> {
let custom_config: CustomConfig = match serde_yaml::from_value(config.clone()) {
Ok(r) => r,
Err(e) => {
log::error!("Unable to parse user.custom: {}", e);
return Err(DependencyError::ParseError());
}
};
let engine = Engine::new();
for (index, cmd) in custom_config.0.iter().enumerate() {
let deltactx = DeltaExecutionContext {
working_dir: cmd.working_dir.as_ref().map(PathBuf::from),
env_vars: cmd.environment.clone().unwrap_or_default(),
timeout: Some(std::time::Duration::from_secs(cmd.timeout.into())),
};
log::info!("Executing custom command #{}: {}.", index, cmd.name);
let result = engine
.execute_command_with_delta(&cmd.command, &ctx, &event_tx, &deltactx)
.await;
if let Err(e) = result {
log::error!("Custom command {} failed: {:?}", index, e);
return Err(DependencyError::CustomFailed(e));
}
};
Ok(())
}
}
+20
View File
@@ -1,4 +1,5 @@
use std::path::PathBuf; use std::path::PathBuf;
use os_info::Info;
use thiserror::Error; use thiserror::Error;
pub const EXITCODE_OK: i32 = 0; pub const EXITCODE_OK: i32 = 0;
@@ -72,6 +73,25 @@ impl PrebakeError {
} }
} }
#[derive(Error, Debug)]
pub enum DependencyError {
#[error("No supported package manager found for {0}")]
UnsupportedPlatform(Info),
#[error("Failed to parse config")]
ParseError(),
#[error("Stage {stage} failed: {source}")]
StageFailed{
stage: &'static str,
#[source]
source: ExecutionError
},
#[error("Custom command execution failed: {0}")]
CustomFailed(#[from] ExecutionError),
}
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum ExecutionError { pub enum ExecutionError {
#[error("Invalid command: {0}")] #[error("Invalid command: {0}")]
+1
View File
@@ -2,6 +2,7 @@ pub mod bootstrap;
pub mod environment; pub mod environment;
pub mod hook; pub mod hook;
pub mod depssystem; pub mod depssystem;
pub mod depsuser;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum PrebakeStage { pub enum PrebakeStage {
+92 -6
View File
@@ -1,12 +1,98 @@
use crate::ExecutionContext; use crate::ExecutionContext;
use crate::engine::{Engine, EventSender, ExecutionError, ExecutionResult}; use crate::engine::{Engine, EventSender, pm};
use crate::prebake::config::PrebakeConfig; use crate::error::DependencyError;
use std::path::PathBuf; use crate::prebake::config::SystemDependency;
use std::collections::HashMap;
/// Handles system dependency installation for the target platform.
///
/// This function:
/// - Detects the operating system and package manager
/// - Optionally changes the package manager mirror
/// - Optionally adds custom repositories
/// - Updates the package index
/// - Installs configured system packages
pub async fn depssystem( pub async fn depssystem(
config: &PrebakeConfig, config: &HashMap<String, SystemDependency>,
ctx: &ExecutionContext, ctx: &ExecutionContext,
event_tx: &EventSender, event_tx: &EventSender,
) -> Result<ExecutionResult, ExecutionError> { ) -> Result<(), DependencyError> {
todo!(); let osinfo = os_info::get();
let engine = Engine::new();
// Detect package manager
let package_manager = pm::detect(&osinfo).ok_or_else(|| {
log::error!("No supported package manager found for {}", &osinfo);
DependencyError::UnsupportedPlatform(osinfo.clone())
})?;
log::info!("Detected package manager: {}", package_manager.name());
// Retrieve configuration
let Some(pm_config) = config.get(package_manager.name()) else {
log::info!(
"No configuration found for {}, skipping system dependencies",
package_manager.name()
);
return Ok(());
};
// Change mirror
if let Some(mirror) = &pm_config.mirror {
log::info!("Changing mirror to: {}", mirror);
let cmd = package_manager.change_mirror(mirror, Some(&osinfo));
log::debug!("Executing: {:?}", &cmd);
engine
.execute(cmd, &ctx, &event_tx)
.await
.map_err(|e| DependencyError::StageFailed {
stage: "MirrorChange",
source: e,
})?;
}
// Add custom repository
if let Some(repos) = &pm_config.repositories {
log::info!(
"Adding {} custom repositories",
repos.as_sequence().map(|s| s.len()).unwrap_or(0)
);
let cmds = package_manager.add_repository(repos, Some(&osinfo));
for cmd in cmds {
log::debug!("Executing: {:?}", &cmd);
engine.execute(cmd, &ctx, &event_tx).await.map_err(|e| {
DependencyError::StageFailed {
stage: "CustomRepoAdd",
source: e,
}
})?;
}
}
// Update package index
{
log::info!("Updating package index...");
let cmd = package_manager.update();
log::debug!("Executing: {:?}", &cmd);
engine
.execute(cmd, &ctx, &event_tx)
.await
.map_err(|e| DependencyError::StageFailed {
stage: "UpdateIndex",
source: e,
})?;
}
// Install packages
if !pm_config.packages.is_empty() {
log::info!("Installing {} packages", pm_config.packages.len());
engine
.install_dependency(&pm_config.packages, &*package_manager, &ctx, &event_tx)
.await
.map_err(|e| DependencyError::StageFailed {
stage: "InstallPackage",
source: e,
})?;
}
Ok(())
} }
+16 -2
View File
@@ -1,4 +1,18 @@
use serde_yaml::Value;
use crate::ExecutionContext; use crate::ExecutionContext;
use crate::engine::{Engine, EventSender, ExecutionError, ExecutionResult}; use crate::engine::{Engine, EventSender, ExecutionError, ExecutionResult};
use crate::prebake::config::PrebakeConfig; use crate::error::DependencyError;
use std::path::PathBuf; use std::collections::HashMap;
pub async fn depsuser(
config: &HashMap<String, Value>,
ctx: &ExecutionContext,
event_tx: &EventSender,
) -> Result<(), DependencyError> {
for (name, cfg) in config {
let package_manager = upm::get(name).unwrap(); // ?
}
todo!();
}
+9 -15
View File
@@ -1,3 +1,4 @@
use crate::engine::types::DeltaExecutionContext;
use crate::ExecutionContext; use crate::ExecutionContext;
use crate::engine::{Engine, EventSender, ExecutionError}; use crate::engine::{Engine, EventSender, ExecutionError};
use crate::types::command::CustomCommand; use crate::types::command::CustomCommand;
@@ -15,23 +16,16 @@ pub async fn hook(
} }
}; };
let engine = Engine::new();
for (index, hook_cmd) in hook.iter().enumerate() { for (index, hook_cmd) in hook.iter().enumerate() {
let engine = Engine::new(); let deltactx = DeltaExecutionContext {
let mut hook_ctx = ctx.clone(); working_dir: hook_cmd.working_dir.as_ref().map(PathBuf::from),
env_vars: hook_cmd.environment.clone().unwrap_or_default(),
// Update working directory if provided timeout: Some(std::time::Duration::from_secs(hook_cmd.timeout.into())),
if let Some(working_dir) = &hook_cmd.working_dir { };
log::debug!("Updating working directory to {}", working_dir); log::info!("Executing hook #{}: {}.", index, hook_cmd.name);
hook_ctx.working_dir = PathBuf::from(working_dir);
}
// Update environment variables if provided
if let Some(environment) = &hook_cmd.environment {
hook_ctx.env_vars.extend(environment.clone());
}
log::info!("Executing hook index {}.", index);
let result = engine let result = engine
.execute_command(&hook_cmd.command, &hook_ctx, &event_tx) .execute_command_with_delta(&hook_cmd.command, &ctx, &event_tx, &deltactx)
.await; .await;
if let Err(e) = result { if let Err(e) = result {
log::error!("Hook {} failed: {:?}", index, e); log::error!("Hook {} failed: {:?}", index, e);
+1 -1
View File
@@ -5,7 +5,7 @@ pub struct CustomCommand {
pub name: String, pub name: String,
pub command: String, pub command: String,
#[serde(default)] #[serde(default)]
pub environment: Option<HashMap<String, String>>, pub environment: Option<HashMap<String, Option<String>>>,
#[serde(default)] #[serde(default)]
pub working_dir: Option<String>, pub working_dir: Option<String>,
#[serde(default = "default_timeout")] #[serde(default = "default_timeout")]