254 lines
14 KiB
Markdown
254 lines
14 KiB
Markdown
# 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.
|
||
|
||
```sh
|
||
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` and `home-tokens.json` add measured shell
|
||
and Home geometry from that same prototype. Generate the committed Kotlin, CSS
|
||
and checksum manifest together:
|
||
|
||
```sh
|
||
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.
|
||
|
||
Compose output maps source weights 600/650 to the platform Bold (700) face, which
|
||
resolves consistently in native captures; the approved JSON weights stay unchanged.
|
||
Nav, picker names, labels and pill weights use that same mapping. Nav label colour
|
||
fades for 200 ms ease-out; the blob retains its critically damped spring.
|
||
|
||
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 with
|
||
the upstream Backdrop and top scrim, while retaining Wolfskin's nav, picker and
|
||
Settings list. Native Up traversal stays in content until the first focusable row,
|
||
then enters the nav. Turning the switch on renders Wolfskin Home; later-slice
|
||
screens remain explicitly labelled navigation targets.
|
||
|
||
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.
|
||
|
||
Default-flavour launcher artwork and English, Hungarian and Polish string overrides
|
||
live in `app/src/default/res/`. Legacy Wolfskin strings in 25 translated
|
||
`main` resource folders have not yet been moved; non-default `appstore`/`firetv`
|
||
flavours therefore combine upstream icons with some legacy Wolfskin text. Those
|
||
flavours and a full locale migration are not certified by this slice. 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.
|
||
|
||
## Wolfskin Home
|
||
|
||
Home is one lazy frame: a non-rotating resume → Next up → newest → pick hero,
|
||
Jellyfin shelves, per-destination "Your …" shelves and the Discover continuation.
|
||
The final action opens Discover at its top. Discover (`discover`) and See all
|
||
(`see-all:{row-id}`) are named integration targets, not implementations of those
|
||
later screens. Title navigation remains the shell's later-slice target.
|
||
|
||
All Home components and immutable screen models live in `:wolfskin-design`.
|
||
The shell's remembered view owns vertical position, each row's card and the
|
||
bounded six-hour ratings session cache. Back preserves the viewport; returning
|
||
to Home does not re-run focus-follow over a manually scrolled saved view. The
|
||
next arrow re-enters visible content when scrolling moved focus off-screen.
|
||
Pointing changes focus without activation, cards activate on key-up, and held
|
||
OK events cannot activate twice.
|
||
|
||
`HomeReadFacade` is the only Home code importing Jellyfin API clients. Release
|
||
variants read the selected user's Resume, Next up, newest titles and destination
|
||
shelves, with app-private last-known-good projections through the shared atomic
|
||
disk cache. A missing saved library becomes a recovery panel; a saved library
|
||
stays visible when Jellyfin is unreachable. Playback resolves an actual film or
|
||
episode and checks Jellyfin before opening upstream's player. No season,
|
||
recommendation stub or different person's session is promoted to playback.
|
||
|
||
Debug variants additionally package the locked fixture and local artwork under
|
||
`wolfskin-design/src/debug/assets/wolfskin-home/`; release variants exclude them.
|
||
The fixture provider supplies the approved household data and Home states without
|
||
network reads in tests. Conductarr sign-in is not implemented here: real signed-out
|
||
Home hides every rating and shows "Discover needs sign-in". No Seerr fallback is
|
||
used. Catalogue data and ratings in debug captures are fixture-backed.
|
||
|
||
Home images request exact slot sizes, use RGB565 for posters, disable crossfades,
|
||
share a 64 MiB memory/128 MiB disk cache, limit image work to four reads and prefetch
|
||
the next shelf. Rating plates use the TV locale, start at 14 sp, shrink to 12 sp
|
||
and drop trailing entries rather than wrap. Their uncached read gate is 350 ms;
|
||
cached focus returns without that delay.
|
||
|
||
Focus rings and native lift draw outside fixed slots. The Saved chip uses its
|
||
own measured geometry rather than the smaller Settings pill; caption separators
|
||
use the approved 40% opacity token. Vertical moves clamp to the short target row,
|
||
including a final See all card, rather than preserving an invalid column.
|
||
|
||
`make test` includes `HomeCaptureTest` (23 native acceptance states) and the
|
||
data-driven paths in `wolfskin-home-presses.json`: play next 2, resume 3, Discover
|
||
4, deep-row Back/Down 9, Trending 13 and Trending's See all card 33 presses.
|
||
Home captures are written to `build/wolfskin-captures/home/`. Compare those images
|
||
side by side with the frozen spec captures; Compose Roboto/JPEG rasterisation,
|
||
the approved button-scale consolidation and removal of prototype sample pills
|
||
are recorded differences, not a claim of browser pixel equality.
|
||
|
||
## 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:
|
||
|
||
```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.
|