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 executor;
pub mod pm;
pub mod upm;
pub mod types;
// 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::types::{
CgroupManager, EventSender, ExecutionContext, ExecutionError, ExecutionEvent, ExecutionResult,
ResourceLimits,
ResourceLimits, DeltaExecutionContext
};
impl super::types::Engine {
@@ -82,6 +82,34 @@ impl super::types::Engine {
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) {
command
.current_dir(&ctx.working_dir)
@@ -162,13 +190,14 @@ impl super::types::Engine {
pub async fn install_dependency(
&self,
package: &[&str],
package: &[String],
pkgman: &dyn PackageManager,
ctx: &ExecutionContext,
event_tx: &EventSender
) -> Result<ExecutionResult, ExecutionError> {
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> {
+9 -9
View File
@@ -9,7 +9,7 @@ pub trait PackageManager {
fn name(&self) -> &'static str;
/// Adopt for a distro
fn adopt(&self, distro: &str) -> bool;
fn adopt(&self, distro: &Info) -> bool;
/// Binary name
fn binary(&self) -> &'static str;
@@ -18,7 +18,7 @@ pub trait PackageManager {
fn update(&self) -> Command;
/// The command of installing packages
fn install(&self, package: &[&str]) -> Command;
fn install(&self, package: &[String]) -> Command;
// Check whether a package is installed
// Currently not planned
@@ -28,10 +28,10 @@ pub trait PackageManager {
// fn is_installed(&self, _package: &str) -> Option<bool> {unimplemented!()}
/// 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
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>> {
@@ -42,13 +42,13 @@ pub fn all_managers() -> Vec<Box<dyn PackageManager>> {
]
}
pub fn detect(distro: os_info::Type) -> Option<Box<dyn PackageManager>> {
let distro_str = distro.to_string();
all_managers().into_iter().find(|pm| pm.adopt(&distro_str))
pub fn detect(distro: &os_info::Info) -> Option<Box<dyn PackageManager>> {
all_managers().into_iter().find(|pm| pm.adopt(distro))
}
pub fn select(manager: &str) -> Option<Box<dyn PackageManager>> {
log::warn!("Manually select package manager is not recommended.");
log::warn!("Use at your own risk.");
// TODO: allow specify PM
// 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)
}
+6 -31
View File
@@ -17,25 +17,8 @@ impl PackageManager for Apt {
"apt"
}
fn adopt(&self, distro: &str) -> bool {
// 匹配所有主要的使用 apt 的发行版
// 注意: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 adopt(&self, distro: &Info) -> bool {
todo!();
}
fn binary(&self) -> &'static str {
@@ -48,7 +31,7 @@ impl PackageManager for Apt {
cmd
}
fn install(&self, package: &[&str]) -> Command {
fn install(&self, package: &[String]) -> Command {
let mut cmd = Command::new("apt");
cmd.arg("install");
cmd.arg("-y"); // 自动确认
@@ -56,12 +39,12 @@ impl PackageManager for Apt {
cmd
}
fn change_mirror(&self, repo: &str, osinfo: Option<Info>) -> Command {
fn change_mirror(&self, repo: &str, osinfo: Option<&Info>) -> Command {
todo!();
}
fn add_repository(&self, repo: &Value, osinfo: Option<Info>) -> Vec<Command> {
fn add_repository(&self, repo: &Value, osinfo: Option<&Info>) -> Vec<Command> {
todo!();
}
@@ -71,14 +54,6 @@ impl PackageManager for Apt {
mod tests {
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]
fn test_update_command_contents() {
let cmd = Apt.update();
@@ -93,7 +68,7 @@ mod tests {
#[test]
fn test_install_command_contents() {
let cmd = Apt.install(&["curl"]);
let cmd = Apt.install(&["curl".to_string()]);
let cmd = cmd.as_std();
assert_eq!(cmd.get_program().to_string_lossy(), "apt");
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 serde::Deserialize;
use serde_yaml::Value;
@@ -74,8 +74,8 @@ impl PackageManager for Pacman {
"pacman"
}
fn adopt(&self, distro: &str) -> bool {
let d = distro.to_lowercase();
fn adopt(&self, distro: &Info) -> bool {
let d = distro.os_type().to_string().to_lowercase();
matches!(
d.as_str(),
"arch"
@@ -101,7 +101,7 @@ impl PackageManager for Pacman {
cmd
}
fn install(&self, package: &[&str]) -> Command {
fn install(&self, package: &[String]) -> Command {
let mut cmd = Command::new("pacman");
cmd.arg("-S");
cmd.arg("--noconfirm");
@@ -110,7 +110,7 @@ impl PackageManager for Pacman {
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 content = format!(
@@ -131,7 +131,7 @@ impl PackageManager for Pacman {
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()) {
Ok(r) => r,
Err(e) => {
@@ -212,28 +212,15 @@ mod tests {
#[test]
fn test_adopt_true_for_arch_based_distros() {
let pacman = Pacman;
assert!(pacman.adopt("arch"));
assert!(pacman.adopt("Arch"));
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"));
// TODO: reimplement adopt_test
todo!()
}
#[test]
fn test_adopt_false_for_non_arch_distros() {
let pacman = Pacman;
assert!(!pacman.adopt("ubuntu"));
assert!(!pacman.adopt("debian"));
assert!(!pacman.adopt("fedora"));
assert!(!pacman.adopt("opensuse"));
assert!(!pacman.adopt("centos"));
// TODO: reimplement adopt_test
todo!()
}
#[test]
@@ -250,7 +237,7 @@ mod tests {
#[test]
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();
assert_eq!(cmd.get_program().to_string_lossy(), "pacman");
let args: Vec<String> = cmd
@@ -262,7 +249,7 @@ mod tests {
#[test]
fn test_install_single_package() {
let cmd = Pacman.install(&["package"]);
let cmd = Pacman.install(&["package".to_string()]);
let cmd = cmd.as_std();
let args: Vec<String> = cmd
.get_args()
+7
View File
@@ -27,6 +27,13 @@ pub struct ExecutionContext {
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 {
fn default() -> 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 os_info::Info;
use thiserror::Error;
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)]
pub enum ExecutionError {
#[error("Invalid command: {0}")]
+1
View File
@@ -2,6 +2,7 @@ pub mod bootstrap;
pub mod environment;
pub mod hook;
pub mod depssystem;
pub mod depsuser;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum PrebakeStage {
+92 -6
View File
@@ -1,12 +1,98 @@
use crate::ExecutionContext;
use crate::engine::{Engine, EventSender, ExecutionError, ExecutionResult};
use crate::prebake::config::PrebakeConfig;
use std::path::PathBuf;
use crate::engine::{Engine, EventSender, pm};
use crate::error::DependencyError;
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(
config: &PrebakeConfig,
config: &HashMap<String, SystemDependency>,
ctx: &ExecutionContext,
event_tx: &EventSender,
) -> Result<ExecutionResult, ExecutionError> {
todo!();
) -> Result<(), DependencyError> {
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::engine::{Engine, EventSender, ExecutionError, ExecutionResult};
use crate::prebake::config::PrebakeConfig;
use std::path::PathBuf;
use crate::error::DependencyError;
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::engine::{Engine, EventSender, ExecutionError};
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() {
let engine = Engine::new();
let mut hook_ctx = ctx.clone();
// Update working directory if provided
if let Some(working_dir) = &hook_cmd.working_dir {
log::debug!("Updating working directory to {}", working_dir);
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 deltactx = DeltaExecutionContext {
working_dir: hook_cmd.working_dir.as_ref().map(PathBuf::from),
env_vars: hook_cmd.environment.clone().unwrap_or_default(),
timeout: Some(std::time::Duration::from_secs(hook_cmd.timeout.into())),
};
log::info!("Executing hook #{}: {}.", index, hook_cmd.name);
let result = engine
.execute_command(&hook_cmd.command, &hook_ctx, &event_tx)
.execute_command_with_delta(&hook_cmd.command, &ctx, &event_tx, &deltactx)
.await;
if let Err(e) = result {
log::error!("Hook {} failed: {:?}", index, e);
+1 -1
View File
@@ -5,7 +5,7 @@ pub struct CustomCommand {
pub name: String,
pub command: String,
#[serde(default)]
pub environment: Option<HashMap<String, String>>,
pub environment: Option<HashMap<String, Option<String>>>,
#[serde(default)]
pub working_dir: Option<String>,
#[serde(default = "default_timeout")]