use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use color_eyre::eyre::{Result, Context};
use crate::models::PackageFormat;
use crate::package::{pkgkey2pkgname, pkgkey2version};
use crate::models::PACKAGE_CACHE;
use crate::version_constraint::check_version_constraint;
use crate::package_cache::map_pkgline2filelist;
use crate::plan::InstallationPlan;
use crate::plan::pkgkey2pkgline;
use crate::parse_requires::VersionConstraint;
use crate::rpm_triggers::{parse_rpm_trigger_condition, RPMTRIGGER_DEFAULT_PRIORITY};
use crate::run::{fork_and_execute, RunOptions};
use shlex;
use glob::Pattern;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum HookOperation {
Install = 1 << 0,
Upgrade = 1 << 1,
Remove = 1 << 2,
}
impl HookOperation {
#[inline]
pub fn as_flag(self) -> u8 {
self as u8
}
}
trait HookOperationFlags {
fn is_set(self, op: HookOperation) -> bool;
}
impl HookOperationFlags for u8 {
#[inline]
fn is_set(self, op: HookOperation) -> bool {
(self & op.as_flag()) != 0
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum HookType {
Path,
Package,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum HookWhen {
PreTransaction,
PostTransaction,
PostUnTrans,
PreInstall,
PreInstall2,
PostInstall,
PostInstall2,
PreRemove,
PreRemove2,
PostRemove,
PostRemove2,
PreUpgrade,
PostUpgrade,
}
#[derive(Debug, Clone)]
pub struct HookTrigger {
pub operations: u8,
pub hook_type: HookType,
pub targets: Vec<String>,
pub positive_targets: Vec<String>,
pub negative_targets: Vec<String>,
pub positive_prefixes: Vec<String>,
pub positive_packages: HashMap<String, Vec<VersionConstraint>>,
pub positive_patterns: Vec<Pattern>,
pub negative_patterns: Vec<Pattern>,
pub type_set: bool,
}
impl Default for HookTrigger {
fn default() -> Self {
Self {
operations: 0,
hook_type: HookType::Path,
targets: Vec::new(),
positive_targets: Vec::new(),
negative_targets: Vec::new(),
positive_prefixes: Vec::new(),
positive_packages: HashMap::new(),
positive_patterns: Vec::new(),
negative_patterns: Vec::new(),
type_set: false,
}
}
}
#[derive(Debug, Clone)]
pub struct HookAction {
pub description: Option<String>,
pub when: HookWhen,
pub exec: String,
pub depends: Vec<String>,
pub abort_on_fail: bool,
pub needs_targets: bool,
pub priority: u32,
pub script_order: u32,
}
impl Default for HookAction {
fn default() -> Self {
Self {
description: None,
when: HookWhen::PostTransaction,
exec: String::new(),
depends: Vec::new(),
abort_on_fail: false,
needs_targets: false,
priority: RPMTRIGGER_DEFAULT_PRIORITY,
script_order: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct Hook {
pub triggers: Vec<HookTrigger>,
pub action: HookAction,
pub hook_name: String,
pub file_path: String,
pub pkgkey: Option<String>,
}
fn extract_hook_file_name(hook_path: &Path, pkgkey: Option<&str>) -> String {
let file_name_full = hook_path.file_name()
.and_then(|n| n.to_str())
.unwrap()
.to_string();
let base_name = file_name_full.strip_suffix(".hook").unwrap_or(&file_name_full).to_string();
if let Some(pkgkey) = pkgkey {
format!("{}-{}", base_name, pkgkey)
} else {
base_name
}
}
pub fn parse_hook_file(hook_path: &Path, pkgkey: Option<&str>) -> Result<Hook> {
let content = fs::read_to_string(hook_path)
.with_context(|| format!("Failed to read hook file: {}", hook_path.display()))?;
let mut current_triggers = Vec::new();
let mut current_action: Option<HookAction> = None;
let mut in_trigger = false;
let mut in_action = false;
let hook_name = extract_hook_file_name(hook_path, pkgkey);
let mut line_num = 0;
for line in content.lines() {
line_num += 1;
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line == "[Trigger]" {
in_trigger = true;
in_action = false;
current_triggers.push(HookTrigger::default());
continue;
} else if line == "[Action]" {
in_action = true;
in_trigger = false;
current_action = Some(HookAction::default());
continue;
}
if in_trigger {
parse_trigger_line(line, &mut current_triggers, hook_path, line_num)?;
} else if in_action {
parse_action_line(line, current_action.as_mut().unwrap(), hook_path, line_num)?;
} else {
return Err(color_eyre::eyre::eyre!(
"hook {} line {}: invalid option {} (not in a section)",
hook_path.display(), line_num, line
));
}
}
let hook = Hook {
triggers: current_triggers,
action: current_action.unwrap(),
hook_name,
file_path: hook_path.to_string_lossy().to_string(),
pkgkey: None,
};
validate_hook(&hook, hook_path)?;
Ok(hook)
}
fn validate_hook(hook: &Hook, file: &Path) -> Result<()> {
if hook.triggers.is_empty() {
return Ok(());
}
for trigger in &hook.triggers {
if trigger.targets.is_empty() {
return Err(color_eyre::eyre::eyre!(
"Missing trigger targets in hook: {}",
file.display()
));
}
if trigger.operations == 0 {
return Err(color_eyre::eyre::eyre!(
"Missing trigger operation in hook: {}",
file.display()
));
}
for pattern in &trigger.positive_prefixes {
let check_pattern = pattern.strip_prefix('!').unwrap_or(pattern.strip_prefix('+').unwrap_or(pattern));
if check_pattern.starts_with('/') {
return Err(color_eyre::eyre::eyre!(
"Target value '{}' in hook {} has leading '/', should be '{}'",
pattern,
file.display(),
check_pattern.strip_prefix('/').unwrap_or(check_pattern)
));
}
}
}
if hook.action.exec.is_empty() {
return Err(color_eyre::eyre::eyre!(
"Missing Exec option in hook: {}",
file.display()
));
}
if hook.action.when == HookWhen::PostTransaction && hook.action.abort_on_fail {
log::warn!(
"AbortOnFail set for PostTransaction hook: {}",
file.display()
);
}
Ok(())
}
fn parse_trigger_line(line: &str, triggers: &mut Vec<HookTrigger>, file: &Path, line_num: usize) -> Result<()> {
if let Some((key, value)) = line.split_once('=') {
let key = key.trim();
let value = value.trim();
let trigger = triggers.last_mut().unwrap();
match key {
"Operation" => {
let operation = match value {
"Install" => HookOperation::Install,
"Upgrade" => HookOperation::Upgrade,
"Remove" => HookOperation::Remove,
_ => return Err(color_eyre::eyre::eyre!(
"hook {} line {}: invalid value {}",
file.display(), line_num, value
)),
};
trigger.operations |= operation.as_flag();
}
"Type" => {
if trigger.type_set {
log::warn!("hook {} line {}: overwriting previous definition of Type", file.display(), line_num);
}
trigger.type_set = true;
trigger.hook_type = match value {
"Path" | "File" => HookType::Path,
"Package" => HookType::Package,
_ => return Err(color_eyre::eyre::eyre!(
"hook {} line {}: invalid value {}",
file.display(), line_num, value
)),
};
}
"Target" => {
trigger.targets.push(value.to_string());
}
_ => {
return Err(color_eyre::eyre::eyre!(
"hook {} line {}: invalid option {}",
file.display(), line_num, key
));
}
}
} else {
return Err(color_eyre::eyre::eyre!(
"hook {} line {}: invalid option {}",
file.display(), line_num, line
));
}
Ok(())
}
fn split_hook_targets(targets: &[String]) -> (Vec<String>, Vec<String>) {
let mut positive_targets = Vec::new();
let mut negative_targets = Vec::new();
for target in targets {
if let Some(stripped) = target.strip_prefix('!') {
negative_targets.push(stripped.to_string());
} else {
positive_targets.push(target.clone());
}
}
(positive_targets, negative_targets)
}
#[inline]
fn is_glob_char(c: char) -> bool {
matches!(c, '*' | '?' | '[' | ']')
}
fn extract_prefix(pattern: &str) -> Option<&str> {
let candidate = pattern.strip_suffix('*').unwrap_or(pattern);
if !candidate.chars().any(is_glob_char) {
return Some(candidate);
}
None
}
fn separate_prefixes_and_patterns(positive_targets: &[String]) -> (Vec<String>, Vec<String>) {
let mut prefixes = Vec::new();
let mut patterns = Vec::new();
for target in positive_targets {
if let Some(prefix) = extract_prefix(target.strip_prefix('/').unwrap_or(target)) {
prefixes.push(prefix.to_string());
} else {
patterns.push(target.clone());
}
}
(prefixes, patterns)
}
fn compile_patterns(patterns: &[String]) -> Vec<Pattern> {
patterns.iter()
.filter_map(|p| Pattern::new(p).ok())
.collect()
}
fn populate_trigger_cache(trigger: &mut HookTrigger) {
let (positive_targets, negative_targets) = split_hook_targets(&trigger.targets);
if trigger.hook_type == HookType::Package {
for target in &positive_targets {
if target.chars().any(is_glob_char) {
match Pattern::new(target) {
Ok(pattern) => {
trigger.positive_patterns.push(pattern);
}
Err(e) => {
log::warn!("Failed to compile pattern '{}' for Package trigger: {}", target, e);
}
}
} else {
let packages = parse_rpm_trigger_condition(target);
for (pkg_name, constraints) in packages {
let entry = trigger.positive_packages.entry(pkg_name.clone()).or_insert_with(Vec::new);
entry.extend(constraints);
}
}
}
} else {
let (prefixes, patterns) = separate_prefixes_and_patterns(&positive_targets);
trigger.positive_prefixes = prefixes;
trigger.positive_patterns = compile_patterns(&patterns);
}
trigger.negative_patterns = compile_patterns(&negative_targets);
trigger.negative_targets = negative_targets;
trigger.positive_targets = positive_targets;
}
fn populate_hook_target_cache(hook: &mut Hook) {
for trigger in &mut hook.triggers {
populate_trigger_cache(trigger);
}
}
fn parse_when_value(raw: &str, file: &Path, line_num: usize) -> Result<HookWhen> {
let v = raw.trim();
let when = match v {
"PreTransaction" => HookWhen::PreTransaction,
"PostTransaction" => HookWhen::PostTransaction,
"PostUnTrans" => HookWhen::PostUnTrans,
"PreInstall" => HookWhen::PreInstall,
"PreInstall2" => HookWhen::PreInstall2,
"PostInstall" => HookWhen::PostInstall,
"PostInstall2" => HookWhen::PostInstall2,
"PreRemove" => HookWhen::PreRemove,
"PreRemove2" => HookWhen::PreRemove2,
"PostRemove" => HookWhen::PostRemove,
"PostRemove2" => HookWhen::PostRemove2,
"PreUpgrade" => HookWhen::PreUpgrade,
"PostUpgrade" => HookWhen::PostUpgrade,
_ => {
return Err(color_eyre::eyre::eyre!(
"hook {} line {}: invalid When value {}",
file.display(),
line_num,
raw
));
}
};
Ok(when)
}
fn parse_action_line(line: &str, action: &mut HookAction, file: &Path, line_num: usize) -> Result<()> {
if let Some((key, value)) = line.split_once('=') {
let key = key.trim();
let value = value.trim();
match key {
"When" => {
if action.when != HookWhen::PostTransaction {
log::warn!(
"hook {} line {}: overwriting previous definition of When",
file.display(),
line_num
);
}
action.when = parse_when_value(value, file, line_num)?;
}
"Description" => {
if action.description.is_some() {
log::warn!("hook {} line {}: overwriting previous definition of Description", file.display(), line_num);
}
action.description = Some(value.to_string());
}
"Depends" => {
action.depends.extend(
value.split_whitespace().map(|s| s.to_string())
);
}
"Exec" => {
if !action.exec.is_empty() {
log::warn!(
"hook {} line {}: overwriting previous definition of Exec",
file.display(),
line_num
);
}
action.exec = value.to_string();
}
"Priority" => {
let prio = value.parse::<u32>().map_err(|e| {
color_eyre::eyre::eyre!(
"hook {} line {}: invalid Priority {} ({})",
file.display(),
line_num,
value,
e
)
})?;
action.priority = prio;
}
"ScriptOrder" => {
let order = value.parse::<u32>().map_err(|e| {
color_eyre::eyre::eyre!(
"hook {} line {}: invalid ScriptOrder {} ({})",
file.display(),
line_num,
value,
e
)
})?;
action.script_order = order;
}
_ => {
return Err(color_eyre::eyre::eyre!(
"hook {} line {}: invalid option {}",
file.display(), line_num, key
));
}
}
} else {
match line {
"AbortOnFail" => {
action.abort_on_fail = true;
}
"NeedsTargets" => {
action.needs_targets = true;
}
_ => {
return Err(color_eyre::eyre::eyre!(
"hook {} line {}: invalid option {}",
file.display(), line_num, line
));
}
}
}
Ok(())
}
fn read_hook_directory_entries(hook_dir: &Path) -> Option<Vec<fs::DirEntry>> {
let entries: Vec<_> = match fs::read_dir(hook_dir) {
Ok(dir) => match dir.collect::<std::result::Result<Vec<_>, _>>() {
Ok(entries) => entries,
Err(e) => {
log::warn!("Failed to read hook directory entries: {}", e);
return None;
}
},
Err(e) => {
log::warn!("Failed to read hook directory {}: {}", hook_dir.display(), e);
return None;
}
};
Some(entries)
}
fn update_existing_hook_pkgkey(
hook_name: &str,
pkgkey: &str,
path: &Path,
plan: &mut InstallationPlan,
) -> bool {
if let Some(existing_hook_arc) = plan.hooks_by_name.get_mut(hook_name) {
if existing_hook_arc.pkgkey.is_none() {
let existing_file_path = existing_hook_arc.file_path.clone();
Arc::make_mut(existing_hook_arc).pkgkey = Some(pkgkey.to_string());
log::info!(
"hook '{}' from package {} ({}) is overriding global hook ({})",
hook_name,
pkgkey,
path.display(),
existing_file_path
);
return true;
}
}
false
}
fn register_hook_to_plan(
mut hook: Hook,
hook_name: String,
pkgkey: Option<&str>,
plan: &mut InstallationPlan,
) {
hook.pkgkey = pkgkey.map(|s| s.to_string());
let hook_arc = Arc::new(hook);
plan.hooks_by_name.insert(hook_name, hook_arc);
}
fn build_hook_indices(plan: &mut InstallationPlan) {
plan.hooks_by_when.clear();
plan.hooks_by_pkgkey.clear();
for hook_arc in plan.hooks_by_name.values() {
plan.hooks_by_when
.entry(hook_arc.action.when.clone())
.or_insert_with(Vec::new)
.push(Arc::clone(hook_arc));
if let Some(ref pkgkey) = hook_arc.pkgkey {
plan.hooks_by_pkgkey
.entry(pkgkey.clone())
.or_insert_with(Vec::new)
.push(Arc::clone(hook_arc));
}
}
log::debug!("build_hook_indices: {} hooks_by_name entries, {} hooks_by_when entries",
plan.hooks_by_name.len(), plan.hooks_by_when.len());
for (when, hooks) in &plan.hooks_by_when {
log::debug!(" hooks_by_when[{:?}]: {} hooks", when, hooks.len());
}
}
fn load_hook_file(
path: &Path,
plan: &mut InstallationPlan,
pkgkey: Option<&str>,
) {
if path.extension().and_then(|e| e.to_str()) != Some("hook") {
log::trace!("skipping non-hook file {}", path.display());
return;
}
let global_hook_name = extract_hook_file_name(&path, None);
if let Some(pkgkey) = pkgkey {
if update_existing_hook_pkgkey(&global_hook_name, pkgkey, path, plan) {
return;
}
}
let hook_name = extract_hook_file_name(&path, pkgkey);
if let Ok(link_target) = fs::read_link(&path) {
if link_target == PathBuf::from("/dev/null") {
log::debug!("Skipping disabled hook: {}", path.display());
return;
}
}
if path.is_dir() {
log::debug!("skipping directory {}", path.display());
return;
}
match parse_hook_file(&path, pkgkey) {
Ok(mut hook) => {
populate_hook_target_cache(&mut hook);
register_hook_to_plan(hook, hook_name, pkgkey, plan);
}
Err(e) => {
log::warn!("Failed to parse hook file {}: {}", path.display(), e);
}
}
}
fn load_hooks_from_directory(
plan: &mut InstallationPlan,
hook_dir: &Path,
pkgkey: Option<&str>,
) -> Result<()> {
if !hook_dir.exists() {
return Ok(());
}
let entries = match read_hook_directory_entries(hook_dir) {
Some(entries) => entries,
None => return Ok(()),
};
for entry in entries {
load_hook_file(&entry.path(), plan, pkgkey);
}
Ok(())
}
fn load_package_hooks(plan: &mut InstallationPlan, pkgkey: &str) -> Result<()> {
let pkgline = pkgkey2pkgline(plan, pkgkey);
if pkgline.is_empty() {
log::debug!("Package {} has no pkgline, skipping hook loading", pkgkey);
return Ok(());
}
let hook_dir = match plan.package_format {
crate::models::PackageFormat::Pacman => {
crate::dirs::path_join(
&plan.store_root.join(&pkgline).join("fs"),
&["usr", "share", "libalpm", "hooks"],
)
}
_ => {
crate::dirs::path_join(&plan.store_root.join(&pkgline), &["info", "install"])
}
};
load_hooks_from_directory(plan, &hook_dir, Some(pkgkey))
}
pub fn load_initial_hooks(plan: &mut InstallationPlan) -> Result<()> {
if plan.package_format == PackageFormat::Pacman {
let etc_hooks_dir = crate::dirs::path_join(&plan.env_root, &["etc", "pacman.d", "hooks"]);
load_hooks_from_directory(plan, &etc_hooks_dir, None)?;
}
if plan.package_format == PackageFormat::Apk {
let etc_hooks_dir = crate::dirs::path_join(&plan.env_root, &["etc", "apk", "commit_hooks.d"]);
load_hooks_from_directory(plan, &etc_hooks_dir, None)?;
}
let pkgkeys: Vec<String> = {
let installed = PACKAGE_CACHE.installed_packages.read().unwrap();
installed.keys().cloned().collect()
};
for pkgkey in pkgkeys {
load_package_hooks(plan, &pkgkey)?;
}
add_systemd_hooks_if_needed(plan)?;
build_hook_indices(plan);
Ok(())
}
fn is_systemd_installed() -> bool {
let world = PACKAGE_CACHE.world.read().unwrap();
world.contains_key("systemd")
}
fn is_systemd_in_no_install() -> bool {
let world = PACKAGE_CACHE.world.read().unwrap();
world.get("no-install")
.map(|s| s.split_whitespace().any(|pkg| pkg == "systemd"))
.unwrap_or(false)
}
fn add_systemd_hooks_if_needed(plan: &mut InstallationPlan) -> Result<()> {
if is_systemd_installed() {
return Ok(());
}
if !is_systemd_in_no_install() {
return Ok(());
}
log::debug!("systemd is in no-install list, adding systemd hooks");
create_deb_sysusers(plan);
create_systemd_hook(
"usr/lib/sysusers.d/*.conf",
"Creating system user accounts...",
"systemd-sysusers",
1000700,
"20-systemd-sysusers",
HookWhen::PostTransaction,
plan,
)?;
create_systemd_hook(
"usr/lib/tmpfiles.d/*.conf",
"Creating temporary files...",
"systemd-tmpfiles --create",
1000600,
"21-systemd-tmpfiles",
HookWhen::PostTransaction,
plan,
)?;
Ok(())
}
fn create_systemd_hook(
target_pattern: &str,
description: &str,
exec: &str,
priority: u32,
hook_name: &str,
when: HookWhen,
plan: &mut InstallationPlan,
) -> Result<()> {
let mut hook = Hook {
triggers: vec![HookTrigger {
operations: HookOperation::Install.as_flag() | HookOperation::Upgrade.as_flag(),
hook_type: HookType::Path,
targets: vec![target_pattern.to_string()],
type_set: true,
..Default::default()
}],
action: HookAction {
description: Some(description.to_string()),
when,
exec: exec.to_string(),
priority,
..Default::default()
},
hook_name: hook_name.to_string(),
file_path: format!("VirtualFile({})", hook_name),
pkgkey: None,
};
populate_hook_target_cache(&mut hook);
register_hook_to_plan(hook, hook_name.to_string(), None, plan);
Ok(())
}
fn create_deb_sysusers(plan: &InstallationPlan)
{
if plan.package_format != crate::models::PackageFormat::Deb {
return;
}
let basic_conf_path = crate::dirs::path_join(&plan.env_root, &["usr", "lib", "sysusers.d", "basic.conf"]);
if basic_conf_path.exists() {
return;
}
if let Some(parent) = basic_conf_path.parent() {
let _ = fs::create_dir_all(parent);
}
let content = r#"g adm 4 -
g tty 5 -
g disk 6 -
g man 12 -
g kmem 15 -
g dialout 20 -
g fax 21 -
g voice 22 -
g cdrom 24 -
g floppy 25 -
g tape 26 -
g sudo 27 -
g audio 29 -
g dip 30 -
g operator 37 -
g src 40 -
g shadow 42 -
g utmp 43 -
g video 44 -
g sasl 45 -
g plugdev 46 -
g staff 50 -
g games 60 -
g users 100 -
g nogroup 65534 -
u root 0 - /root /bin/bash
u daemon 1 - /usr/sbin /usr/sbin/nologin
u bin 2 - /bin /usr/sbin/nologin
u sys 3 - /dev /usr/sbin/nologin
u sync 4:65534 - /bin /bin/sync
u games 5:60 - /usr/games /usr/sbin/nologin
u man 6:12 - /var/cache/man /usr/sbin/nologin
u lp 7 - /var/spool/lpd /usr/sbin/nologin
u mail 8 - /var/mail /usr/sbin/nologin
u news 9 - /var/spool/news /usr/sbin/nologin
u uucp 10 - /var/spool/uucp /usr/sbin/nologin
u proxy 13 - /bin /usr/sbin/nologin
u www-data 33 - /var/www /usr/sbin/nologin
u backup 34 - /var/backups /usr/sbin/nologin
u list 38 - /var/list /usr/sbin/nologin
u irc 39 - /run/ircd /usr/sbin/nologin
u _apt 42:65534 - /nonexistent /usr/sbin/nologin
u nobody 65534:65534 - /nonexistent /usr/sbin/nologin"#;
if let Err(e) = fs::write(&basic_conf_path, content) {
log::warn!("Failed to write {}: {}", basic_conf_path.display(), e);
} else {
log::debug!("Created missing {}", basic_conf_path.display());
run_in_env(&plan.env_root, "systemd-sysusers", &["basic.conf"]);
}
}
fn run_in_env(env_root: &Path, cmd: &str, args: &[&str])
{
let run_options = RunOptions {
command: cmd.to_string(),
args: args.iter().map(|s| s.to_string()).collect(),
no_exit: true,
chdir_to_env_root: true,
timeout: 30,
..Default::default()
};
if let Err(e) = fork_and_execute(env_root, &run_options) {
log::warn!("Failed to run {}: {}", cmd, e);
}
}
pub fn load_batch_hooks(plan: &mut InstallationPlan) -> Result<()> {
let pkgkeys: Vec<String> = plan.batch.new_pkgkeys.iter().cloned().collect();
for pkgkey in pkgkeys {
load_package_hooks(plan, &pkgkey)?;
}
build_hook_indices(plan);
log::trace!("hooks after batch load: {:#?}", plan.hooks_by_name);
Ok(())
}
fn match_path_trigger(
plan: &InstallationPlan,
trigger: &HookTrigger,
needs_targets: bool,
pkgkey_filter: Option<&str>,
) -> Result<(bool, Vec<String>)> {
if trigger.positive_targets.is_empty() {
return Ok((false, Vec::new()));
}
log::debug!("match_path_trigger: targets={:?}, fresh_installs={}, installed={}, pkgkey_filter={:?}",
trigger.positive_targets, plan.batch.fresh_installs.len(), plan.installed.len(), pkgkey_filter);
let mut matched_targets = Vec::new();
let upgrades_new_set =
collect_matching_files(plan, trigger, needs_targets, pkgkey_filter, &plan.batch.upgrades_new)?;
let upgrades_old_set =
collect_matching_files(plan, trigger, needs_targets, pkgkey_filter, &plan.batch.upgrades_old)?;
let wants_install = trigger.operations.is_set(HookOperation::Install);
if wants_install {
let installed_set =
collect_matching_files(plan, trigger, needs_targets, pkgkey_filter, &plan.installed)?;
let fresh_install_set =
collect_matching_files(plan, trigger, needs_targets, pkgkey_filter, &plan.batch.fresh_installs)?;
matched_targets.extend(installed_set.into_iter());
matched_targets.extend(fresh_install_set.into_iter());
matched_targets.extend(upgrades_new_set.difference(&upgrades_old_set).cloned());
}
let wants_remove = trigger.operations.is_set(HookOperation::Remove);
if wants_remove {
let old_remove_set =
collect_matching_files(plan, trigger, needs_targets, pkgkey_filter, &plan.batch.old_removes)?;
matched_targets.extend(old_remove_set.into_iter());
matched_targets.extend(upgrades_old_set.difference(&upgrades_new_set).cloned());
}
let wants_upgrade = trigger.operations.is_set(HookOperation::Upgrade);
if wants_upgrade {
matched_targets.extend(upgrades_old_set.intersection(&upgrades_new_set).cloned());
}
let matched = !matched_targets.is_empty();
if matched && (
plan.package_format == PackageFormat::Deb ||
plan.package_format == PackageFormat::Apk
) {
let trigger_names: Vec<String> = trigger.positive_targets.clone();
Ok((true, trigger_names))
} else {
Ok((matched, matched_targets))
}
}
fn collect_matching_files(
plan: &InstallationPlan,
trigger: &HookTrigger,
needs_targets: bool,
pkgkey_filter: Option<&str>,
pkgkeys: &HashSet<String>,
) -> Result<HashSet<String>> {
let mut out = HashSet::new();
if let Some(filter) = pkgkey_filter {
if pkgkeys.contains(filter) {
out = collect_matching_files_for_pkg(plan, trigger, needs_targets, filter)?;
}
return Ok(out);
}
for pkgkey in pkgkeys {
let pkg_matches = collect_matching_files_for_pkg(plan, trigger, needs_targets, pkgkey)?;
if !pkg_matches.is_empty() {
if !needs_targets {
return Ok(pkg_matches);
}
out.extend(pkg_matches.into_iter());
}
}
Ok(out)
}
fn collect_matching_files_for_pkg(
plan: &InstallationPlan,
trigger: &HookTrigger,
needs_targets: bool,
pkgkey: &str,
) -> Result<HashSet<String>> {
let store_root = &plan.store_root;
let mut out = HashSet::new();
if let Some(info) = crate::plan::pkgkey2installinfo(plan, pkgkey) {
let files = map_pkgline2filelist(store_root, &info.pkgline)?;
for file in &files {
if matches_patterns(file, trigger, pkgkey, plan) {
out.insert(file.clone());
if !needs_targets {
return Ok(out);
}
}
}
}
Ok(out)
}
fn matches_any_prefix(text: &str, prefixes: &[String]) -> bool {
prefixes.iter().any(|prefix| text.starts_with(prefix))
}
fn matches_any_pattern(text: &str, patterns: &[Pattern]) -> bool {
patterns.iter().any(|pattern| pattern.matches(text))
}
fn matches_any_package(
pkgname: &str,
pkgkey: &str,
trigger: &HookTrigger,
plan: &InstallationPlan,
) -> bool {
if trigger.positive_packages.is_empty() {
return false;
}
if let Some(constraints) = trigger.positive_packages.get(pkgname) {
if !constraints.is_empty() {
if let Ok(pkg_version) = pkgkey2version(pkgkey) {
for constraint in constraints {
match check_version_constraint(&pkg_version, constraint, plan.package_format) {
Ok(true) => {
}
Ok(false) => {
return false;
}
Err(e) => {
log::warn!("Failed to check version constraint for {} {}: {}", pkgname, pkg_version, e);
}
}
}
return true;
} else {
return false;
}
} else {
return true;
}
}
false
}
fn matches_patterns(
text: &str,
trigger: &HookTrigger,
pkgkey: &str,
plan: &InstallationPlan,
) -> bool {
(
matches_any_package(text, pkgkey, trigger, plan) ||
matches_any_prefix(text, &trigger.positive_prefixes) ||
matches_any_pattern(text, &trigger.positive_patterns)
) &&
!matches_any_pattern(text, &trigger.negative_patterns)
}
fn match_package_trigger(
trigger: &HookTrigger,
plan: &InstallationPlan,
pkgkey_filter: Option<&str>,
) -> Result<(bool, Vec<String>)> {
let is_deb_trigger = plan.package_format == PackageFormat::Deb;
if trigger.positive_targets.is_empty() {
return Ok((false, Vec::new()));
}
let wants_install = trigger.operations.is_set(HookOperation::Install);
let wants_upgrade = trigger.operations.is_set(HookOperation::Upgrade);
let wants_remove = trigger.operations.is_set(HookOperation::Remove);
let mut matched_targets = Vec::new();
if wants_install {
let install_pkgs = collect_matching_pkg_names(&plan.batch.fresh_installs, trigger, plan, pkgkey_filter);
matched_targets.extend(install_pkgs);
}
if wants_upgrade {
let upgrade_pkgs = collect_matching_pkg_names(&plan.batch.upgrades_new, trigger, plan, pkgkey_filter);
matched_targets.extend(upgrade_pkgs);
}
if wants_remove {
let remove_pkgs = collect_matching_pkg_names(&plan.batch.old_removes, trigger, plan, pkgkey_filter);
matched_targets.extend(remove_pkgs);
}
let matched = !matched_targets.is_empty();
if is_deb_trigger && matched {
let trigger_names = trigger.positive_targets.clone();
Ok((true, trigger_names))
} else {
Ok((matched, matched_targets))
}
}
fn collect_matching_pkg_names(
pkgkeys: &HashSet<String>,
trigger: &HookTrigger,
plan: &InstallationPlan,
pkgkey_filter: Option<&str>,
) -> Vec<String> {
let mut matched = Vec::new();
if let Some(filter) = pkgkey_filter {
if pkgkeys.contains(filter) {
add_matching_pkgname(filter, trigger, plan, &mut matched);
}
} else {
for pkgkey in pkgkeys {
add_matching_pkgname(pkgkey, trigger, plan, &mut matched);
}
}
matched
}
fn add_matching_pkgname(
pkgkey: &str,
trigger: &HookTrigger,
plan: &InstallationPlan,
matched: &mut Vec<String>,
) {
if plan.package_format == PackageFormat::Deb {
if let Some(trigger_names) = plan.deb_activate_triggers_by_pkg.get(pkgkey) {
for trigger_name in trigger_names {
if matches_patterns(trigger_name, trigger, pkgkey, plan) {
matched.push(trigger_name.clone());
}
}
}
return;
}
if let Ok(pkgname) = pkgkey2pkgname(pkgkey) {
if matches_patterns(&pkgname, trigger, pkgkey, plan) {
matched.push(pkgname);
}
}
}
fn check_dependency(
fresh_installs: &HashSet<String>,
dep: &str,
) -> bool {
fn pkgkey_matches_dep(pkgkey: &str, dep: &str) -> bool {
matches!(pkgkey2pkgname(pkgkey), Ok(pkgname) if pkgname == dep)
}
let installed = PACKAGE_CACHE.installed_packages.read().unwrap();
installed.iter().any(|(pkgkey, _)| pkgkey_matches_dep(pkgkey, dep))
|| fresh_installs.iter().any(|pkgkey| pkgkey_matches_dep(pkgkey, dep))
}
fn count_installed_instances_by_name(pkgname: &str) -> u32 {
let installed = PACKAGE_CACHE.installed_packages.read().unwrap();
let mut count = 0u32;
for (pkgkey, _) in installed.iter() {
if let Ok(name) = pkgkey2pkgname(pkgkey) {
if name == pkgname {
count += 1;
}
}
}
count
}
fn get_triggering_pkgname(
hook: &Hook,
matched_targets: &[String],
_plan: &InstallationPlan,
) -> Option<String> {
if !hook.triggers.is_empty() && hook.triggers[0].hook_type == HookType::Package {
return matched_targets.first().cloned();
}
None
}
fn add_deb_trigger_args(
_hook: &Hook,
matched_targets: &[String],
plan: &InstallationPlan,
args: &mut Vec<String>,
) {
if plan.package_format != PackageFormat::Deb {
return;
}
if !matched_targets.is_empty() {
args.push(matched_targets.join(" "));
}
}
fn add_rpm_trigger_instance_args(
hook: &Hook,
matched_targets: &[String],
plan: &InstallationPlan,
args: &mut Vec<String>,
) {
if plan.package_format != PackageFormat::Rpm {
return;
}
if hook.pkgkey.is_none() {
return;
}
let triggered_pkgname = hook.pkgkey.as_ref()
.and_then(|pkgkey| pkgkey2pkgname(pkgkey).ok());
let triggered_count = if let Some(ref name) = triggered_pkgname {
count_installed_instances_by_name(name)
} else {
0
};
let triggering_pkgname = get_triggering_pkgname(hook, matched_targets, plan);
let triggering_count = if let Some(ref name) = triggering_pkgname {
count_installed_instances_by_name(name)
} else {
triggered_count
};
args.push(triggered_count.to_string());
args.push(triggering_count.to_string());
}
fn check_hook_dependencies(hook: &Hook, plan: &InstallationPlan) -> Result<()> {
for dep in &hook.action.depends {
if !check_dependency(&plan.batch.fresh_installs, dep) {
return Err(color_eyre::eyre::eyre!(
"unable to run hook {}: could not satisfy dependencies",
hook.file_path
));
}
}
Ok(())
}
fn parse_hook_exec(hook: &Hook) -> Result<(String, Vec<String>)> {
let exec_command = if hook.action.exec.contains("%PKGINFO_DIR") {
let pkginfo_dir = std::path::Path::new(&hook.file_path)
.parent()
.and_then(|p| p.parent())
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| {
log::warn!("Could not determine PKGINFO_DIR for hook {}", hook.file_path);
String::new()
});
#[cfg(not(target_os = "linux"))]
let pkginfo_dir = {
let pkginfo_path = std::path::Path::new(&pkginfo_dir);
crate::scriptlets::host_path_to_guest_path(pkginfo_path)
.to_string_lossy()
.to_string()
};
hook.action.exec.replace("%PKGINFO_DIR", &pkginfo_dir)
} else {
hook.action.exec.clone()
};
let exec_parts = match shlex::split(&exec_command) {
Some(parts) => {
if parts.is_empty() {
return Err(color_eyre::eyre::eyre!("Empty Exec in hook {}", hook.file_path));
}
parts
}
None => {
return Err(color_eyre::eyre::eyre!(
"hook {}: invalid Exec value {}",
hook.file_path, exec_command
));
}
};
let command = exec_parts[0].clone();
let args = exec_parts[1..].iter().map(|s| s.to_string()).collect();
Ok((command, args))
}
pub fn execute_hook(
hook: &Hook,
plan: &InstallationPlan,
matched_targets: &[String],
) -> Result<()> {
let env_root = &plan.env_root;
check_hook_dependencies(hook, plan)?;
let (command, mut args) = parse_hook_exec(hook)?;
if plan.package_format == PackageFormat::Deb {
add_deb_trigger_args(hook, matched_targets, plan, &mut args);
} else if plan.package_format == PackageFormat::Rpm {
add_rpm_trigger_instance_args(hook, matched_targets, plan, &mut args);
}
log::info!("Executing hook {}: {} {:?}", hook.file_path, command, args);
println!("Running hook: {}", hook.file_path);
let env_vars = HashMap::new();
let (stdin_data, use_args_for_targets) = if hook.action.needs_targets {
if plan.package_format == PackageFormat::Apk {
(None, true)
} else {
(Some(matched_targets.join("\n").into_bytes()), false)
}
} else {
(None, false)
};
if use_args_for_targets {
args.extend(matched_targets.iter().cloned());
}
let run_options = crate::run::RunOptions {
command,
args,
env_vars,
stdin: stdin_data,
no_exit: !hook.action.abort_on_fail,
chdir_to_env_root: true,
timeout: 300,
..Default::default()
};
match crate::run::fork_and_execute(env_root, &run_options) {
Ok(None) => {
log::debug!("Hook {} executed successfully", hook.file_path);
Ok(())
}
Ok(Some(_)) => {
unreachable!("Foreground process should not return PID")
}
Err(e) => {
let error_msg = format!("{}", e);
if error_msg.contains("VM failed to start") {
log::debug!("Hook {} skipped due to VM startup failure", hook.file_path);
Ok(())
} else if hook.action.abort_on_fail {
Err(e).with_context(|| format!("Hook {} failed and AbortOnFail is set", hook.file_path))
} else {
log::warn!("Hook {} failed: {}", hook.file_path, e);
Ok(())
}
}
}
}
fn check_trigger_match(
trigger: &HookTrigger,
plan: &InstallationPlan,
needs_targets: bool,
pkgkey_filter: Option<&str>,
) -> Result<(bool, Vec<String>)> {
match trigger.hook_type {
HookType::Path => {
match_path_trigger(
plan,
trigger,
needs_targets,
pkgkey_filter,
)
}
HookType::Package => {
let (pmatched, matched_targets) =
match_package_trigger(trigger, plan, pkgkey_filter)?;
if pmatched && needs_targets {
Ok((true, matched_targets))
} else {
Ok((pmatched, Vec::new()))
}
}
}
}
fn sort_triggered_hooks(
triggered_hooks: &mut [(&Arc<Hook>, Vec<String>)],
package_format: PackageFormat,
) {
triggered_hooks.sort_by(|a, b| {
let a_hook = a.0;
let b_hook = b.0;
if package_format == PackageFormat::Rpm {
a_hook.action.priority.cmp(&b_hook.action.priority)
.then_with(|| a_hook.hook_name.cmp(&b_hook.hook_name))
.then_with(|| a_hook.hook_name.len().cmp(&b_hook.hook_name.len()))
} else {
a_hook.hook_name.cmp(&b_hook.hook_name)
.then_with(|| a_hook.hook_name.len().cmp(&b_hook.hook_name.len()))
}
});
}
fn find_triggered_hooks<'a>(
relevant_hooks: &'a [Arc<Hook>],
plan: &InstallationPlan,
pkgkey_filter: Option<&str>,
) -> Result<Vec<(&'a Arc<Hook>, Vec<String>)>> {
let mut triggered_hooks = Vec::new();
log::debug!("find_triggered_hooks: checking {} hooks, fresh_installs={}, installed={}",
relevant_hooks.len(), plan.batch.fresh_installs.len(), plan.installed.len());
for hook in relevant_hooks {
if hook.triggers.is_empty() {
continue;
}
let mut hook_matched = false;
let mut all_matched_targets = Vec::new();
for trigger in &hook.triggers {
let (matched, matched_targets) = check_trigger_match(
trigger,
plan,
hook.action.needs_targets,
pkgkey_filter,
)?;
log::trace!("find_triggered_hooks: hook={}, trigger={:?}, matched={}, targets={:?}",
hook.hook_name, trigger.positive_targets, matched, matched_targets);
if matched {
hook_matched = true;
if hook.action.needs_targets {
all_matched_targets.extend(matched_targets);
} else {
break;
}
}
}
if hook_matched {
all_matched_targets.sort();
all_matched_targets.dedup();
triggered_hooks.push((hook, all_matched_targets));
}
}
sort_triggered_hooks(&mut triggered_hooks, plan.package_format);
Ok(triggered_hooks)
}
fn execute_triggered_hooks(
triggered_hooks: Vec<(&Arc<Hook>, Vec<String>)>,
plan: &InstallationPlan,
when: &HookWhen,
) -> Result<()> {
for (hook, matched_targets) in triggered_hooks {
log::info!("running '{}'...", hook.file_path);
if let Err(e) = execute_hook(
hook.as_ref(),
plan,
&matched_targets,
) {
if hook.action.abort_on_fail {
return Err(e).with_context(|| format!("failed to run transaction hooks"));
}
}
if *when == HookWhen::PreTransaction {
}
}
Ok(())
}
pub fn run_hooks(
plan: &InstallationPlan,
when: HookWhen,
) -> Result<()> {
log::debug!("run_hooks: when={:?}, is_first={}, new_pkgkeys={}, hooks_by_when entries={}",
when, plan.batch.is_first, plan.batch.new_pkgkeys.len(), plan.hooks_by_when.len());
if plan.batch.is_first {
run_trans_hooks(plan, when)?;
} else {
for pkgkey in &plan.batch.new_pkgkeys {
run_pkgkey_hooks_pair(plan, when.clone(), pkgkey)?;
}
}
Ok(())
}
fn run_trans_hooks(
plan: &InstallationPlan,
when: HookWhen,
) -> Result<()> {
let relevant_hooks = match plan.hooks_by_when.get(&when) {
Some(hooks) => hooks,
None => {
log::debug!("run_trans_hooks: no hooks for when={:?}", when);
return Ok(());
}
};
log::debug!("run_trans_hooks: when={:?}, {} hooks available", when, relevant_hooks.len());
let triggered_hooks = find_triggered_hooks(relevant_hooks, plan, None)?;
log::debug!("run_trans_hooks: {} hooks triggered", triggered_hooks.len());
execute_triggered_hooks(triggered_hooks, plan, &when)?;
Ok(())
}
fn filter_hooks_by_when(hooks: &[Arc<Hook>], when: &HookWhen) -> Vec<Arc<Hook>> {
hooks
.iter()
.filter(|h| h.action.when == *when)
.cloned()
.collect()
}
fn run_pkgkey_hooks(
plan: &InstallationPlan,
when: &HookWhen,
pkgkey: &str,
) -> Result<std::collections::HashSet<String>> {
let pkg_hooks = match plan.hooks_by_pkgkey.get(pkgkey) {
Some(h) => h,
None => return Ok(std::collections::HashSet::new()),
};
let relevant_hooks = filter_hooks_by_when(pkg_hooks, when);
let triggered_hooks = find_triggered_hooks(&relevant_hooks, plan, None)?;
let mut executed_hooks = std::collections::HashSet::new();
for (hook, _) in &triggered_hooks {
executed_hooks.insert(hook.hook_name.clone());
}
execute_triggered_hooks(triggered_hooks, plan, when)?;
Ok(executed_hooks)
}
fn run_hooks_on_pkgkey(
plan: &InstallationPlan,
when: &HookWhen,
pkgkey: &str,
executed_hooks: std::collections::HashSet<String>,
) -> Result<()> {
let relevant_hooks = match plan.hooks_by_when.get(&when) {
Some(hooks) => hooks,
None => return Ok(()),
};
let triggered_hooks = find_triggered_hooks(&relevant_hooks, plan, Some(pkgkey))?;
let filtered_hooks: Vec<_> = triggered_hooks
.into_iter()
.filter(|(hook, _)| !executed_hooks.contains(&hook.hook_name))
.collect();
execute_triggered_hooks(filtered_hooks, plan, when)?;
Ok(())
}
pub fn run_pkgkey_hooks_pair(
plan: &InstallationPlan,
when: HookWhen,
pkgkey: &str,
) -> Result<()> {
let executed_hooks = run_pkgkey_hooks(plan, &when, pkgkey)?;
run_hooks_on_pkgkey(plan, &when, pkgkey, executed_hooks)?;
Ok(())
}