diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..0f2d2017 --- /dev/null +++ b/Makefile @@ -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 diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..bae48a77 --- /dev/null +++ b/docker/Dockerfile @@ -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 diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 00000000..5b56071c --- /dev/null +++ b/docker/README.md @@ -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. diff --git a/docker/gradle.sh b/docker/gradle.sh new file mode 100755 index 00000000..76950dc6 --- /dev/null +++ b/docker/gradle.sh @@ -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 diff --git a/docker/release-signing.properties b/docker/release-signing.properties new file mode 100644 index 00000000..7e15afe4 --- /dev/null +++ b/docker/release-signing.properties @@ -0,0 +1 @@ +release.signing.config=wolfskin diff --git a/docker/release-tag.sh b/docker/release-tag.sh new file mode 100755 index 00000000..14b2637c --- /dev/null +++ b/docker/release-tag.sh @@ -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)" diff --git a/docker/signing.init.gradle b/docker/signing.init.gradle new file mode 100644 index 00000000..b07a4770 --- /dev/null +++ b/docker/signing.init.gradle @@ -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 + } + } +}