Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions .github/sign-fatjars.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env bash

# SPDX-FileCopyrightText: 2026 Bernard Ladenthin <bernard.ladenthin@gmail.com>
#
# SPDX-License-Identifier: MIT OR Apache-2.0

# Cross-repo shared script — kept BYTE-IDENTICAL in java-llama.cpp and srcmorph (sync any
# edit to both). GPG-signs the fat jars (jar-with-dependencies) in a directory with a
# detached, armored .asc signature — the authenticity counterpart to any .sha256 integrity
# file. The caller builds/collects the fat jars; this only signs every
# *-jar-with-dependencies*.jar it finds in <asset-dir> (thin jars are left untouched).
#
# Signed in the GitHub-Release attach path, not at Maven build time, because only that
# dispatch-gated path receives the signing key: GPG_PRIVATE_KEY / GPG_PASSPHRASE are scoped
# to the `maven-central` GitHub Environment. Cross-repo convention + per-repo shapes:
# workspace/policies/fat-jar-release-assets.md.
#
# Usage: sign-fatjars.sh <asset-dir>
# <asset-dir> directory holding the fat jars (and possibly thin jars); every
# *-jar-with-dependencies*.jar in it is signed in place (-> <jar>.asc).
#
# Env: GPG_PRIVATE_KEY armored secret key (required)
# GPG_PASSPHRASE passphrase for the key (may be empty)
#
# Fail-loud: aborts if the key is absent, if no fat jar is found, or if any signature
# fails to verify.

set -euo pipefail

DIR="${1:?usage: sign-fatjars.sh <asset-dir>}"

if [ -z "${GPG_PRIVATE_KEY:-}" ]; then
echo "::error::GPG_PRIVATE_KEY is empty — cannot sign the fat jars. The maven-central environment did not deliver the secret to this ref." >&2
exit 1
fi
if [ -n "${GPG_PASSPHRASE:-}" ]; then echo "::add-mask::${GPG_PASSPHRASE}"; fi

# Ephemeral, private keyring; removed on exit (do NOT touch the runner's default one).
GNUPGHOME="$(mktemp -d)"
export GNUPGHOME
chmod 700 "$GNUPGHOME"
cleanup() { gpgconf --kill gpg-agent >/dev/null 2>&1 || true; rm -rf "$GNUPGHOME"; }
trap cleanup EXIT

printf '%s\n' "$GPG_PRIVATE_KEY" | gpg --batch --import
KEYID="$(gpg --list-secret-keys --with-colons --fixed-list-mode | awk -F: '$1=="sec"{print $5; exit}')"
if [ -z "$KEYID" ]; then
echo "::error::No secret key imported from GPG_PRIVATE_KEY." >&2
exit 1
fi

shopt -s nullglob
jars=("$DIR"/*-jar-with-dependencies*.jar)
shopt -u nullglob
if [ "${#jars[@]}" -eq 0 ]; then
echo "::error::No *-jar-with-dependencies*.jar found in '$DIR' to sign." >&2
exit 1
fi

for f in "${jars[@]}"; do
# Skip a signature file itself if the glob ever catches one.
case "$f" in *.asc) continue ;; esac
printf '%s' "${GPG_PASSPHRASE:-}" | gpg --batch --yes --pinentry-mode loopback \
--passphrase-fd 0 --local-user "$KEYID" --detach-sign --armor "$f"
gpg --batch --verify "$f.asc" "$f"
echo "signed + verified: $(basename "$f") -> $(basename "$f").asc"
done

echo "Signed ${#jars[@]} fat jar(s) in '$DIR'."
2 changes: 1 addition & 1 deletion .github/signing-selftest/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin <bernard.ladenthin@gmail.com>
//
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT OR Apache-2.0

// Throwaway signing project used ONLY by the `verify-signing-key-gradle`
// preflight job in .github/workflows/publish.yml. It signs a tiny throwaway Zip
Expand Down
2 changes: 1 addition & 1 deletion .github/signing-selftest/settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin <bernard.ladenthin@gmail.com>
//
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT OR Apache-2.0

rootProject.name = "signing-selftest"
32 changes: 32 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3188,9 +3188,15 @@ jobs:
# the GitHub pre-release assets must not be lost in that case.
if: ${{ !cancelled() && (needs.publish-snapshot.result == 'success' || needs.publish-snapshot.result == 'failure') && needs.package-fatjars.result == 'success' }}
runs-on: ubuntu-latest
# maven-central so the GPG_PRIVATE_KEY / GPG_PASSPHRASE secret is delivered (it is
# scoped to this environment) for signing the fat jars below. This environment has
# no approval gate (the standalone verify-signing-key jobs use it on every run), so
# declaring it here does not block the release.
environment: maven-central
permissions:
contents: write
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
with:
name: signed-snapshot-assets
Expand All @@ -3203,6 +3209,16 @@ jobs:
with:
name: llama-fatjars
path: snapshot-assets/
# GPG-sign the fat jars so each carries a detached .asc signature alongside its
# .sha256 checksum — signature parity with the thin jars (which maven-gpg signs at
# deploy) and with the BAF / srcmorph sibling fat jars. The .sha256 files (integrity)
# are kept; the .asc adds authenticity. Signed here (not in package-fatjars) because
# only this dispatch-gated path has the key. See workspace/policies/fat-jar-release-assets.md.
- name: GPG-sign the fat jars (.asc)
env:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
run: bash .github/sign-fatjars.sh snapshot-assets
- name: Update snapshot pre-release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down Expand Up @@ -3386,9 +3402,15 @@ jobs:
# the GitHub release assets must not be lost in that case.
if: ${{ !cancelled() && (needs.publish-release.result == 'success' || needs.publish-release.result == 'failure') && needs.package-fatjars.result == 'success' }}
runs-on: ubuntu-latest
# maven-central so the GPG_PRIVATE_KEY / GPG_PASSPHRASE secret is delivered (it is
# scoped to this environment) for signing the fat jars below. This environment has
# no approval gate (the standalone verify-signing-key jobs use it on every run), so
# declaring it here does not block the release.
environment: maven-central
permissions:
contents: write
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
with:
name: signed-release-assets
Expand All @@ -3401,6 +3423,16 @@ jobs:
with:
name: llama-fatjars
path: release-assets/
# GPG-sign the fat jars so each carries a detached .asc signature alongside its
# .sha256 checksum — signature parity with the thin jars (which maven-gpg signs at
# deploy) and with the BAF / srcmorph sibling fat jars. The .sha256 files (integrity)
# are kept; the .asc adds authenticity. Signed here (not in package-fatjars) because
# only this dispatch-gated path has the key. See workspace/policies/fat-jar-release-assets.md.
- name: GPG-sign the fat jars (.asc)
env:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
run: bash .github/sign-fatjars.sh release-assets
- name: Upload release assets
uses: softprops/action-gh-release@v3
with:
Expand Down
16 changes: 12 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,8 +348,11 @@ are all git-ignored (staged by CI, never committed).
Every pipeline run assembles **per-OS multi-backend server fat jars** and, on the release
paths, attaches them to GitHub: `llama-<version>-all-<os>-<arch>-jar-with-dependencies.jar`
for `linux-x86-64`, `linux-aarch64`, `windows-x86-64`, `windows-aarch64`, plus the default
CPU fat jar — each with a `.sha256` file. They are **download assets only**: the Central
deploy invocations run without the `assembly` profile and are untouched.
CPU fat jar — each with a `.sha256` **and** a detached GPG `.asc` signature. They are
**download assets only**: the Central deploy invocations run without the `assembly` profile
and are untouched. (The cross-repo "fat jar → GitHub Release, never Central, signed `.asc`"
convention shared with BAF and srcmorph is documented in
[`../workspace/policies/fat-jar-release-assets.md`](../workspace/policies/fat-jar-release-assets.md).)

Mechanism (three pieces):

Expand Down Expand Up @@ -379,8 +382,13 @@ Mechanism (three pieces):
`--chat-template chatml`): poll `/health` to 200, assert a `/v1/chat/completions` choice,
and require the loader's backend-selection log line. `publish-snapshot`/`publish-release`
`need` `package-fatjars` + both smokes (fail-loud gating); `github-release-signed` and
`github-snapshot` additionally download `llama-fatjars` into their asset directory so the
fat jars land on the tag release and the rolling `snapshot` pre-release.
`github-snapshot` additionally download `llama-fatjars` into their asset directory, then
**GPG-sign each fat jar** via `.github/sign-fatjars.sh` (a detached `.asc` alongside the
`.sha256`), so the fat jars land signed on the tag release and the rolling `snapshot`
pre-release. Both jobs declare `environment: maven-central` (where the signing key secret is
scoped; it has no approval gate) and `checkout` the repo so the script is present. Signing
happens in these attach jobs — not in `package-fatjars` — because only this dispatch-gated
release path receives the key.

A backend loading successfully but finding **zero usable devices** (e.g. CUDA toolkit
installed, no NVIDIA GPU) is benign: ggml's backend registry contributes no devices and
Expand Down
37 changes: 28 additions & 9 deletions llama/lombok.config
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,49 @@ config.stopBubbling = true

# Emit @lombok.Generated on every generated member. SpotBugs / JaCoCo /
# SonarQube special-case this annotation and skip the synthetic methods
# from coverage requirements and bug detectors.
# from coverage requirements and bug detectors. Without this, SpotBugs at
# effort=Max + threshold=Low surfaces dozens of synthetic-bytecode findings
# (USBR_UNNECESSARY_STORE_BEFORE_RETURN, IMC_IMMATURE_CLASS_NO_TOSTRING,
# NM_FIELD_NAMING_CONVENTION, ...) on every Lombok-generated method.
lombok.addLombokGeneratedAnnotation = true

# Default to "skip" on @EqualsAndHashCode / @ToString: we inherit from
# Object in almost all cases; "skip" is the right default for
# Object-extending classes. Classes that extend a non-Object base override
# per-annotation with @EqualsAndHashCode(callSuper = true) /
# @ToString(callSuper = true).
# @ToString(callSuper = true). Without this, Lombok emits a WARNING on
# every @EqualsAndHashCode without explicit callSuper, which -Werror
# promotes to a build break.
lombok.equalsAndHashCode.callSuper = skip
lombok.toString.callSuper = skip

# Force Lombok's @EqualsAndHashCode / @ToString to read FIELDS directly
# instead of routing through `this.getX()` (the default). Rationale lives
# in ../workspace/policies/lombok-config.md. Cross-repo invariant: all
# three Lombok-using repos ship the same setting. Without it,
# fb-contrib's OI_OPTIONAL_ISSUES_CHECKING_REFERENCE fires on every
# Lombok-generated `this$x == null` branch when `x` is an Optional, and
# Optional/unmodifiable-wrapper getters allocate fresh wrappers on every
# equals call.
# instead of routing through `this.getX()` (the default). Rationale:
#
# Some classes expose value-add getters that wrap their @Nullable field in
# Optional<T> or wrap a list field in Collections.unmodifiableList + Optional.
# Those wrappers are the public-API contract, not the equality contract:
#
# 1. fb-contrib's OI_OPTIONAL_ISSUES_CHECKING_REFERENCE fires on every
# Lombok-generated `this$x == null` branch when `x` is an Optional —
# Optional is the standard "never null" type, so the null branch is
# dead code.
# 2. unmodifiableList + Optional wrapper getters allocate fresh wrappers
# on every equals call. Field access avoids the allocations.
# 3. The two forms are semantically equivalent: Optional.equals and
# Collections.unmodifiableList(x).equals(...) both delegate to value-
# based comparison of the underlying state.
#
# All value classes in these repos are `final`, so subclass-override of a
# getter cannot change equality. callSuper=true chains are unaffected —
# `super.equals()` is still a method call, and the parent class's own
# field handling is governed by the same setting.
lombok.equalsAndHashCode.doNotUseGetters = true
lombok.toString.doNotUseGetters = true

# Do NOT generate Spring-style @ConstructorProperties; java.beans is not
# needed by this codebase and pulls in the desktop module on some JDKs.
lombok.anyConstructor.addConstructorProperties = false

# Allow Lombok-style accessor patterns without warnings.
lombok.accessors.flagUsage = ALLOW
Loading