feat: add static musl Linux binary built in Docker and published in CI (FJMCP-4) #4

Merged
David merged 2 commits from feat/static-musl-binary-fjmcp-4 into main 2026-05-28 13:44:05 +02:00
Owner

Resolves FJMCP-4.

Adds a fully static x86_64-unknown-linux-musl binary (forgejo-mcp-linux-x86_64-static) alongside the existing dynamic glibc Linux and mingw Windows artifacts, so forgejo-mcp runs on any Linux without a matching glibc/OpenSSL. The glibc and Windows artifacts are unchanged.

What changed

  • oci-build/Dockerfile.static: builds the musl release binary and exports it through a FROM scratch AS binary stage (no runtime stage, since a static binary needs no base image). Mirrors oci-build/Dockerfile: dummy-main dependency-cache prime, sharing=locked cargo cache mounts, CARGO_BUILD_JOBS / GIT_SHA build args.
  • Cargo.toml / Cargo.lock: openssl is declared as a direct dependency with the vendored feature, scoped to [target.'cfg(target_env = "musl")'.dependencies]. Cargo feature unification then statically vendors OpenSSL into every transitive openssl-sys for the musl build only, so the glibc and Windows builds keep linking OpenSSL exactly as before. libgit2 / libssh2 / zlib link statically because building for a non-host target triple makes the pkg-config crate skip system-library probing and fall back to their bundled cc-compiled sources.
  • .forgejo/workflows/build-binary-static.yml: mirrors build-binary.yml (same main / v* triggers and src + Cargo.* + oci-build/** + own-file paths); publishes the -static artifact to Generic Packages.
  • CARGO_BUILD_JOBS divisor dropped from nproc/2 to nproc/3 across all three binary workflows (floored at 1): one push can now run three concurrent binary builds on a single runner, so this keeps combined parallelism near nproc per CI.md.
  • just build-static recipe; README.md and CLAUDE.md document the new artifact and reconcile the prior glibc-not-musl note.

Tooling gap

No rust-builder-musl image exists in the org builder-image family yet (docker manifest inspect ghcr.io/niceguyit/rust-builder-musl:... returns manifest unknown), so Dockerfile.static adds the x86_64-unknown-linux-musl rustup target and musl-tools inline. That gap is tracked in FJMCP-5 (filed, not worked around silently) to move the toolchain into a rust-builder-musl image so the inline block can be dropped.

Verification

Built locally with just build-static:

  • file ./output/forgejo-mcp -> ELF 64-bit LSB pie executable, x86-64, static-pie linked
  • ldd ./output/forgejo-mcp -> statically linked
  • readelf -d shows no NEEDED / libssl / libcrypto / libgit2 / libssh2 entries
  • The binary answers an MCP initialize request over stdio and exits cleanly
  • just pre-commit (fmt + clippy + build + test in the glibc image) passes; the glibc build compiles git2/ssh2-config but not openssl-src, confirming the vendored feature stays inactive for glibc

Test plan

  • On merge to main, build-binary-static.yml builds the static binary in Docker and publishes forgejo-mcp-linux-x86_64-static to Generic Packages
  • build-binary.yml and build-binary-windows.yml still publish forgejo-mcp-linux-x86_64 and forgejo-mcp-windows-x86_64.exe unchanged
Resolves FJMCP-4. Adds a fully static `x86_64-unknown-linux-musl` binary (`forgejo-mcp-linux-x86_64-static`) alongside the existing dynamic glibc Linux and mingw Windows artifacts, so forgejo-mcp runs on any Linux without a matching glibc/OpenSSL. The glibc and Windows artifacts are unchanged. ## What changed - `oci-build/Dockerfile.static`: builds the musl release binary and exports it through a `FROM scratch AS binary` stage (no runtime stage, since a static binary needs no base image). Mirrors `oci-build/Dockerfile`: dummy-main dependency-cache prime, `sharing=locked` cargo cache mounts, `CARGO_BUILD_JOBS` / `GIT_SHA` build args. - `Cargo.toml` / `Cargo.lock`: `openssl` is declared as a direct dependency with the `vendored` feature, scoped to `[target.'cfg(target_env = "musl")'.dependencies]`. Cargo feature unification then statically vendors OpenSSL into every transitive `openssl-sys` for the musl build only, so the glibc and Windows builds keep linking OpenSSL exactly as before. `libgit2` / `libssh2` / `zlib` link statically because building for a non-host target triple makes the `pkg-config` crate skip system-library probing and fall back to their bundled `cc`-compiled sources. - `.forgejo/workflows/build-binary-static.yml`: mirrors `build-binary.yml` (same `main` / `v*` triggers and `src` + `Cargo.*` + `oci-build/**` + own-file `paths`); publishes the `-static` artifact to Generic Packages. - `CARGO_BUILD_JOBS` divisor dropped from `nproc/2` to `nproc/3` across all three binary workflows (floored at 1): one push can now run three concurrent binary builds on a single runner, so this keeps combined parallelism near `nproc` per `CI.md`. - `just build-static` recipe; `README.md` and `CLAUDE.md` document the new artifact and reconcile the prior glibc-not-musl note. ## Tooling gap No `rust-builder-musl` image exists in the org builder-image family yet (`docker manifest inspect ghcr.io/niceguyit/rust-builder-musl:...` returns `manifest unknown`), so `Dockerfile.static` adds the `x86_64-unknown-linux-musl` rustup target and `musl-tools` inline. That gap is tracked in FJMCP-5 (filed, not worked around silently) to move the toolchain into a `rust-builder-musl` image so the inline block can be dropped. ## Verification Built locally with `just build-static`: - `file ./output/forgejo-mcp` -> `ELF 64-bit LSB pie executable, x86-64, static-pie linked` - `ldd ./output/forgejo-mcp` -> `statically linked` - `readelf -d` shows no `NEEDED` / `libssl` / `libcrypto` / `libgit2` / `libssh2` entries - The binary answers an MCP `initialize` request over stdio and exits cleanly - `just pre-commit` (fmt + clippy + build + test in the glibc image) passes; the glibc build compiles `git2`/`ssh2-config` but not `openssl-src`, confirming the vendored feature stays inactive for glibc ## Test plan - [ ] On merge to `main`, `build-binary-static.yml` builds the static binary in Docker and publishes `forgejo-mcp-linux-x86_64-static` to Generic Packages - [ ] `build-binary.yml` and `build-binary-windows.yml` still publish `forgejo-mcp-linux-x86_64` and `forgejo-mcp-windows-x86_64.exe` unchanged
feat: add static musl Linux binary built in Docker, published in CI
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 28s
ca562eb348
Add a fully static x86_64-unknown-linux-musl artifact (forgejo-mcp-linux-x86_64-static) alongside the existing dynamic glibc Linux and mingw Windows binaries, so forgejo-mcp runs on any Linux without a matching glibc/OpenSSL. The glibc and Windows artifacts are unchanged.

oci-build/Dockerfile.static builds the musl release binary and exports it via a `FROM scratch AS binary` stage; there is no runtime stage because a static binary needs no base image. It mirrors oci-build/Dockerfile (dummy-main dependency-cache prime, sharing=locked cargo cache mounts, CARGO_BUILD_JOBS / GIT_SHA build args).

OpenSSL is vendored from source via the `openssl` crate `vendored` feature, declared as a direct dependency scoped to `cfg(target_env = "musl")` so Cargo's feature unification propagates it to the transitive openssl-sys only for the musl build; the glibc and Windows builds keep linking OpenSSL exactly as before. libgit2 / libssh2 / zlib link statically because building for a target triple different from the host makes the pkg-config crate skip system-library probing (PKG_CONFIG_ALLOW_CROSS left unset) and fall back to their bundled cc-compiled sources. Verified locally: `ldd` reports statically linked, `file` reports static-pie, and the binary has no NEEDED / libssl / libgit2 entries; it answers an MCP initialize over stdio.

.forgejo/workflows/build-binary-static.yml mirrors build-binary.yml (same main / v* triggers and src + Cargo.* + oci-build/** + own-file paths) and publishes the -static artifact to Generic Packages. One push can now run three binary builds on a single runner, so CARGO_BUILD_JOBS drops from nproc/2 to nproc/3 across all three workflows (floored at 1) to keep the combined parallelism near nproc per CI.md.

just build-static added; README.md and CLAUDE.md document the new artifact and reconcile the prior glibc-not-musl note.

No rust-builder-musl image exists in the org builder-image family yet, so Dockerfile.static adds the musl rustup target and musl-tools inline. That gap is tracked in FJMCP-5 (not worked around silently) to move the toolchain into a rust-builder-musl image.

#FJMCP-4

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Merge branch 'main' into feat/static-musl-binary-fjmcp-4
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 1m17s
Create release / Create release from merged PR (pull_request) Has been skipped
1bbdf058b9
Resolve conflicts from the FJMCP-3 (clap CLI + version self-updater, with build.rs baking FORGEJO_MCP_GIT_HASH / FORGEJO_MCP_BUILD_DATE / FORGEJO_MCP_UPDATE_URL) and FJMCP-6 (Streamable HTTP transport) work that landed on main after this branch was cut.

Cargo.toml: keep both the musl-scoped `openssl` vendored dependency and main's `[dev-dependencies]` (tempfile, wiremock). Cargo.lock: keep both `openssl` and `reqwest` under the forgejo-mcp package. build-binary.yml / build-binary-windows.yml: keep main's `build_date` build-meta line and apply this branch's nproc/3 concurrency cap (three binary workflows now share a runner).

Integrate the static build with main's build.rs, which this branch predated: oci-build/Dockerfile.static now `COPY build.rs ./` (the crate uses `env!("FORGEJO_MCP_GIT_HASH")` at compile time, so the musl build fails to compile without it) and accepts the FORGEJO_MCP_GIT_HASH / FORGEJO_MCP_BUILD_DATE build args, mirroring oci-build/Dockerfile. build-binary-static.yml now computes build_date, passes both build-meta args, and publishes a `.sha256` sidecar, matching the glibc and Windows workflows and the CLAUDE.md "each published artifact gets a .sha256 sidecar" convention.

Verified: `cargo build --release --locked` on glibc (openssl-src is not compiled, so the glibc artifact is unchanged) and the full musl Docker build (`file` -> static-pie, `ldd` -> statically linked, `readelf -d` -> no NEEDED/libssl/libgit2/libssh2, `--version` bakes the real git hash).

#FJMCP-4

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author
Owner

Resolved the merge conflicts against current main (merge commit, no rebase/force-push).

Conflicts were caused by FJMCP-3 (clap CLI + version self-updater, including build.rs) and FJMCP-6 (Streamable HTTP transport) landing on main after this branch was cut. Resolution:

  • Cargo.toml: kept both the musl-scoped openssl (vendored) dependency and main's [dev-dependencies] (tempfile, wiremock). Cargo.lock: kept both openssl and reqwest.
  • build-binary.yml / build-binary-windows.yml: kept main's build_date build-meta line and applied this branch's nproc/3 concurrency cap.
  • Integrated the static build with main's build.rs (this branch predated it): oci-build/Dockerfile.static now COPY build.rs ./ (the crate uses env!("FORGEJO_MCP_GIT_HASH") at compile time, so the musl build will not compile without it) and accepts the FORGEJO_MCP_GIT_HASH / FORGEJO_MCP_BUILD_DATE build args, mirroring oci-build/Dockerfile. build-binary-static.yml now computes build_date, passes both build-meta args, and publishes a .sha256 sidecar, matching the glibc/Windows workflows and the CLAUDE.md "each published artifact gets a .sha256 sidecar" convention.

Verification after the merge: just pre-commit passes (fmt + clippy + build + 36 tests in the glibc image); the host glibc build does not compile openssl-src (glibc artifact unchanged); the full musl Docker build produces a file -> static-pie, ldd -> statically linked binary with no NEEDED/libssl/libgit2/libssh2 entries, and --version bakes the real git hash.

Resolved the merge conflicts against current `main` (merge commit, no rebase/force-push). Conflicts were caused by FJMCP-3 (clap CLI + `version` self-updater, including `build.rs`) and FJMCP-6 (Streamable HTTP transport) landing on `main` after this branch was cut. Resolution: - `Cargo.toml`: kept both the musl-scoped `openssl` (vendored) dependency and main's `[dev-dependencies]` (tempfile, wiremock). `Cargo.lock`: kept both `openssl` and `reqwest`. - `build-binary.yml` / `build-binary-windows.yml`: kept main's `build_date` build-meta line and applied this branch's `nproc/3` concurrency cap. - Integrated the static build with main's `build.rs` (this branch predated it): `oci-build/Dockerfile.static` now `COPY build.rs ./` (the crate uses `env!("FORGEJO_MCP_GIT_HASH")` at compile time, so the musl build will not compile without it) and accepts the `FORGEJO_MCP_GIT_HASH` / `FORGEJO_MCP_BUILD_DATE` build args, mirroring `oci-build/Dockerfile`. `build-binary-static.yml` now computes `build_date`, passes both build-meta args, and publishes a `.sha256` sidecar, matching the glibc/Windows workflows and the CLAUDE.md "each published artifact gets a `.sha256` sidecar" convention. Verification after the merge: `just pre-commit` passes (fmt + clippy + build + 36 tests in the glibc image); the host glibc build does not compile `openssl-src` (glibc artifact unchanged); the full musl Docker build produces a `file` -> static-pie, `ldd` -> statically linked binary with no `NEEDED`/`libssl`/`libgit2`/`libssh2` entries, and `--version` bakes the real git hash.
David merged commit ba2cceffac into main 2026-05-28 13:44:05 +02:00
David deleted branch feat/static-musl-binary-fjmcp-4 2026-05-28 13:44:06 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
pandoras-box/forgejo-mcp!4
No description provided.