From 50777fa18e2556c9ee729a4cfc49026440a11760 Mon Sep 17 00:00:00 2001 From: Jer Miller Date: Fri, 17 Jul 2026 07:48:34 -0600 Subject: [PATCH] fix(rust): statically link libFLAC for releases Bundle libFLAC so the Ubuntu baseline binary does not encode a distro-specific FLAC soname that breaks Fedora and openSUSE packages. Exclude host build output from both container engines, reject dirty release trees and conflicting artifacts, and select native packages by the current crate version.\n\nRefresh the operator documentation and release-rail tests to match the static dependency and version-derived workflow. --- .containerignore | 9 ++++ .dockerignore | 1 + Cargo.lock | 10 ++++ RELEASING.md | 35 +++++++------- crates/solstone-linux/Cargo.toml | 2 +- .../solstone-linux/src/release_rail_tests.rs | 25 +++++++++- packaging/Containerfile | 17 +++---- packaging/INSTALL-NOTES | 9 ++-- scripts/build-release.sh | 46 +++++++++++++++---- 9 files changed, 113 insertions(+), 41 deletions(-) create mode 100644 .containerignore create mode 120000 .dockerignore diff --git a/.containerignore b/.containerignore new file mode 100644 index 0000000..e0ded5c --- /dev/null +++ b/.containerignore @@ -0,0 +1,9 @@ +target/ +dist/ +.venv/ +build/ +**/__pycache__/ +**/*.pyc +.pytest_cache/ +.mypy_cache/ +.installed diff --git a/.dockerignore b/.dockerignore new file mode 120000 index 0000000..092a75d --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.containerignore \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 9751937..470e857 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -400,6 +400,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.5" @@ -1264,6 +1273,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fc5cbb957a914952ee9b8667e82b984c6dc280087df01497fc5b4776d303582" dependencies = [ + "cmake", "libc", ] diff --git a/RELEASING.md b/RELEASING.md index 090799a..cd05883 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -2,8 +2,7 @@ The Rust release rail is operator-run and separate from the Python/PyPI rail. It produces portable, Debian, and RPM artifacts; it does not publish, tag, or -create a hosted release. Releases remain manual by policy—do not add CI/CD -publishing for this repository. +create a hosted release. Releases remain an operator-run process. ## 1. Host prerequisites @@ -21,9 +20,9 @@ The Rust version comes from `[workspace.package].version` and the member's `version.workspace = true`. It is independent of the Python package version. Every artifact is written below `dist/rust/` and contains the Rust version: -- `solstone-linux-0.1.0-linux-x86_64.tar.gz` -- `solstone-linux_0.1.0-1_amd64.deb` -- `solstone-linux-0.1.0-1.x86_64.rpm` +- `solstone-linux--linux-x86_64.tar.gz` +- `solstone-linux_-1_amd64.deb` +- `solstone-linux--1.x86_64.rpm` ## 3. Build commands @@ -57,18 +56,18 @@ rpm -qpl dist/rust/solstone-linux-*-1.x86_64.rpm sha256sum dist/rust/* ``` -Confirm that each artifact contains the binary, LICENSE, INSTALL-NOTES, and all -13 icons. It must contain no systemd unit and no desktop file. +Confirm that each artifact contains the binary, LICENSE, INSTALL-NOTES, and the +icon set declared in the member package manifest. It must contain no systemd +unit and no desktop file. ## 5. Blocking first-release FLAC validation This checkpoint is mandatory. Do not release based only on a successful link. -`flac-bound` 0.5 documents system libFLAC 1.4 or newer for its -`libflac-nobuild` feature. Ubuntu 22.04 supplies libFLAC 1.3.3 with soname 8. -The code uses a small encoder surface, but an ABI mismatch could still crash at -runtime. The pre-release soak must therefore exercise real output through the -shipped binary: +The release binary statically links the bundled libFLAC, so it has no direct +cross-distribution libFLAC runtime dependency. A distro's PulseAudio stack may +independently load its own libFLAC through libsndfile. The pre-release soak must +still exercise real encoded output through the shipped binary: 1. Install the produced artifact on a test Linux desktop with the runtime dependencies from `packaging/INSTALL-NOTES`. @@ -78,22 +77,20 @@ shipped binary: `flac -t path/to/new/audio.flac` (or each split mono FLAC). 4. Treat any encoder crash or decode failure as a release blocker. -If the checkpoint fails, stop. The follow-up is a source-built libFLAC >=1.4 -layer in `packaging/Containerfile`; changing the crate feature or raising the -Ubuntu baseline is not part of this rail. +If the checkpoint fails, stop and diagnose the bundled encoder before release. ## 6. Portable installer Preview a local tarball installation without writes: ```bash -scripts/install.sh --dry-run dist/rust/solstone-linux-0.1.0-linux-x86_64.tar.gz +scripts/install.sh --dry-run "dist/rust/solstone-linux--linux-x86_64.tar.gz" ``` Install to the default `$HOME/.local` prefix: ```bash -scripts/install.sh dist/rust/solstone-linux-0.1.0-linux-x86_64.tar.gz +scripts/install.sh "dist/rust/solstone-linux--linux-x86_64.tar.gz" ``` The script reports when `$HOME/.local/bin` is not on PATH. A different prefix @@ -116,6 +113,10 @@ succeed, upload the three versioned files and checksum list through the chosen manual release surface. Do not reuse the Python version, Python tag/publish script, or PyPI release artifacts. +Release-note bodies come only from the matching `CHANGELOG.md` block. Extract +that block with `scripts/extract_changelog.sh `; do not create a +separate engineering-tone release-note template here. + ## 9. Known constraints - x86_64 only diff --git a/crates/solstone-linux/Cargo.toml b/crates/solstone-linux/Cargo.toml index a927725..0dea99d 100644 --- a/crates/solstone-linux/Cargo.toml +++ b/crates/solstone-linux/Cargo.toml @@ -16,7 +16,7 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" sha2 = "0.10" claxon = "0.4.3" -flac-bound = { version = "0.5.0", default-features = false, features = ["libflac-nobuild"] } +flac-bound = { version = "0.5.0", default-features = false, features = ["libflac-noogg"] } libpulse-binding = "2.30.1" chrono = "0.4" rustix = { version = "1", features = ["event", "fs", "process", "time"] } diff --git a/crates/solstone-linux/src/release_rail_tests.rs b/crates/solstone-linux/src/release_rail_tests.rs index 25144d4..7585f8f 100644 --- a/crates/solstone-linux/src/release_rail_tests.rs +++ b/crates/solstone-linux/src/release_rail_tests.rs @@ -10,7 +10,7 @@ use std::process::{Command, Output}; use toml::Value; -const VERSION: &str = "0.1.0"; +const VERSION: &str = env!("CARGO_PKG_VERSION"); fn manifest_dir() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) @@ -118,6 +118,29 @@ fn package_metadata_and_resolved_licenses() { let rpm = &member["package"]["metadata"]["generate-rpm"]; assert_eq!(rpm["license"].as_str(), Some("AGPL-3.0-only")); assert_eq!(rpm["auto-req"].as_str(), Some("auto")); + + let flac = &member["dependencies"]["flac-bound"]; + assert_eq!(flac["default-features"].as_bool(), Some(false)); + assert_eq!( + flac["features"].as_array().unwrap(), + &[Value::String("libflac-noogg".into())] + ); +} + +// AC: both supported container engines share one ignore policy that excludes +// host build products without hiding the canonical icon sources. +#[test] +fn container_context_excludes_host_outputs() { + let root = workspace_root(); + assert_eq!( + fs::read_link(root.join(".dockerignore")).unwrap(), + PathBuf::from(".containerignore") + ); + let ignore = fs::read_to_string(root.join(".containerignore")).unwrap(); + for excluded in ["target/", "dist/", ".venv/", "**/__pycache__/"] { + assert!(ignore.lines().any(|line| line == excluded)); + } + assert!(!ignore.lines().any(|line| line.contains("contrib"))); } // AC: each tool's asset dialect resolves to the same committed files plus the diff --git a/packaging/Containerfile b/packaging/Containerfile index 34a126a..ec76492 100644 --- a/packaging/Containerfile +++ b/packaging/Containerfile @@ -9,8 +9,8 @@ ARG RUST_VERSION=1.92.0 RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ ca-certificates \ + cmake \ curl \ - libflac-dev \ libglib2.0-dev \ libgstreamer1.0-dev \ libpulse-dev \ @@ -18,9 +18,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ xz-utils \ && rm -rf /var/lib/apt/lists/* -# flac-bound 0.5 documents libFLAC >=1.4 for libflac-nobuild, while Jammy -# supplies 1.3.3 (soname 8). Linking is not proof of runtime ABI safety: the -# blocking first-release checkpoint in RELEASING.md validates real FLAC output. +# flac-bound builds libFLAC without Ogg and links it statically. CMake and the C +# toolchain are build-only requirements; no system libFLAC is used at runtime. RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ | sh -s -- -y --profile minimal --default-toolchain "${RUST_VERSION}" ENV PATH="/root/.cargo/bin:${PATH}" @@ -30,6 +29,8 @@ WORKDIR /src # deliberately panics if those canonical source files cannot be read. COPY . . +# Cargo currently serializes package name immediately before version. Keep the +# non-empty guard below so any JSON ordering change fails the build. RUN cargo build --locked --release -p solstone-linux \ && VERSION=$(cargo metadata --format-version 1 --no-deps \ | sed -n 's/.*"name":"solstone-linux","version":"\([^"]*\)".*/\1/p') \ @@ -48,8 +49,8 @@ FROM baseline AS deb-build RUN cargo install cargo-deb --version 3.7.0 --locked \ && cargo deb --locked -p solstone-linux \ && VERSION=$(cat /release/VERSION) \ - && DEB=$(find target/debian -maxdepth 1 -type f -name '*.deb' -print -quit) \ - && test -n "$DEB" \ + && DEB="target/debian/solstone-linux_${VERSION}-1_amd64.deb" \ + && test -f "$DEB" \ && cp "$DEB" "/release/solstone-linux_${VERSION}-1_amd64.deb" \ && rm /release/VERSION @@ -71,8 +72,8 @@ COPY --from=baseline /release /release RUN cargo install cargo-generate-rpm --version 0.21.0 --locked \ && cargo generate-rpm -p solstone-linux \ && VERSION=$(cat /release/VERSION) \ - && RPM=$(find target/generate-rpm -maxdepth 1 -type f -name '*.rpm' -print -quit) \ - && test -n "$RPM" \ + && RPM="target/generate-rpm/solstone-linux-${VERSION}-1.x86_64.rpm" \ + && test -f "$RPM" \ && cp "$RPM" "/release/solstone-linux-${VERSION}-1.x86_64.rpm" \ && rm /release/VERSION diff --git a/packaging/INSTALL-NOTES b/packaging/INSTALL-NOTES index 5e353cc..9924994 100644 --- a/packaging/INSTALL-NOTES +++ b/packaging/INSTALL-NOTES @@ -8,7 +8,6 @@ Runtime requirements: - glibc 2.35 or newer - libpulse and a PulseAudio-compatible server (PipeWire Pulse is supported) -- libFLAC - GStreamer 1.0 core, base, good, PipeWire, and X11 plugins - PipeWire and xdg-desktop-portal with ScreenCast support for Wayland capture - xdg-utils for opening links @@ -16,17 +15,17 @@ Runtime requirements: Typical package families: -- Debian/Ubuntu: libpulse0, libflac8/libflac12, libgstreamer1.0-0, +- Debian/Ubuntu: libpulse0, libgstreamer1.0-0, gstreamer1.0-plugins-base, gstreamer1.0-plugins-good, gstreamer1.0-pipewire, gstreamer1.0-x, pipewire-pulse, xdg-desktop-portal, xdg-utils -- Fedora/RHEL: pulseaudio-libs, flac-libs, gstreamer1, +- Fedora/RHEL: pulseaudio-libs, gstreamer1, gstreamer1-plugins-base, gstreamer1-plugins-good, pipewire-gstreamer, pipewire-pulseaudio, xdg-desktop-portal, xdg-utils -- openSUSE: libpulse0, libFLAC12, gstreamer, gstreamer-plugins-base, +- openSUSE: libpulse0, gstreamer, gstreamer-plugins-base, gstreamer-plugins-good, gstreamer-plugin-pipewire, pipewire-pulseaudio, xdg-desktop-portal, xdg-utils -- Arch: libpulse, flac, gstreamer, gst-plugins-base, gst-plugins-good, +- Arch: libpulse, gstreamer, gst-plugins-base, gst-plugins-good, gst-plugin-pipewire, pipewire-pulse, xdg-desktop-portal, xdg-utils The Rust install-service subcommand is not implemented in this release. Run diff --git a/scripts/build-release.sh b/scripts/build-release.sh index 40adcac..0b9f94e 100755 --- a/scripts/build-release.sh +++ b/scripts/build-release.sh @@ -36,6 +36,17 @@ if [[ "$ARCH" != "x86_64" ]]; then exit 2 fi +REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || { + echo "error: run this script from a solstone-linux Git checkout" >&2 + exit 1 +} +cd "$REPO_ROOT" + +if ! git diff --quiet HEAD || [[ -n "$(git status --porcelain)" ]]; then + echo "error: working tree dirty; commit or stash changes before building release artifacts" >&2 + exit 1 +fi + if command -v podman >/dev/null 2>&1; then ENGINE="podman" elif command -v docker >/dev/null 2>&1; then @@ -45,12 +56,6 @@ else exit 1 fi -REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || { - echo "error: run this script from a solstone-linux Git checkout" >&2 - exit 1 -} -cd "$REPO_ROOT" - OUTPUT_TMP=$(mktemp -d) trap 'rm -rf "$OUTPUT_TMP"' EXIT @@ -78,12 +83,35 @@ fi TARBALL_VERSION=${TARBALLS[0]##*/solstone-linux-} TARBALL_VERSION=${TARBALL_VERSION%-linux-x86_64.tar.gz} -if [[ "${PACKAGES[0]##*/}" != *"$TARBALL_VERSION"* ]]; then +case "$FORMAT" in + deb) EXPECTED_PACKAGE="solstone-linux_${TARBALL_VERSION}-1_amd64.deb" ;; + rpm) EXPECTED_PACKAGE="solstone-linux-${TARBALL_VERSION}-1.x86_64.rpm" ;; +esac +if [[ "${PACKAGES[0]##*/}" != "$EXPECTED_PACKAGE" ]]; then echo "error: package and tarball versions do not match" >&2 exit 1 fi +check_artifact() { + local source="$1" + local destination="dist/rust/${source##*/}" + if [[ -e "$destination" ]] && ! cmp -s "$source" "$destination"; then + echo "error: refusing to overwrite existing artifact with different bytes: $destination" >&2 + exit 1 + fi +} + mkdir -p dist/rust -install -m 0644 "${TARBALLS[0]}" "dist/rust/${TARBALLS[0]##*/}" -install -m 0644 "${PACKAGES[0]}" "dist/rust/${PACKAGES[0]##*/}" +ARTIFACTS=("${TARBALLS[0]}" "${PACKAGES[0]}") +for ARTIFACT in "${ARTIFACTS[@]}"; do + check_artifact "$ARTIFACT" +done +for ARTIFACT in "${ARTIFACTS[@]}"; do + DESTINATION="dist/rust/${ARTIFACT##*/}" + if [[ -e "$DESTINATION" ]]; then + echo "keeping byte-identical existing artifact: $DESTINATION" + else + install -m 0644 "$ARTIFACT" "$DESTINATION" + fi +done echo "built Rust $FORMAT artifacts for version $TARBALL_VERSION in dist/rust/" -- 2.51.2