Add pinned container builds and Wolfskin release signing

This commit is contained in:
justinzeus 2026-10-22 12:45:29 +02:00
parent 2d87d526db
commit 3d6f8bae1a
7 changed files with 200 additions and 0 deletions

21
Makefile Normal file
View file

@ -0,0 +1,21 @@
.DEFAULT_GOAL := apk
WOLFSKIN_BUILD_IMAGE ?= wolfskin-build:sdk37-jdk21
WOLFSKIN_GRADLE_VOLUME ?= wolfskin-gradle
WOLFSKIN_SECRETS_DIR ?= /opt/stacks/ventoy/machine-parity/manifest-tank/secrets/wolfskin
export WOLFSKIN_BUILD_IMAGE WOLFSKIN_GRADLE_VOLUME WOLFSKIN_SECRETS_DIR
.PHONY: build-image apk apk-debug test release-tag
build-image:
docker build --platform linux/amd64 --tag "$(WOLFSKIN_BUILD_IMAGE)" docker
apk: build-image
@bash docker/gradle.sh release clean assembleDefaultRelease
apk-debug: build-image
@bash docker/gradle.sh debug assembleDefaultDebug
test: build-image
@bash docker/gradle.sh release testDefaultReleaseUnitTest
release-tag:
@bash docker/release-tag.sh

17
docker/Dockerfile Normal file
View file

@ -0,0 +1,17 @@
FROM eclipse-temurin:21.0.11_10-jdk-jammy@sha256:dbfd085220ae632a0830166e443747d1ee89e9038d92e3b48c3e5e9d8292b9a7
RUN apt-get update && apt-get install -y --no-install-recommends git unzip \
&& rm -rf /var/lib/apt/lists/*
ENV ANDROID_HOME=/opt/android-sdk ANDROID_SDK_ROOT=/opt/android-sdk
ENV PATH="/opt/android-sdk/cmdline-tools/23.0/bin:/opt/android-sdk/build-tools/36.0.0:${PATH}"
RUN curl -fsSL https://dl.google.com/android/repository/commandlinetools-linux-16111833_latest.zip -o /tmp/tools.zip \
&& echo 'e025545c62a8e64c7559119566a569fb1dec5f60 /tmp/tools.zip' | sha1sum -c - \
&& mkdir -p "$ANDROID_HOME/cmdline-tools" \
&& unzip -q /tmp/tools.zip -d /tmp/android-tools \
&& mv /tmp/android-tools/cmdline-tools "$ANDROID_HOME/cmdline-tools/23.0" \
&& rm -rf /tmp/tools.zip /tmp/android-tools \
&& yes | sdkmanager --licenses >/dev/null \
&& sdkmanager 'platforms;android-37.0' 'build-tools;36.0.0' \
&& mkdir -m 1777 /gradle
ENV GRADLE_USER_HOME=/gradle HOME=/tmp
COPY signing.init.gradle /opt/wolfskin/signing.init.gradle

93
docker/README.md Normal file
View file

@ -0,0 +1,93 @@
# Wolfskin TV container builds
Run from a full Git checkout with the upstream `v*` and `p*` tags available. The
host needs Docker, GNU Make, Bash, and Git, not Java or the Android SDK.
```sh
git fetch origin --tags
make apk # clean, signed default release: three ABI splits + universal
make test # upstream testDefaultReleaseUnitTest in the same image
make apk-debug # default debug, no secrets; MPV stub and no extra codec decoders
```
APKs remain under `app/build/outputs/apk/default/{release,debug}/`. Debug builds
are not playback-complete and must not be distributed as releases. Upstream's
`.github/workflows` and dependency version catalog are unchanged; release builds
use the catalog's exact Wholphin extension version (currently `0.2.4`). Do not
place local extension AARs in `app/libs`: upstream prefers them over the pinned
GitHub Packages dependencies.
## Toolchain and disk use
`Dockerfile` pins Temurin JDK `21.0.11+10` by image digest, Android command-line
tools `23.0` by archive and checksum, SDK platform `37.0`, and build-tools
`36.0.0`. Gradle comes from the committed wrapper. The image is built for
`linux/amd64`; the default local image tag is `wolfskin-build:sdk37-jdk21`.
Upstream's build timestamp means APKs are not byte-for-byte reproducible.
Gradle downloads and caches live in the named volume `wolfskin-gradle`, not
`/tmp`. Module outputs stay in the checkout. Builds run with the host UID/GID,
`--no-daemon`, and at most four workers. The repository's Git metadata is mounted
read-only, including when the checkout is a linked worktree.
Override `WOLFSKIN_BUILD_IMAGE` to change the locally built image tag and
`WOLFSKIN_GRADLE_VOLUME` to isolate a cache. `make build-image` builds only the
image. Do not delete shared caches during another build.
## Release credentials
The default escrow is
`/opt/stacks/ventoy/machine-parity/manifest-tank/secrets/wolfskin/`. Check
`mountpoint -q /mnt/ventoy` before using that path. Set `WOLFSKIN_SECRETS_DIR`
to use another existing directory. Its required files are:
| File | Contents |
| --- | --- |
| `release.jks` | Separate Wolfskin PKCS12 keystore, RSA 4096, alias `wolfskin` |
| `release.properties` | Java properties `storePassword` and `keyPassword` |
| `github-packages-token` | GitHub token with `read:packages`, username `JustinZeus` |
The directory must be mode `0700`, each file mode `0600`, owned by the building
user. Never commit them or put their contents in logs. Back up the keystore and
passwords together: losing the release key forces a reinstall. Do not reuse
wolfskin-android's key or the gh CLI login token.
Release builds mount escrow read-only. The token is loaded inside the container
into upstream's `ORG_GRADLE_PROJECT_WholphinExtensionsUsername` and
`ORG_GRADLE_PROJECT_WholphinExtensionsPassword` environment hooks, never a Docker
command argument. The signing init script registers `wolfskin`; the nonsecret
`release-signing.properties` is mounted as `local.properties` so upstream's
`release.signing.config` hook selects it. Any host `local.properties` is hidden
only for the duration of this container. Keystore and passwords stay outside the
checkout and image. Missing credentials fail the release rather than silently
shipping the MPV stub.
## Versioning and local release markers
Upstream computes `versionName` from
`git describe --tags --long --match='v*'`, dropping the leading `v`, and computes
`versionCode` from the count of all local `v*` and `p*` tags. Preserve and fetch
those tags; a shallow or incomplete checkout can give an incorrect version.
Use the full `git describe` output as the human release name.
After committing the intended release, with a clean checkout and fetched tags:
```sh
make release-tag
make apk
```
The helper creates one lightweight `pwolfskin-YYYY_MM_DD` tag in UTC, appending
`_1`, `_2`, etc. on collision. Each marker adds one to `versionCode` without
changing upstream's `v*`-based version name. It never pushes tags, publishes a
release, or installs an APK. Do not delete published release markers. Tag
creation and publication must be serialized between release operators.
## Inspecting release artifacts
Use `apksigner verify --verbose --print-certs APK` and
`aapt2 dump badging APK` inside the build image. Compare the signer SHA-256 with
`keytool -list -v -alias wolfskin -keystore /path/to/release.jks` (enter its
password privately). Inspect APK ZIP entries for real native MPV and ffmpeg
libraries for each shipped ABI; a successful Java compilation alone does not
prove that codec extensions are included.

33
docker/gradle.sh Executable file
View file

@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Run the upstream wrapper with a persistent cache and read-only build credentials.
set -euo pipefail
mode=$1
shift
root=$(git rev-parse --show-toplevel)
git_common=$(git rev-parse --path-format=absolute --git-common-dir)
image=${WOLFSKIN_BUILD_IMAGE:-wolfskin-build:sdk37-jdk21}
cache=${WOLFSKIN_GRADLE_VOLUME:-wolfskin-gradle}
secrets=${WOLFSKIN_SECRETS_DIR:-/opt/stacks/ventoy/machine-parity/manifest-tank/secrets/wolfskin}
arguments=(--rm --platform linux/amd64 --user "$(id -u):$(id -g)"
--mount "type=bind,src=$root,dst=$root" --workdir "$root"
--mount "type=volume,src=$cache,dst=/gradle"
--mount "type=bind,src=$git_common,dst=$git_common,readonly")
gradle_arguments=(--no-daemon --console=plain --max-workers=4)
if [[ $mode == release ]]; then
for name in release.jks release.properties github-packages-token; do
if [[ ! -s $secrets/$name ]]; then
printf 'Missing build credential: %s/%s\n' "$secrets" "$name" >&2
exit 1
fi
done
arguments+=(--mount "type=bind,src=$secrets,dst=/run/wolfskin,readonly"
--mount "type=bind,src=$root/docker/release-signing.properties,dst=$root/local.properties,readonly")
gradle_arguments+=(--init-script /opt/wolfskin/signing.init.gradle)
docker run "${arguments[@]}" "$image" bash -euc '
export ORG_GRADLE_PROJECT_WholphinExtensionsUsername=JustinZeus
export ORG_GRADLE_PROJECT_WholphinExtensionsPassword=$(cat /run/wolfskin/github-packages-token)
exec ./gradlew "$@"
' bash "${gradle_arguments[@]}" "$@"
else
docker run "${arguments[@]}" "$image" ./gradlew "${gradle_arguments[@]}" "$@"
fi

View file

@ -0,0 +1 @@
release.signing.config=wolfskin

18
docker/release-tag.sh Executable file
View file

@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Add one lightweight release marker without publishing it.
set -euo pipefail
if [[ -n $(git status --porcelain) ]]; then
printf 'Commit or discard checkout changes before tagging a release.\n' >&2
exit 1
fi
base="pwolfskin-$(date -u +%Y_%m_%d)"
tag=$base
sequence=1
while git show-ref --verify --quiet "refs/tags/$tag"; do
tag="${base}_${sequence}"
sequence=$((sequence + 1))
done
git tag "$tag"
printf 'Created %s locally; no tag was pushed.\n' "$tag"
printf 'versionName: %s\n' "$(git describe --tags --long --match='v*' | cut -c 2-)"
printf 'versionCode: %s\n' "$(git tag --list 'v*' 'p*' | wc -l)"

View file

@ -0,0 +1,17 @@
// Keep signing material outside the checkout; upstream selects this configuration via local.properties.
allprojects {
pluginManager.withPlugin('com.android.application') {
def passwords = new Properties()
file('/run/wolfskin/release.properties').withInputStream { passwords.load(it) }
android.signingConfigs.create('wolfskin') {
storeFile = file('/run/wolfskin/release.jks')
storePassword = passwords.getProperty('storePassword')
keyPassword = passwords.getProperty('keyPassword')
keyAlias = 'wolfskin'
enableV1Signing = true
enableV2Signing = true
enableV3Signing = true
enableV4Signing = true
}
}
}