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/"