From b033d64b5b5c9655ecefee9f99da299e887cebfa Mon Sep 17 00:00:00 2001 From: SFG545 Date: Tue, 3 Mar 2026 00:15:20 -0600 Subject: [PATCH] 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 --- Cargo.lock | 6 +- Cargo.toml | 4 +- README.md | 4 + contrib/depot.d/build.toml | 4 + meson.build | 2 +- src/builder/autotools.rs | 11 +- src/builder/mod.rs | 101 +++++++++++++-- src/commands.rs | 255 ++++++++++++++++++++++++++----------- src/install/hooks.rs | 159 +++++++++++++++++++++-- src/package/interactive.rs | 20 +++ src/package/spec.rs | 156 +++++++++++++++++++++++ 11 files changed, 612 insertions(+), 110 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d9cd2ae..0e69adf 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 2f31c60..23fc4da 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md index ef19b34..1ed3da9 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/contrib/depot.d/build.toml b/contrib/depot.d/build.toml index b77f915..fa7fa08 100644 --- a/contrib/depot.d/build.toml +++ b/contrib/depot.d/build.toml @@ -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" diff --git a/meson.build b/meson.build index 53bc0d2..74f9104 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project( 'depot', - version: '0.7.0', + version: '0.8.0', meson_version: '>=0.60.0', ) diff --git a/src/builder/autotools.rs b/src/builder/autotools.rs index 01cbe2f..5e0878d 100755 --- a/src/builder/autotools.rs +++ b/src/builder/autotools.rs @@ -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); } diff --git a/src/builder/mod.rs b/src/builder/mod.rs index 5673957..e743098 100755 --- a/src/builder/mod.rs +++ b/src/builder/mod.rs @@ -28,6 +28,23 @@ pub fn set_env_var(env_vars: &mut EnvVars, key: &str, value: impl Into) } } +fn compiler_flag_sets( + flags: &crate::package::BuildFlags, +) -> (Vec, Vec, Vec, Vec) { + 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()); diff --git a/src/commands.rs b/src/commands.rs index 9b2446a..0372ecc 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -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, + 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 { 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> { + 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 { + 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> { + 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> { - 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 { diff --git a/src/install/hooks.rs b/src/install/hooks.rs index 25790ee..32aec95 100644 --- a/src/install/hooks.rs +++ b/src/install/hooks.rs @@ -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, +} + #[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 = 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 { + 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::>>()?; + + #[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> { 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 { + 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"); + } } diff --git a/src/package/interactive.rs b/src/package/interactive.rs index 4e7f508..6740a2b 100644 --- a/src/package/interactive.rs +++ b/src/package/interactive.rs @@ -880,6 +880,19 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result { ), ); } + 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 { ), ); } + 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")); diff --git a/src/package/spec.rs b/src/package/spec.rs index 84b1a32..166b996 100755 --- a/src/package/spec.rs +++ b/src/package/spec.rs @@ -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, #[serde(default, deserialize_with = "deserialize_string_or_array")] pub ldflags: Vec, + /// 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, /// Keep existing files and install package-provided replacement as `.depotnew`. #[serde(default, deserialize_with = "deserialize_string_or_array")] pub keep: Vec, + /// 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() }