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
+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()
}