feat: run transaction hooks at boundaries and add configurable LTO flags

- execute transaction pre hooks at transaction start and post hooks at transaction end
- add batched hook execution with ordered "(n/total)" logging
- refactor install flows to plan package actions first, then run hooks around commits
- add `build.flags.ltoflags` and `build.flags.use_lto` (default: true)
- export `LTOFLAGS` and append LTO flags to `CFLAGS`, `CXXFLAGS`, and `LDFLAGS`
  unless `use_lto = false`
- make autotools CFLAGS expansion use the final computed CFLAGS value
- update spec parsing/appends, interactive TOML output, docs, and contrib build config
- bump project version to 0.8.0 and update `zip` to 8.2.0
This commit is contained in:
2026-03-03 00:15:20 -06:00
parent 6cae1f636a
commit b033d64b5b
11 changed files with 612 additions and 110 deletions
+145 -14
View File
@@ -104,6 +104,17 @@ pub struct HookExecutionContext<'a> {
pub affected_paths: &'a [String],
}
/// Owned transaction hook context used for batched execution.
#[derive(Debug, Clone)]
pub struct HookExecutionContextOwned {
/// Current operation (`install`, `update`, `remove`).
pub operation: HookOperation,
/// Package being processed.
pub package: String,
/// Filesystem paths affected by this package action.
pub affected_paths: Vec<String>,
}
#[derive(Debug, Clone)]
struct TransactionHook {
file_path: PathBuf,
@@ -195,11 +206,7 @@ pub fn run_transaction_hooks(rootfs: &Path, ctx: &HookExecutionContext<'_>) -> R
return Ok(0);
}
let normalized_paths: Vec<String> = ctx
.affected_paths
.iter()
.map(|p| normalize_match_target(p))
.collect();
let normalized_paths = normalize_affected_paths(ctx.affected_paths);
let mut executed = 0usize;
for hook_file in hook_files {
@@ -208,13 +215,83 @@ pub fn run_transaction_hooks(rootfs: &Path, ctx: &HookExecutionContext<'_>) -> R
continue;
}
run_hook_command(rootfs, &hook, ctx, &normalized_paths)?;
run_hook_command(rootfs, &hook, ctx, &normalized_paths, None)?;
executed += 1;
}
Ok(executed)
}
/// Load and execute hooks for a batch of transaction contexts in stable order.
///
/// Hooks are discovered and parsed once, then matched against each context in
/// input order. Matching hooks run in hook-file order for each context.
///
/// Returns the number of hook commands executed.
pub fn run_transaction_hooks_batch(
rootfs: &Path,
phase: HookPhase,
contexts: &[HookExecutionContextOwned],
) -> Result<usize> {
if contexts.is_empty() {
return Ok(0);
}
let hook_dir = transaction_hooks_dir(rootfs);
let hook_files = discover_hook_files(&hook_dir)?;
if hook_files.is_empty() {
return Ok(0);
}
let hooks = hook_files
.iter()
.map(|hook_file| parse_hook_file(hook_file))
.collect::<Result<Vec<_>>>()?;
#[derive(Clone, Copy)]
struct ScheduledHookRun {
hook_idx: usize,
ctx_idx: usize,
}
let mut scheduled = Vec::new();
for (ctx_idx, ctx_owned) in contexts.iter().enumerate() {
let normalized_paths = normalize_affected_paths(&ctx_owned.affected_paths);
let ctx = HookExecutionContext {
phase,
operation: ctx_owned.operation,
package: &ctx_owned.package,
affected_paths: &ctx_owned.affected_paths,
};
for (hook_idx, hook) in hooks.iter().enumerate() {
if hook.matches(&ctx, &normalized_paths) {
scheduled.push(ScheduledHookRun { hook_idx, ctx_idx });
}
}
}
let total = scheduled.len();
for (run_idx, run) in scheduled.into_iter().enumerate() {
let ctx_owned = &contexts[run.ctx_idx];
let normalized_paths = normalize_affected_paths(&ctx_owned.affected_paths);
let ctx = HookExecutionContext {
phase,
operation: ctx_owned.operation,
package: &ctx_owned.package,
affected_paths: &ctx_owned.affected_paths,
};
run_hook_command(
rootfs,
&hooks[run.hook_idx],
&ctx,
&normalized_paths,
Some((run_idx + 1, total)),
)?;
}
Ok(total)
}
fn discover_hook_files(hook_dir: &Path) -> Result<Vec<PathBuf>> {
if !hook_dir.exists() {
return Ok(Vec::new());
@@ -392,16 +469,28 @@ fn run_hook_command(
hook: &TransactionHook,
ctx: &HookExecutionContext<'_>,
normalized_paths: &[String],
sequence: Option<(usize, usize)>,
) -> Result<()> {
let hook_name = hook.display_name();
crate::log_info!(
"Running transaction hook '{}' ({}) for {}:{}:{}",
hook_name,
hook.file_path.display(),
ctx.operation.as_str(),
ctx.phase.as_str(),
ctx.package
);
if let Some((index, total)) = sequence {
crate::log_info!(
"Running transaction hook ({}/{}) '{}' for {}:{}:{}",
index,
total,
hook_name,
ctx.operation.as_str(),
ctx.phase.as_str(),
ctx.package
);
} else {
crate::log_info!(
"Running transaction hook '{}' for {}:{}:{}",
hook_name,
ctx.operation.as_str(),
ctx.phase.as_str(),
ctx.package
);
}
let stdin_payload = if hook.needs_paths {
let mut payload = normalized_paths.join("\n");
@@ -468,6 +557,10 @@ fn run_command_with_optional_stdin(
}
}
fn normalize_affected_paths(paths: &[String]) -> Vec<String> {
paths.iter().map(|p| normalize_match_target(p)).collect()
}
fn normalize_match_target(raw: &str) -> String {
raw.trim()
.trim_start_matches("./")
@@ -613,4 +706,42 @@ command = "touch \"$DEPOT_ROOTFS/should_not_exist\""
assert_eq!(ran, 0);
assert!(!tmp.path().join("should_not_exist").exists());
}
#[test]
fn run_transaction_hooks_batch_executes_in_context_order() {
let tmp = tempfile::tempdir().unwrap();
write_hook(
tmp.path(),
"batch.toml",
r#"
[hook]
name = "batch"
[when]
phase = "post"
operation = ["install"]
[exec]
command = "printf '%s\n' \"$DEPOT_PACKAGE\" >> \"$DEPOT_ROOTFS/batch.out\""
"#,
);
let contexts = vec![
HookExecutionContextOwned {
operation: HookOperation::Install,
package: "foo".to_string(),
affected_paths: Vec::new(),
},
HookExecutionContextOwned {
operation: HookOperation::Install,
package: "bar".to_string(),
affected_paths: Vec::new(),
},
];
let ran = run_transaction_hooks_batch(tmp.path(), HookPhase::Post, &contexts).unwrap();
assert_eq!(ran, 2);
let out = std::fs::read_to_string(tmp.path().join("batch.out")).unwrap();
assert_eq!(out, "foo\nbar\n");
}
}