wolfskin/docker/README.md

300 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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` adds shell and grid measurements;
`title-tokens.json` adds title, performer and playback-sheet measurements 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.
Settings pills have a separate 1 dp stroke (2 px on the stage), not a focus ring.
The stroke is inset within the fixed 20 dp / 40 px height; horizontal border-box
padding includes it. The 6 px dash / 4 px gap follows the browser's 3:2 stroke-width
ratios. Resting borders are white at 22% opacity; focused borders use ink at 40%.
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 restores 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.
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.
## 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.
Down clamps a short final title row before entering the separate Try again / Show
more row; an empty slot cannot activate recovery. An unavailable library hides
its unknown item count.
Caption separators use 40% of the existing muted colour's alpha. The rail follows
the locked prototype CSS: bold 24 px letters, with the focused letter bold 28 px,
overriding the spec table's generic caption style for this component.
Library header initials use the muted colour over the person's fill; navigation,
picker and Settings avatars retain their dark initials.
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 opens `title/{id}` through `Destination.Wolfskin` for prototype
identities, or the existing typed media destination for real Jellyfin identities.
Movies, series, episodes and people use the native title family; collections and
music keep their upstream destinations.
`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.
## Title pages and people
`TitleSurface` owns the film/series/episode header, season chips, episode tiles,
cast and related shelves. `PersonSurface` owns the real biography and continuous
six-column filmography. Both reuse the shell/nav/back ladder, `LibraryPoster`,
the adaptive `LibraryRatingPlate`, shared captions and the single bounded
`LibraryImages` loader. Image requests use exact slot sizes, RGB565 for opaque
posters, no crossfade, four concurrent reads and next-row prefetch. The fixed
title backdrop remains visible behind deep rows; only off-screen row images
leave composition.
The options row preserves complete labels beyond the copy-column measure.
Version choices close after selection; Audio and subtitles stays open, including
an explicit Off choice. Real source IDs use the Jellyfin SDK UUID parser, and
stream indexes remain the server's indexes rather than positions in a list.
Selections persist through the same playback repository as the upstream player.
Marking a whole series asks first; an explicit episode remains the exact play and
watched target even while another season is selected. Nested person/title visits
retain their entry's focus and selection state.
Hardware and remote Back share the sheet/picker/request-overlay precedence, then
pop a pushed title/person from content **or** nav. Initial loading has no content
target: Down stays in nav, while Back can still leave the title. A missing rating
set emits no layout node, so it cannot reserve an invisible extra facts gap.
`TitleReadFacade` confines SDK reads, server/user-scoped last-good disk copies,
person credits and playback mutations to one boundary. Cached content appears
before refresh. Production ratings stay hidden until Conductarr sign-in exists.
Debug-only `wolfskin-titles.json` and local `wolfskin-titles/assets/` project the
locked household fixture, including portraits; release never packages them.
Those synthetic IDs are not invented Jellyfin playback IDs. Real held-title
playback uses the existing player; debug catalogue identities cannot play a file.
Held trailers use Jellyfin remote/local trailer data. YouTube URLs target an
installed YouTube handler such as SmartTube, never a browser. No handler leaves
the title visible with a calm message. Titles nobody holds have no Trailer
button until Conductarr's trailer field exists. Conductarr sign-in and request
submission/receipt workflows belong to their separate slices; title pages do not
pretend those operations succeeded or fall back to Seerr.
`TitleCaptureTest` writes the 34 approved title/person/options frames and native
heading layout measurements at 1920×1080, density 2.0. The approved typeface ruling
keeps platform Roboto rather than bundling the browser's DejaVu Sans: there is no
per-title compensation and no wrapping-parity claim. `TitleKeysTest` replays
real Android key events from title entry, including modal Back, held OK, nearest
row entry, remembered columns, pending seasons and mouse-wheel re-entry.
`title-press-paths.json` also retains the complete launch paths and their entry
boundaries; their Home prefixes require the Home surface before end-to-end replay.
SDK cache, exact-episode, track-index, identity, locale-layout and trailer-handler
tests run entirely offline with in-process service replies and local art.
## 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.