diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa10e9a..ed48b55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,7 @@ on: - 'Cargo.toml' - 'Cargo.lock' - 'deny.toml' + - 'docker/**' - '.github/workflows/ci.yml' pull_request: branches: [ main ] @@ -18,6 +19,7 @@ on: - 'Cargo.toml' - 'Cargo.lock' - 'deny.toml' + - 'docker/**' - '.github/workflows/ci.yml' # Cancel outdated workflow runs @@ -301,6 +303,67 @@ jobs: working-directory: fuzz run: cargo audit + # Job 5b: Container images must build, and the binary inside must run. + # + # Nothing validated docker/Dockerfile before: packages.yml only fires on a + # published release or a manual dispatch. Two defects accumulated unnoticed -- + # the builder pinned a Rust below the workspace MSRV, and the alpine stage + # copied a Debian glibc binary into a musl image, producing a container in + # which prtip could not exec at all. + # + # `--version` is the check that matters. A glibc/musl mismatch builds a + # perfectly good-looking image and only fails when something tries to run it. + docker: + name: Docker Images + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Debian runtime image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile + target: runtime + tags: prtip:ci-runtime + load: true + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Build Alpine image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile + target: alpine + tags: prtip:ci-alpine + load: true + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Binary runs in both images + run: | + for image in prtip:ci-runtime prtip:ci-alpine; do + echo "::group::$image" + docker run --rm "$image" --version + echo "::endgroup::" + done + + - name: Alpine binary is musl-linked, not glibc + run: | + # Guards against a regression to copying the Debian binary: that + # produced an image which built fine and failed on first exec. + interp=$(docker run --rm --entrypoint sh prtip:ci-alpine \ + -c 'ldd /usr/local/bin/prtip 2>&1' | head -1) + echo "interpreter: $interp" + case "$interp" in + *ld-musl*) echo "OK: musl-linked" ;; + *) echo "FAIL: expected a musl interpreter, got: $interp"; exit 1 ;; + esac + # Job 6: MSRV (Minimum Supported Rust Version) check msrv: name: MSRV Check (1.88) diff --git a/CHANGELOG.md b/CHANGELOG.md index c100bdb..c8de820 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -261,6 +261,28 @@ along with every unmaintained/unsound warning. `cargo audit` and third-party-material statement moved there, because `LICENSE` has to be the licence text and nothing else for both of the reasons above. Nothing was dropped in the move. +- **The Docker build was broken by the MSRV raise.** `docker/Dockerfile` pinned + `ARG RUST_VERSION=1.85`, below the workspace's new 1.88 floor, so the builder + stage could no longer compile the project at all. Nothing caught it because + `packages.yml` — the only workflow that touches the Dockerfile — runs solely on + a published release or a manual dispatch. +- **The Alpine image never worked.** Its stage copied the Debian-built **glibc** + binary into a **musl** image. Alpine ships no `/lib64/ld-linux-x86-64.so.2`, so + the kernel failed the exec with a bare "no such file or directory" — the image + built cleanly and `prtip` could not run in it, which the stage's own + `HEALTHCHECK` would have failed on every start. The comment in place + ("For now, use the glibc binary (requires compatibility layer or rebuild)") + acknowledged it. There is now a real `musl-builder` stage building natively on + `rust:1.88-alpine` with `libpcap-dev`, `openssl-dev` and a C toolchain from + apk, which is far simpler than assembling a musl sysroot on Debian. Verified: + the binary reports `prtip 1.0.0` inside the image and links + `/lib/ld-musl-x86_64.so.1`; the image is 26.9 MB against the Debian variant's + 97 MB. +- **CI now builds both container images and runs the binary in each.** A + glibc/musl mismatch produces a perfectly good-looking image that only fails + when something tries to execute it, so the gate asserts `--version` succeeds + and that the Alpine binary's interpreter is musl. `ci.yml` path filters now + include `docker/**`. - **Seven broken internal documentation links** in `to-dos/PHASE-5/SPRINT-5.5.5-TODO.md` and `SPRINT-5.5.6-TODO.md`. They pointed at `../docs/...` and `../benchmarks/...` from `to-dos/PHASE-5/`, which resolves diff --git a/docker/Dockerfile b/docker/Dockerfile index b6fdc81..1f4fa1d 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -9,7 +9,10 @@ # docker run --rm --net=host --cap-add=NET_RAW prtip -sS -p 80,443 192.168.1.1 # docker run -it --rm --net=host --cap-add=NET_RAW prtip --tui -ARG RUST_VERSION=1.85 +# Must be >= the workspace `rust-version` in Cargo.toml (1.88). A pin below +# the MSRV fails the build with "requires rustc 1.88 or newer"; nothing in CI +# caught the drift because packages.yml only runs on releases. +ARG RUST_VERSION=1.88 ARG VERSION=1.0.0 # ============================================================================== @@ -104,6 +107,34 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ ENTRYPOINT ["prtip"] CMD ["--help"] +# ============================================================================== +# Stage 2b: musl builder (for the Alpine variant) +# ============================================================================== +# Built inside Alpine rather than cross-compiled from Debian. `pcap` links +# against libpcap, and `mlua` builds a vendored Lua, so both need a C toolchain +# and headers that match the target libc. Building natively on Alpine gets musl +# versions of both from apk, which is far simpler than assembling a musl +# sysroot on Debian. +FROM rust:${RUST_VERSION}-alpine AS musl-builder + +WORKDIR /build + +RUN apk add --no-cache \ + libpcap-dev \ + musl-dev \ + pkgconfig \ + build-base \ + linux-headers \ + openssl-dev \ + openssl-libs-static + +COPY Cargo.toml Cargo.lock ./ +COPY crates/ crates/ + +RUN cargo build --release --locked + +RUN strip /build/target/release/prtip + # ============================================================================== # Stage 3: Alpine variant (smaller image, musl libc) # ============================================================================== @@ -123,12 +154,13 @@ RUN apk add --no-cache \ # Create non-root user RUN adduser -D -s /bin/sh prtip -# Note: For Alpine, you need to build with musl target -# This stage expects a pre-built musl binary -# COPY --from=builder /build/target/x86_64-unknown-linux-musl/release/prtip /usr/local/bin/prtip - -# For now, use the glibc binary (requires compatibility layer or rebuild) -COPY --from=builder /build/target/release/prtip /usr/local/bin/prtip +# Binary comes from the musl builder stage above, not from `builder`. +# Copying the Debian-built glibc binary here produced an image in which +# `prtip` could not execute at all -- musl provides no +# /lib64/ld-linux-x86-64.so.2, so the kernel fails the exec with a bare +# "no such file or directory". The HEALTHCHECK below would have caught it +# on every run of this image. +COPY --from=musl-builder /build/target/release/prtip /usr/local/bin/prtip COPY README.md /usr/share/doc/prtip/ COPY LICENSE /usr/share/doc/prtip/