wolfskin/docker/README.md

12 KiB
Raw Blame History

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, Git, and flock (util-linux), not Java or the Android SDK.

git fetch origin --tags
make apk        # clean, signed default release: three ABI splits + universal
make test       # app default/debug suite plus wolfskin-design JVM/capture suite
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.

Upload release assets renamed to Wolfskin.apk / Wolfskin-<abi>.apk for the in-app updater. Gradle outputs Wholphin-default-release-<versionName>-<versionCode>[-<abi>].apk; strip the flavor, version, and code as well as replacing the prefix.

Upstream enables JVM unit tests only for debug variants; there is no testDefaultReleaseUnitTest task. make test uses the closest default-flavor task without changing upstream's test configuration.

Upstream v1.1.0 integration

The release merge brings Kotlin 2.4.21, Gradle 9.8.2, AGP 9.5.0, and ExoplayerHdrUtils 0.4.1. The pinned SDK/JDK image and serialized Gradle wrapper remain compatible without changes; native Wholphin extensions remain 0.2.4. There are no committed dependency lockfiles or Gradle verification metadata. Forgejo Actions stays disabled; upstream GitHub workflows remain dormant.

The app database upgrades from Room schema 35 through 40 to 41 on first launch after installation. The upstream migrations add series track choices and a home settings source preference. Downgrading with the upgraded database is not supported: a TV installation is a one-way step and requires explicit approval. The optional companion plugin can supply Discover credentials before locally saved Seerr profiles; do not configure an administrator API key in place of the household's per-profile Jellyfin login, which Conductarr uses for request routing.

Build only after committing a release merge: version names come from Git HEAD, not the working tree. An uncommitted merge still builds with the old version name. Neither an upstream merge nor its validation build authorizes a release or TV installation.

Wolfskin shell

Wolfskin UI lives in app/src/wolfskin/java, beside upstream source. The signed-in entry in MainContent.kt calls WolfskinApp; Wolfskin-only routes use the single Destination.Wolfskin subtype in the existing navigation stack. Other destinations continue through upstream DestinationContent. Full-screen player/settings pages cover the Wolfskin nav.

The independent :wolfskin-design module owns the shell components, fixed person colours, focus policy, per-person root-view memory, palette and motion. Its tokens.json is the copied approved TV-spec source, retaining the locked prototype SHA and provenance. shell-tokens.json adds shell and grid measurements from that same prototype. Generate the committed Kotlin, CSS and checksum manifest together:

python3 wolfskin-design/generate_tokens.py
python3 wolfskin-design/generate_tokens.py --check

preBuild checks the manifest against the source JSON, generator and generated outputs. Editing any of them without regenerating fails the build. The shell uses the generated palette rather than the retired WOLFSKIN theme-enum value; the old numeric value is reserved.

The small gear under the final person opens Settings with Requests focused. Settings groups hand off to existing upstream pages; the Requests entry explicitly has no request data yet. Advanced settings → Wolfskin screens is the persistent rollout switch: turning it off renders upstream Home and library destinations, while retaining Wolfskin's nav, picker and Settings list. Turning it on renders the labelled placeholders until the corresponding screen slices ship.

Debug people, tabs and initials come from the shell-only projection of the locked prototype fixture in app/src/debug/assets/wolfskin-shell.json; release resources do not package it. Saved Jellyfin identities use existing upstream services. The shell makes no Conductarr calls and does not add Conductarr sign-in.

Native Roborazzi captures use Robolectric SDK 34 and w960dp-h540dp-xhdpi (1920×1080 at density 2.0). make test writes capture PNGs and the sampled Compose nav-spring curve to build/wolfskin-captures/. Key replays exercise the Android event adapter, including the 6/8/10/11-press shell paths. The profile hand-off capture targets the real HomeSettingsPage; Playback is a separate press-count path.

Robolectric SDK 28/30/34 runtimes resolve through Gradle's existing persistent dependency cache, then copy to wolfskin-design/build/robolectric-sdk/. Test workers use that local directory in offline mode instead of downloading Android JARs into the container's ephemeral home. A cache miss still needs Maven Central; rate-limited hosts can seed the existing cache from SHA512-verified local Maven artifacts without changing the wrapper or adding volumes.

Branding strings and launcher artwork live in app/src/default/res/. Debug assembly runs checkWolfskinBranding against every packaged APK through AGP's public artifact provider and AAPT2. It checks all string, plural and array configurations, rejecting user-visible upstream branding while preserving the upstream legal attribution and project URL. The check does not depend on incremental merge files.

Libraries and See all

Movies, Shows and Anime use the shell's retained view and a continuous six-column lazy grid. Up from its first row reaches Sort/Filter, then the nav; Back preserves the card and scroll. Sort cycles A–Z, Recently added and Rating. The letter rail exists only at 200 titles with A–Z selected. Card OK activates on key-up through the same shell hold guard, without a long-press menu.

The debug-only wolfskin-library.json and library/assets/ contain the locked prototype's titles and local poster art. Release libraries read through LibraryReadFacade, with 200-item Jellyfin chunks and ten-minute per-server/person snapshots using the existing atomic disk cache. Saved data is painted before a quiet refresh; a failed refresh keeps it. Production ratings remain hidden until Conductarr sign-in exists.

Home/Discover shelves open a full list through WolfskinSeeAllLists.open(person, key, title, fullList, navigation, loadNextPage). The optional suspend page provider returns LibraryPage(items, hasMore) using the generated Conductarr client. The grid reads within ten cards of its end, deduplicates title identities, and pauses on Try again or Show more rather than looping. Title activation pushes the explicitly named library-title/{id} destination for the title-screen implementation; this slice does not implement title pages.

LibraryCaptureTest renders the sixteen approved library/See all states offline. LibraryKeysTest reads the approved JSON path and reaches Spirited Away in exactly 20 Android key presses. Paging and snapshot tests cover duplicate-only reads, failure/retry, stale copies, and person/server isolation. Captures are written to the same build/wolfskin-captures/ directory as the shell captures.

Toolchain and disk use

Dockerfile pins Temurin JDK 21.0.11+10 by image digest, command-line tools 23.0 and platform-tools 37.0.1 by archive and checksum, and build-tools 36.0.0 by SDK package version. The SDK manager registers platform 37.0 and its licenses; the build rejects any platform revision other than 2. An SDK revision upgrade therefore requires a deliberate Dockerfile update. 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.

Every Gradle container holds a host-side exclusive lock for its entire run. Concurrent targets sharing a cache queue rather than timing out on Gradle's cross-container cache journal lock; a waiting invocation prints one line. The default lock is /var/tmp/${WOLFSKIN_GRADLE_VOLUME}.gradle.lock (using wolfskin-gradle when unset). Override WOLFSKIN_GRADLE_LOCK_FILE with a path whose parent directory already exists. All callers sharing a volume must use the same lock path on the Docker host. Do not delete the file while builds are running or waiting. The lock releases automatically when the invocation exits.

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:

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.