From e843cd73c04b96a53d7043e609bebc7e97cef68e Mon Sep 17 00:00:00 2001 From: John Eckersberg Date: Wed, 28 Oct 2026 11:53:52 -0400 Subject: [PATCH 1/4] Cargo.toml: Drop panic = "abort" from release profile This was added in 6792973b (Aug 2023) because a panic unwinding across an `extern "C"` boundary was undefined behavior. Since Rust 1.81.0, panics reaching a non-unwind ABI boundary abort automatically, so the profile setting is no longer needed for FFI safety. Our MSRV is 1.85.0. - https://blog.rust-lang.org/2024/09/05/Rust-1.81.0/ - https://rust-lang.github.io/rfcs/2945-c-unwind-abi.html Keeping it has a real cost: the test harness requires unwinding, so `cargo test --release` (and `%cargo_test` in the RPM build) rebuilds every dependency with a different panic strategy instead of reusing the release build. See https://doc.rust-lang.org/cargo/reference/profiles.html#panic Spawned threads/tasks have their join handles awaited, so a panic in one is propagated as an error. The one exception is the hyper connection driver task for the podman API client, where a panic would surface as a failed request instead. Assisted-by: AI Signed-off-by: John Eckersberg --- Cargo.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7f8449678..2b4873d6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,8 +7,6 @@ opt-level = 1 # No optimizations are too slow for us. [profile.release] lto = "thin" -# We use FFI so this is safest -panic = "abort" # We assume we're being delivered via e.g. RPM which supports split debuginfo debug = true From a254f6e908b239203251c04c9c5771b10aa405a4 Mon Sep 17 00:00:00 2001 From: John Eckersberg Date: Wed, 28 Oct 2026 12:19:39 -0400 Subject: [PATCH 2/4] Cargo.toml: Enable tokio test-util for all builds The `test-util` feature was only enabled via a dev-dependency in bootc-lib (for the `start_paused` retry tests in deploy.rs). Cargo unifies dev-dependency features when building tests, so tokio and everything depending on it (hyper, tokio-util, composefs, etc.) was compiled twice: once for `cargo build --release` and again for `cargo test --release`. Enabling it in the workspace dependency means both use the same tokio build. After `cargo build --release`, `cargo test --release` now rebuilds 20 crates (workspace members and dev-only deps) instead of 36. The cost to the shipped binary is negligible: the stripped `bootc` binary grows by ~28KB (0.1%), within normal build-to-build variance. Assisted-by: AI Signed-off-by: John Eckersberg --- Cargo.toml | 2 +- crates/lib/Cargo.toml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2b4873d6b..7b7b76b20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,7 +74,7 @@ similar-asserts = "2.0.0" static_assertions = "1.1.0" tempfile = "3.10.1" thiserror = "2.0.11" -tokio = ">= 1.37.0" +tokio = { version = ">= 1.37.0", features = ["test-util"] } tokio-util = { features = ["io-util"], version = "0.7.10" } toml = "1.0.0" tracing = "0.1.40" diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index 6342b2f58..c0ea199d4 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -83,7 +83,6 @@ uapi-version = "0.4.0" ostree-ext = { path = "../ostree-ext", features = ["bootc", "internal-testing-api"] } similar-asserts = { workspace = true } static_assertions = { workspace = true } -tokio = { workspace = true, features = ["test-util"] } [features] default = ["install-to-disk", "selinux"] From cfe0ba11bbecbca42246d2305dd148b0128d48f2 Mon Sep 17 00:00:00 2001 From: John Eckersberg Date: Wed, 28 Oct 2026 12:52:47 -0400 Subject: [PATCH 3/4] build: Don't rebuild during RPM %install `make install` depends on `completion` -> `bin` -> `manpages`, so the RPM %install phase re-ran cargo three times: rebuilding xtask, building bootc with the docgen feature to dump the CLI, then building the binaries again without docgen. In the Koji build log this accounted for roughly 12 minutes. Worse, `%cargo_prep` symlinks `target/release` to `target/rpm`, so these `--release` builds overwrote the `%cargo_build` output, and the RPM actually shipped binaries built with our `release` profile rather than Fedora's `rpm` profile. Add a `SKIP_BUILD=1` option to the Makefile that drops the build dependency from `completion`, and use it from the spec's %install so it only installs what %build produced. Plain `make install` is unchanged. Assisted-by: AI Signed-off-by: John Eckersberg --- Makefile | 8 ++++++++ contrib/packaging/bootc.spec | 6 ++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 5c924f3a4..baef616e6 100644 --- a/Makefile +++ b/Makefile @@ -56,6 +56,11 @@ CARGO_OPTIONS ?= --features "$(CARGO_FEATURES)" # image configuration rather than documentation. DISABLE_DOCS ?= +# Set to 1 to install already-built binaries and man pages without invoking +# cargo, e.g. from an RPM %install after the binaries were built in %build: +# make install SKIP_BUILD=1 +SKIP_BUILD ?= + # Build all binaries .PHONY: bin # The man pages are built first so that the production binary below @@ -73,7 +78,10 @@ manpages: cargo run --release --package xtask -- manpages -- $(CARGO_OPTIONS) .PHONY: completion +ifneq ($(SKIP_BUILD),1) completion: bin +endif +completion: mkdir -p target/completion for shell in bash elvish fish powershell zsh; do \ target/release/bootc completion $$shell > target/completion/bootc.$$shell; \ diff --git a/contrib/packaging/bootc.spec b/contrib/packaging/bootc.spec index 164dd6ee8..a6977ce25 100644 --- a/contrib/packaging/bootc.spec +++ b/contrib/packaging/bootc.spec @@ -165,8 +165,10 @@ sed -i -e '/https:\/\//d' cargo-vendor.txt %endif %install -# Pass CARGO_FEATURES explicitly to prevent auto-detection rebuild in install environment -%make_install INSTALL="install -p -c" CARGO_FEATURES="%{?with_rhsm:rhsm} %{?with_rhel9:rhel9}" +# Everything was built in %%build; SKIP_BUILD=1 keeps make from invoking cargo +# again (which would also overwrite the %%cargo_build output in target/rpm via +# the target/release symlink created by %%cargo_prep). +%make_install INSTALL="install -p -c" SKIP_BUILD=1 %if %{with ostree_ext} make install-ostree-hooks DESTDIR=%{?buildroot} %endif From a7f042188ced74bcbd4dae97c4fedc34adcf0653 Mon Sep 17 00:00:00 2001 From: John Eckersberg Date: Wed, 28 Oct 2026 13:56:10 -0400 Subject: [PATCH 4/4] build: Don't build bootc to generate man pages in the RPM `make manpages` builds bootc with the docgen feature, dumps its CLI as JSON and syncs the generated options/subcommands sections of docs/src/man/*.md before converting them to roff. The CI `validate` job already enforces (via `update-generated from-code --check`) that those committed sections match the CLI, so in a release build the sync is a no-op that costs a full extra build of bootc (~7 minutes in Koji). Add `xtask manpages --no-sync` and a `make manpages-no-sync` target that only do the conversion, and use it from the spec. The output is identical to `make manpages`. xtask itself still needs to be built, and it must not disturb the %cargo_build state that %cargo_test reuses. %cargo_prep symlinks target/release to target/rpm, and the %cargo_* macros run cargo with RUSTC_BOOTSTRAP=1, which some build scripts (proc-macro2, anyhow, thiserror) track. Building xtask with plain `cargo run --release` therefore caused %cargo_test to rebuild 100+ crates. Add XTASK_PROFILE and CARGO variables to the Makefile, and have the spec build xtask with the `rpm` profile and `%{__cargo}`. Assisted-by: AI Signed-off-by: John Eckersberg --- Makefile | 21 +++++++++++++++++++-- contrib/packaging/bootc.spec | 13 +++++++++++-- crates/xtask/src/man.rs | 7 +++++++ crates/xtask/src/xtask.rs | 16 +++++++++++++++- 4 files changed, 52 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index baef616e6..17bb57b79 100644 --- a/Makefile +++ b/Makefile @@ -61,6 +61,17 @@ DISABLE_DOCS ?= # make install SKIP_BUILD=1 SKIP_BUILD ?= +# Cargo profile used to build xtask (e.g. for man pages). RPM builds set this +# to `rpm` so xtask shares the %cargo_build profile instead of clobbering its +# state through the target/release -> target/rpm symlink from %cargo_prep. +XTASK_PROFILE ?= release + +# The cargo command used by the build targets. RPM builds set this to +# %{__cargo} so the environment (e.g. RUSTC_BOOTSTRAP) matches the +# %cargo_* macros; otherwise build scripts that depend on it re-run and +# invalidate the shared build state. +CARGO ?= cargo + # Build all binaries .PHONY: bin # The man pages are built first so that the production binary below @@ -69,13 +80,19 @@ ifneq ($(DISABLE_DOCS),1) bin: manpages endif bin: - cargo build --release $(CARGO_OPTIONS) --bins + $(CARGO) build --release $(CARGO_OPTIONS) --bins # The docgen feature this adds implies install-to-disk, so the man pages # document the full CLI regardless of CARGO_OPTIONS. .PHONY: manpages manpages: - cargo run --release --package xtask -- manpages -- $(CARGO_OPTIONS) + $(CARGO) run --profile $(XTASK_PROFILE) --package xtask -- manpages -- $(CARGO_OPTIONS) + +# Convert the man page markdown in docs/src/man without building bootc to +# sync its generated CLI sections; CI ensures those are already up to date. +.PHONY: manpages-no-sync +manpages-no-sync: + $(CARGO) run --profile $(XTASK_PROFILE) --package xtask -- manpages --no-sync .PHONY: completion ifneq ($(SKIP_BUILD),1) diff --git a/contrib/packaging/bootc.spec b/contrib/packaging/bootc.spec index a6977ce25..9d2a6cf4b 100644 --- a/contrib/packaging/bootc.spec +++ b/contrib/packaging/bootc.spec @@ -137,8 +137,6 @@ cd %{name}-%{version}-build %build export SYSTEM_REINSTALL_BOOTC_INSTALL_PODMAN_PATH=%{system_reinstall_bootc_install_podman_path} -# Build this first to avoid feature skew -make manpages # Build all binaries %if 0%{?container_build} @@ -156,6 +154,17 @@ cargo build -j%{_smp_build_ncpus} --release %{?with_rhsm:--features rhsm} %{?wit %endif %endif +# The generated CLI sections in the man page sources are kept in sync by CI, +# so just convert them; this avoids building bootc a second time. +# Outside of container builds, use the same cargo environment and profile as +# %%cargo_build/%%cargo_test, so xtask doesn't invalidate their shared state +# (target/release is a symlink to target/rpm, created by %%cargo_prep). +%if 0%{?container_build} +make manpages-no-sync +%else +make manpages-no-sync XTASK_PROFILE=rpm CARGO="%{__cargo}" +%endif + %if ! 0%{?container_build} %cargo_vendor_manifest # https://pagure.io/fedora-rust/rust-packaging/issue/33 diff --git a/crates/xtask/src/man.rs b/crates/xtask/src/man.rs index 173ef469a..5ca400fbd 100644 --- a/crates/xtask/src/man.rs +++ b/crates/xtask/src/man.rs @@ -835,6 +835,13 @@ pub fn generate_man_pages(sh: &Shell, cargo_options: &[String]) -> Result<()> { generate_pages(sh, false) } +/// Generate manuals from the Markdown as-is, without building bootc to sync +/// the generated CLI sections first. +#[context("Generating manpages")] +pub fn generate_man_pages_no_sync(sh: &Shell) -> Result<()> { + generate_pages(sh, false) +} + /// Get version from Cargo.toml #[context("Querying package version")] fn get_package_version() -> Result { diff --git a/crates/xtask/src/xtask.rs b/crates/xtask/src/xtask.rs index 9c496e254..d06b56c86 100644 --- a/crates/xtask/src/xtask.rs +++ b/crates/xtask/src/xtask.rs @@ -79,6 +79,11 @@ struct Cli { enum Commands { /// Generate man pages Manpages { + /// Only convert the markdown in docs/src/man, without first building + /// bootc to sync its generated CLI sections. CI verifies those are up + /// to date, so this is suitable for packaging a release. + #[arg(long, conflicts_with = "cargo_options")] + no_sync: bool, /// Extra options for cargo when building bootc, after `--` /// (e.g. `-- --no-default-features --features install-to-disk`) #[arg(last = true)] @@ -395,7 +400,16 @@ fn try_main() -> Result<()> { let sh = xshell::Shell::new()?; match cli.command { - Commands::Manpages { cargo_options } => man::generate_man_pages(&sh, &cargo_options), + Commands::Manpages { + no_sync, + cargo_options, + } => { + if no_sync { + man::generate_man_pages_no_sync(&sh) + } else { + man::generate_man_pages(&sh, &cargo_options) + } + } Commands::GuideManpages => man::generate_guide_man_pages(&sh), Commands::CheckDocs => man::check_docs(), Commands::UpdateGenerated { command } => match command {