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
Generated
+3 -3
View File
@@ -382,7 +382,7 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2"
[[package]]
name = "depot"
version = "0.7.0"
version = "0.8.0"
dependencies = [
"anyhow",
"ar",
@@ -2995,9 +2995,9 @@ dependencies = [
[[package]]
name = "zip"
version = "8.1.0"
version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e499faf5c6b97a0d086f4a8733de6d47aee2252b8127962439d8d4311a73f72"
checksum = "b680f2a0cd479b4cff6e1233c483fdead418106eae419dc60200ae9850f6d004"
dependencies = [
"aes",
"bzip2",
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "depot"
version = "0.7.0"
version = "0.8.0"
edition = "2024"
[lints.rust]
@@ -27,7 +27,7 @@ toml = "0.9.8"
url = "2.5.8"
walkdir = "2.5.0"
xz2 = "0.1.7"
zip = "8.1.0"
zip = "8.2.0"
zstd = { version = "0.13.3", features = ["zstdmt"] }
inquire = "0.9.4"
md5 = "0.8.0"
+4
View File
@@ -85,6 +85,10 @@ runtime = ["libc"]
optional = ["bash-completion"]
```
LTO controls are available via `build.flags`:
- `ltoflags`: exported as `LTOFLAGS`
- `use_lto`: defaults to `true`; when enabled, `ltoflags` are appended to `CFLAGS`, `CXXFLAGS`, and `LDFLAGS`
## Configuration
Depot can be configured via `/etc/depot.d/` (or relative to the rootfs).
+4
View File
@@ -10,6 +10,10 @@ cflags += ["-pipe"]
# Default LDFLAGS
ldflags = ["-Wl,-O1"]
# Default LTOFLAGS and automatic injection into CFLAGS/CXXFLAGS/LDFLAGS.
ltoflags = ["-flto=auto"]
use_lto = true
# CARCH short name (can be overridden per-package)
carch = "x86_64"
+1 -1
View File
@@ -1,6 +1,6 @@
project(
'depot',
version: '0.7.0',
version: '0.8.0',
meson_version: '>=0.60.0',
)
+8 -3
View File
@@ -32,9 +32,14 @@ pub fn build(
flags.cc.clone()
};
if export_compiler_flags && !flags.cflags.is_empty() {
// Expand shell command substitutions like $($CC -print-resource-dir)
let cflags_str = flags.cflags.join(" ");
if export_compiler_flags
&& let Some(cflags_str) = env_vars
.iter()
.find(|(key, _)| key == "CFLAGS")
.map(|(_, value)| value.clone())
&& !cflags_str.trim().is_empty()
{
// Expand shell command substitutions like $($CC -print-resource-dir).
let expanded = expand_shell_commands(&cflags_str, &cc)?;
crate::builder::set_env_var(&mut env_vars, "CFLAGS", expanded);
}
+89 -12
View File
@@ -28,6 +28,23 @@ pub fn set_env_var(env_vars: &mut EnvVars, key: &str, value: impl Into<String>)
}
}
fn compiler_flag_sets(
flags: &crate::package::BuildFlags,
) -> (Vec<String>, Vec<String>, Vec<String>, Vec<String>) {
let mut cflags = flags.cflags.clone();
let mut cxxflags = flags.cxxflags.clone();
let mut ldflags = flags.ldflags.clone();
let ltoflags = flags.ltoflags.clone();
if flags.use_lto && !ltoflags.is_empty() {
cflags.extend(ltoflags.iter().cloned());
cxxflags.extend(ltoflags.iter().cloned());
ldflags.extend(ltoflags.iter().cloned());
}
(cflags, cxxflags, ldflags, ltoflags)
}
pub fn standard_build_env(
spec: &PackageSpec,
cross: Option<&CrossConfig>,
@@ -39,24 +56,25 @@ pub fn standard_build_env(
let export_compiler_flags = export_compiler_flags && !flags.no_flags;
if include_compiler_env && export_compiler_flags {
if !flags.cflags.is_empty() {
set_env_var(&mut env_vars, "CFLAGS", flags.cflags.join(" "));
let (cflags, cxxflags, ldflags, ltoflags) = compiler_flag_sets(flags);
if !cflags.is_empty() {
set_env_var(&mut env_vars, "CFLAGS", cflags.join(" "));
}
if !flags.cxxflags.is_empty() {
set_env_var(&mut env_vars, "CXXFLAGS", flags.cxxflags.join(" "));
if !cxxflags.is_empty() {
set_env_var(&mut env_vars, "CXXFLAGS", cxxflags.join(" "));
}
if !ltoflags.is_empty() {
set_env_var(&mut env_vars, "LTOFLAGS", ltoflags.join(" "));
}
let ldflags = if !flags.ldflags.is_empty() || !flags.libc.is_empty() {
let ldflags = if !ldflags.is_empty() || !flags.libc.is_empty() {
if flags.libc.is_empty() {
flags.ldflags.join(" ")
} else if flags.ldflags.is_empty() {
ldflags.join(" ")
} else if ldflags.is_empty() {
format!("-Wl,--dynamic-linker={}", flags.libc)
} else {
format!(
"{} -Wl,--dynamic-linker={}",
flags.ldflags.join(" "),
flags.libc
)
format!("{} -Wl,--dynamic-linker={}", ldflags.join(" "), flags.libc)
}
} else {
String::new()
@@ -424,6 +442,65 @@ mod tests {
);
}
#[test]
fn test_standard_build_env_injects_ltoflags_into_compiler_and_linker_flags() {
let mut spec = mk_spec(vec!["-O2"], vec!["-Wl,--as-needed"]);
spec.build.flags.cxxflags = vec!["-O2".into()];
spec.build.flags.ltoflags = vec!["-flto=auto".into(), "-fuse-linker-plugin".into()];
spec.build.flags.use_lto = true;
let env = standard_build_env(&spec, None, true, true);
assert!(
env.iter()
.any(|(k, v)| { k == "CFLAGS" && v == "-O2 -flto=auto -fuse-linker-plugin" }),
"expected LTOFLAGS to be appended to CFLAGS"
);
assert!(
env.iter()
.any(|(k, v)| k == "CXXFLAGS" && v == "-O2 -flto=auto -fuse-linker-plugin"),
"expected LTOFLAGS to be appended to CXXFLAGS"
);
assert!(
env.iter().any(|(k, v)| {
k == "LDFLAGS" && v == "-Wl,--as-needed -flto=auto -fuse-linker-plugin"
}),
"expected LTOFLAGS to be appended to LDFLAGS"
);
assert!(
env.iter()
.any(|(k, v)| k == "LTOFLAGS" && v == "-flto=auto -fuse-linker-plugin"),
"expected LTOFLAGS variable to be exported"
);
}
#[test]
fn test_standard_build_env_skips_lto_injection_when_disabled() {
let mut spec = mk_spec(vec!["-O2"], vec!["-Wl,--as-needed"]);
spec.build.flags.cxxflags = vec!["-O2".into()];
spec.build.flags.ltoflags = vec!["-flto=auto".into()];
spec.build.flags.use_lto = false;
let env = standard_build_env(&spec, None, true, true);
assert!(
env.iter().any(|(k, v)| k == "CFLAGS" && v == "-O2"),
"expected CFLAGS to remain unchanged when use_lto is false"
);
assert!(
env.iter().any(|(k, v)| k == "CXXFLAGS" && v == "-O2"),
"expected CXXFLAGS to remain unchanged when use_lto is false"
);
assert!(
env.iter()
.any(|(k, v)| k == "LDFLAGS" && v == "-Wl,--as-needed"),
"expected LDFLAGS to remain unchanged when use_lto is false"
);
assert!(
env.iter()
.any(|(k, v)| k == "LTOFLAGS" && v == "-flto=auto"),
"expected LTOFLAGS variable to be exported even when use_lto is false"
);
}
#[test]
fn test_standard_build_env_exports_makeflags() {
let mut spec = mk_spec(Vec::new(), Vec::new());
+180 -75
View File
@@ -491,12 +491,25 @@ fn build_lib32_companion_package(
Ok(Some((lib32_pkg_spec, lib32_destdir)))
}
fn install_staged_to_rootfs(
#[derive(Debug, Clone)]
struct PlannedStagedInstall {
is_update: bool,
remove_paths: Vec<String>,
hook_context: install::hooks::HookExecutionContextOwned,
}
#[derive(Debug, Clone)]
struct PlannedPackageInstall {
spec: package::PackageSpec,
destdir: PathBuf,
staged: PlannedStagedInstall,
}
fn plan_staged_install(
pkg_spec: &package::PackageSpec,
destdir: &Path,
rootfs: &Path,
config: &config::Config,
) -> Result<()> {
) -> Result<PlannedStagedInstall> {
std::fs::create_dir_all(&config.db_dir).with_context(|| {
format!(
"Failed to create database directory: {}",
@@ -506,10 +519,6 @@ fn install_staged_to_rootfs(
let db_path = config.db_dir.join("packages.db");
let is_update = db::get_package_version(&db_path, &pkg_spec.package.name)?.is_some();
let staged_scripts_dir = install::scripts::staged_scripts_dir(destdir);
let installed_scripts_dir =
install::scripts::installed_scripts_dir(rootfs, &pkg_spec.package.name);
let new_files = staging::generate_manifest_with_dirs(destdir)?;
let remove_paths =
db::calculate_upgrade_paths(&db_path, &pkg_spec.package.name, &new_files.files)?;
@@ -523,17 +532,64 @@ fn install_staged_to_rootfs(
affected_paths.sort();
affected_paths.dedup();
install::hooks::run_transaction_hooks(
rootfs,
&install::hooks::HookExecutionContext {
phase: install::hooks::HookPhase::Pre,
Ok(PlannedStagedInstall {
is_update,
remove_paths,
hook_context: install::hooks::HookExecutionContextOwned {
operation,
package: &pkg_spec.package.name,
affected_paths: &affected_paths,
package: pkg_spec.package.name.clone(),
affected_paths,
},
)?;
})
}
if is_update {
fn plan_package_outputs_for_install(
pkg_spec: &package::PackageSpec,
destdir: &Path,
config: &config::Config,
) -> Result<Vec<PlannedPackageInstall>> {
let mut plans = Vec::new();
for out in pkg_spec.outputs() {
let mut spec_for_out = pkg_spec.clone();
let output_name = out.name.clone();
spec_for_out.package = out;
spec_for_out.alternatives = pkg_spec.alternatives_for_output(&output_name);
spec_for_out.dependencies = pkg_spec.dependencies_for_output(&output_name);
let out_destdir = output_destdir_for(destdir, &pkg_spec.package.name, &output_name);
let staged = plan_staged_install(&spec_for_out, &out_destdir, config)?;
plans.push(PlannedPackageInstall {
spec: spec_for_out,
destdir: out_destdir,
staged,
});
}
Ok(plans)
}
fn run_transaction_hooks_for_plans(
rootfs: &Path,
phase: install::hooks::HookPhase,
plans: &[PlannedPackageInstall],
) -> Result<usize> {
let contexts: Vec<_> = plans
.iter()
.map(|plan| plan.staged.hook_context.clone())
.collect();
install::hooks::run_transaction_hooks_batch(rootfs, phase, &contexts)
}
fn install_staged_to_rootfs(
pkg_spec: &package::PackageSpec,
destdir: &Path,
rootfs: &Path,
config: &config::Config,
plan: &PlannedStagedInstall,
) -> Result<()> {
let staged_scripts_dir = install::scripts::staged_scripts_dir(destdir);
let installed_scripts_dir =
install::scripts::installed_scripts_dir(rootfs, &pkg_spec.package.name);
if plan.is_update {
let has_staged_pre = install::scripts::run_hook_if_present(
&staged_scripts_dir,
install::scripts::Hook::PreUpdate,
@@ -562,10 +618,11 @@ fn install_staged_to_rootfs(
destdir,
rootfs,
&tx_base,
&remove_paths,
&plan.remove_paths,
&pkg_spec.build.flags.keep,
)?;
let db_path = config.db_dir.join("packages.db");
if let Err(e) = db::register_package(&db_path, pkg_spec, destdir) {
let _ = tx.rollback();
return Err(e);
@@ -578,7 +635,7 @@ fn install_staged_to_rootfs(
&pkg_spec.package.name,
)?;
if is_update {
if plan.is_update {
let _ = install::scripts::run_hook_if_present(
&installed_scripts_dir,
install::scripts::Hook::PostUpdate,
@@ -594,36 +651,33 @@ fn install_staged_to_rootfs(
)?;
}
install::hooks::run_transaction_hooks(
rootfs,
&install::hooks::HookExecutionContext {
phase: install::hooks::HookPhase::Post,
operation,
package: &pkg_spec.package.name,
affected_paths: &affected_paths,
},
)?;
Ok(())
}
fn install_planned_packages_to_rootfs(
plans: &[PlannedPackageInstall],
rootfs: &Path,
config: &config::Config,
) -> Result<Vec<package::PackageInfo>> {
let mut installed = Vec::with_capacity(plans.len());
for plan in plans {
install_staged_to_rootfs(&plan.spec, &plan.destdir, rootfs, config, &plan.staged)?;
installed.push(plan.spec.package.clone());
}
Ok(installed)
}
#[cfg(test)]
fn install_package_outputs_to_rootfs(
pkg_spec: &package::PackageSpec,
destdir: &Path,
rootfs: &Path,
config: &config::Config,
) -> Result<Vec<package::PackageInfo>> {
let mut installed = Vec::new();
for out in pkg_spec.outputs() {
let mut spec_for_out = pkg_spec.clone();
let output_name = out.name.clone();
spec_for_out.package = out;
spec_for_out.alternatives = pkg_spec.alternatives_for_output(&output_name);
spec_for_out.dependencies = pkg_spec.dependencies_for_output(&output_name);
let out_destdir = output_destdir_for(destdir, &pkg_spec.package.name, &output_name);
install_staged_to_rootfs(&spec_for_out, &out_destdir, rootfs, config)?;
installed.push(spec_for_out.package);
}
let plans = plan_package_outputs_for_install(pkg_spec, destdir, config)?;
run_transaction_hooks_for_plans(rootfs, install::hooks::HookPhase::Pre, &plans)?;
let installed = install_planned_packages_to_rootfs(&plans, rootfs, config)?;
run_transaction_hooks_for_plans(rootfs, install::hooks::HookPhase::Post, &plans)?;
Ok(installed)
}
@@ -1196,8 +1250,32 @@ fn execute_install_plan_with_child_commands(
signature_pb.finish_and_clear();
}
let mut binary_pre_hook_plans = Vec::new();
for step in &actionable_steps {
if let planner::PlanOrigin::Binary { repo_name, record } = &step.origin {
let cached = binary_archives
.get(&(repo_name.clone(), record.filename.clone()))
.with_context(|| {
format!(
"Cached archive missing for planned binary step '{}' from repo '{}'",
record.filename, repo_name
)
})?;
let staged = extract_package_archive_to_staging(&cached.package_path)?;
let spec = package_spec_from_repo_record(record);
let plans = plan_package_outputs_for_install(&spec, staged.path(), config)?;
binary_pre_hook_plans.extend(plans);
}
}
run_transaction_hooks_for_plans(
rootfs,
install::hooks::HookPhase::Pre,
&binary_pre_hook_plans,
)?;
let exe = std::env::current_exe().context("Failed to locate depot executable")?;
let total_steps = actionable_steps.len();
let mut binary_post_hook_plans = Vec::new();
for (idx, step) in actionable_steps.into_iter().enumerate() {
match &step.origin {
planner::PlanOrigin::Source { path, .. } => {
@@ -1241,8 +1319,9 @@ fn execute_install_plan_with_child_commands(
})?;
let staged = extract_package_archive_to_staging(&cached.package_path)?;
let spec = package_spec_from_repo_record(record);
let installed =
install_package_outputs_to_rootfs(&spec, staged.path(), rootfs, config)?;
let plans = plan_package_outputs_for_install(&spec, staged.path(), config)?;
let installed = install_planned_packages_to_rootfs(&plans, rootfs, config)?;
binary_post_hook_plans.extend(plans);
for pkg in installed {
ui::success(format!("Installed {} v{}", pkg.name, pkg.version));
}
@@ -1251,6 +1330,11 @@ fn execute_install_plan_with_child_commands(
}
}
run_transaction_hooks_for_plans(
rootfs,
install::hooks::HookPhase::Post,
&binary_post_hook_plans,
)?;
install::scripts::run_deferred_hooks_if_possible(rootfs)?;
Ok(())
}
@@ -1528,6 +1612,8 @@ fn run_direct_install_request(
destdir
};
let mut transaction_plans = Vec::new();
if !options.lib32_only {
if staging_dir.is_none() {
// Source-build path: apply staging transforms (strip/compress/static cleanup).
@@ -1537,15 +1623,8 @@ fn run_direct_install_request(
ui::info("Installing binary archive payload without staging transforms");
}
let installed =
install_package_outputs_to_rootfs(&pkg_spec, &destdir, options.rootfs, config)?;
for pkg in installed {
ui::success(format!(
"Successfully installed {} v{}",
pkg.name, pkg.version
));
// TODO(snapper): create post-install snapshot after install commit succeeds.
}
let output_plans = plan_package_outputs_for_install(&pkg_spec, &destdir, config)?;
transaction_plans.extend(output_plans);
}
if let Some(src_dir) = built_src_dir.as_deref()
@@ -1558,12 +1637,32 @@ fn run_direct_install_request(
options.lib32_only,
)?
{
install_staged_to_rootfs(&lib32_spec, &lib32_destdir, options.rootfs, config)?;
let staged = plan_staged_install(&lib32_spec, &lib32_destdir, config)?;
transaction_plans.push(PlannedPackageInstall {
spec: lib32_spec,
destdir: lib32_destdir,
staged,
});
}
run_transaction_hooks_for_plans(
options.rootfs,
install::hooks::HookPhase::Pre,
&transaction_plans,
)?;
let installed = install_planned_packages_to_rootfs(&transaction_plans, options.rootfs, config)?;
for pkg in installed {
ui::success(format!(
"Successfully installed {} v{}",
lib32_spec.package.name, lib32_spec.package.version
pkg.name, pkg.version
));
// TODO(snapper): create post-install snapshot after install commit succeeds.
}
run_transaction_hooks_for_plans(
options.rootfs,
install::hooks::HookPhase::Post,
&transaction_plans,
)?;
Ok(true)
}
@@ -1925,37 +2024,43 @@ pub fn run(cli: Cli) -> Result<()> {
));
}
if ui::prompt_package_action("installation", &install_targets, false)? {
let mut transaction_plans = Vec::new();
if !cli.lib32_only {
for out in pkg_spec.outputs() {
let mut spec_for_out = pkg_spec.clone();
let output_name = out.name.clone();
spec_for_out.package = out;
spec_for_out.alternatives =
pkg_spec.alternatives_for_output(&output_name);
spec_for_out.dependencies =
pkg_spec.dependencies_for_output(&output_name);
let out_destdir =
output_destdir_for(&destdir, &pkg_spec.package.name, &output_name);
install_staged_to_rootfs(
&spec_for_out,
&out_destdir,
&cli.rootfs,
&config,
)?;
ui::success(format!(
"Successfully installed {} v{}",
spec_for_out.package.name, spec_for_out.package.version
));
// TODO(snapper): create post-install snapshot after --install commit succeeds.
}
let output_plans =
plan_package_outputs_for_install(&pkg_spec, &destdir, &config)?;
transaction_plans.extend(output_plans);
}
if let Some((lib32_spec, lib32_destdir)) = &lib32_install_bundle {
install_staged_to_rootfs(lib32_spec, lib32_destdir, &cli.rootfs, &config)?;
let staged = plan_staged_install(lib32_spec, lib32_destdir, &config)?;
transaction_plans.push(PlannedPackageInstall {
spec: lib32_spec.clone(),
destdir: lib32_destdir.clone(),
staged,
});
}
run_transaction_hooks_for_plans(
&cli.rootfs,
install::hooks::HookPhase::Pre,
&transaction_plans,
)?;
let installed = install_planned_packages_to_rootfs(
&transaction_plans,
&cli.rootfs,
&config,
)?;
for pkg in installed {
ui::success(format!(
"Successfully installed {} v{}",
lib32_spec.package.name, lib32_spec.package.version
pkg.name, pkg.version
));
// TODO(snapper): create post-install snapshot after --install commit succeeds.
}
run_transaction_hooks_for_plans(
&cli.rootfs,
install::hooks::HookPhase::Post,
&transaction_plans,
)?;
install::scripts::run_deferred_hooks_if_possible(&cli.rootfs)?;
} else {
+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");
}
}
+20
View File
@@ -880,6 +880,19 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
),
);
}
if !spec.build.flags.ltoflags.is_empty() {
flags_tbl.insert(
"ltoflags".into(),
Value::Array(
spec.build
.flags
.ltoflags
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !spec.build.flags.keep.is_empty() {
flags_tbl.insert(
"keep".into(),
@@ -893,6 +906,9 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
),
);
}
if spec.build.flags.use_lto != defaults.use_lto {
flags_tbl.insert("use_lto".into(), Value::Boolean(spec.build.flags.use_lto));
}
if spec.build.flags.no_flags != defaults.no_flags {
flags_tbl.insert("no_flags".into(), Value::Boolean(spec.build.flags.no_flags));
}
@@ -1474,8 +1490,10 @@ mod tests {
flags.config_settings = vec!["editable_mode=compat".into()];
flags.rustflags = vec!["-Ctarget-cpu=native".into()];
flags.cxxflags = vec!["-O2".into(), "-fno-rtti".into()];
flags.ltoflags = vec!["-flto=auto".into()];
flags.target = "x86_64-unknown-linux-gnu".into();
flags.keep = vec!["etc/locale.gen".into()];
flags.use_lto = false;
flags.no_flags = true;
flags.no_strip = true;
flags.no_delete_static = true;
@@ -1532,9 +1550,11 @@ mod tests {
assert!(toml.contains("config_setting = ["));
assert!(toml.contains("rustflags = ["));
assert!(toml.contains("cxxflags = ["));
assert!(toml.contains("ltoflags = ["));
assert!(toml.contains("target = \"x86_64-unknown-linux-gnu\""));
assert!(toml.contains("keep = ["));
assert!(toml.contains("\"etc/locale.gen\""));
assert!(toml.contains("use_lto = false"));
assert!(toml.contains("no_flags = true"));
assert!(toml.contains("no_strip = true"));
assert!(toml.contains("no_delete_static = true"));
+156
View File
@@ -300,6 +300,17 @@ impl PackageSpec {
self.build.flags.ldflags = vec![s.to_string()];
}
}
"ltoflags" | "lto_flags" | "lto-flags" => {
if let Some(arr) = v.as_array() {
self.build.flags.ltoflags = arr
.iter()
.filter_map(|x| x.as_str())
.map(String::from)
.collect();
} else if let Some(s) = v.as_str() {
self.build.flags.ltoflags = vec![s.to_string()];
}
}
"keep" => {
if let Some(arr) = v.as_array() {
self.build.flags.keep = arr
@@ -505,6 +516,11 @@ impl PackageSpec {
self.build.flags.no_flags = b;
}
}
"use_lto" | "use-lto" => {
if let Some(b) = toml_value_as_boolish(v) {
self.build.flags.use_lto = b;
}
}
"no_strip" | "no-strip" => {
if let Some(b) = v.as_bool() {
self.build.flags.no_strip = b;
@@ -694,6 +710,18 @@ impl PackageSpec {
}
}
}
"ltoflags" | "lto_flags" | "lto-flags" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.ltoflags
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build.flags.ltoflags.push(s.to_string());
}
}
}
"keep" => {
for v in values {
if let Some(arr) = v.as_array() {
@@ -1065,6 +1093,11 @@ impl PackageSpec {
self.build.flags.no_flags = b;
}
}
"use_lto" | "use-lto" => {
if let Some(b) = values.last().and_then(toml_value_as_boolish) {
self.build.flags.use_lto = b;
}
}
"no_strip" | "no-strip" => {
if let Some(b) = values.last().and_then(|v| v.as_bool()) {
self.build.flags.no_strip = b;
@@ -1686,6 +1719,8 @@ make_vars = ["V=1"]
make_dirs = ["lib"]
make_test_dirs = ["tests"]
make_install_dirs = ["lib"]
ltoflags = ["-flto=auto"]
use_lto = true
no_flags = true
no_strip = true
no_delete_static = true
@@ -1719,6 +1754,16 @@ post_configure = ["echo configured"]
"etc/locale.gen".to_string(),
)])],
);
config.appends.insert(
"build.flags.ltoflags".to_string(),
vec![toml::Value::Array(vec![toml::Value::String(
"-fno-fat-lto-objects".to_string(),
)])],
);
config.appends.insert(
"build.flags.use_lto".to_string(),
vec![toml::Value::Boolean(false)],
);
config.appends.insert(
"build.flags.no_strip".to_string(),
vec![toml::Value::Boolean(false)],
@@ -1794,6 +1839,19 @@ post_configure = ["echo configured"]
.rustflags
.contains(&"opt-level=3".to_string())
);
assert!(
spec.build
.flags
.ltoflags
.contains(&"-flto=auto".to_string())
);
assert!(
spec.build
.flags
.ltoflags
.contains(&"-fno-fat-lto-objects".to_string())
);
assert!(!spec.build.flags.use_lto);
assert!(spec.build.flags.no_flags);
assert!(!spec.build.flags.no_strip);
assert!(!spec.build.flags.no_delete_static);
@@ -1917,6 +1975,79 @@ no_flags = true
assert!(spec.build.flags.no_flags);
}
#[test]
fn parse_ltoflags_and_use_lto_from_spec() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "custom"
[build.flags]
ltoflags = ["-flto=auto", "-fuse-linker-plugin"]
use_lto = false
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(
spec.build.flags.ltoflags,
vec!["-flto=auto".to_string(), "-fuse-linker-plugin".to_string()]
);
assert!(!spec.build.flags.use_lto);
}
#[test]
fn parse_ltoflags_and_use_lto_aliases_from_spec() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "custom"
[build.flags]
LTOFLAGS = "-flto=auto"
"use-lto" = false
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(spec.build.flags.ltoflags, vec!["-flto=auto".to_string()]);
assert!(!spec.build.flags.use_lto);
}
#[test]
fn parse_no_strip_no_delete_static_and_no_compress_man_from_spec() {
let tmp = tempfile::tempdir().unwrap();
@@ -2800,9 +2931,28 @@ pub struct BuildFlags {
pub cxxflags_lib32: Vec<String>,
#[serde(default, deserialize_with = "deserialize_string_or_array")]
pub ldflags: Vec<String>,
/// Link-time optimization flags exported to `LTOFLAGS`.
///
/// When `use_lto` is true (default), these flags are also appended to
/// `CFLAGS`, `CXXFLAGS`, and `LDFLAGS`.
#[serde(
default,
alias = "lto-flags",
alias = "lto_flags",
alias = "LTOFLAGS",
deserialize_with = "deserialize_string_or_array"
)]
pub ltoflags: Vec<String>,
/// Keep existing files and install package-provided replacement as `<path>.depotnew`.
#[serde(default, deserialize_with = "deserialize_string_or_array")]
pub keep: Vec<String>,
/// Disable automatic LTOFLAGS injection into CFLAGS/CXXFLAGS/LDFLAGS.
#[serde(
default = "default_use_lto",
alias = "use-lto",
deserialize_with = "deserialize_boolish"
)]
pub use_lto: bool,
/// Disable exporting CFLAGS/CXXFLAGS/LDFLAGS for this package build.
#[serde(default, alias = "no-flags")]
pub no_flags: bool,
@@ -3071,7 +3221,9 @@ impl Default for BuildFlags {
cxxflags: Vec::new(),
cxxflags_lib32: Vec::new(),
ldflags: Vec::new(),
ltoflags: Vec::new(),
keep: Vec::new(),
use_lto: default_use_lto(),
no_flags: false,
no_strip: false,
no_delete_static: false,
@@ -3256,6 +3408,10 @@ fn default_cc() -> String {
"gcc".to_string()
}
fn default_use_lto() -> bool {
true
}
fn default_ar() -> String {
"ar".to_string()
}