docs(android): fix plan (dict source, Capacitor 8 / SDK 36 / JDK 21) and record scaffolding progress + native-build notes
Correct ANDROID_PLAN.md: bundled offline dictionaries come from the scrabble-dictionary release (DICT_VERSION), not scrabble-solver; pin the toolchain to Capacitor 8 (compileSdk/targetSdk 36, minSdk 24, JDK 21 — @capacitor/android compiles at Java 21). Add a Progress section marking the scaffolding milestone done, and capture the native-Android build recipe + toolchain gotchas in .claude/CLAUDE.md so a fresh session resumes without re-deriving them. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,36 @@ on it**, and prefer the authoritative docs where they overlap. Add to this file
|
||||
- **pnpm corepack pre-flight flakes.** `pnpm exec` / `pnpm check` occasionally abort on a corepack
|
||||
pre-flight. Dodge it by invoking the tool directly: `node_modules/.bin/<tool>`.
|
||||
|
||||
## Native Android build (Capacitor)
|
||||
|
||||
The native Android app (`ANDROID_PLAN.md`) is a Capacitor 8 wrapper of the `ui` SPA, scaffolded under
|
||||
`ui/` (a Node project outside `go.work`). Hard-won bring-up facts — verify against current code:
|
||||
|
||||
- **Capacitor 8 needs JDK 21, not 17.** `@capacitor/android` compiles at `VERSION_21`; a JDK 17 Gradle
|
||||
run dies with `error: invalid source release: 21`. Install sudo-free via the Homebrew **formula**
|
||||
`brew install openjdk@21` (the `temurin@21` **cask** wants sudo for a system `.pkg`, unusable
|
||||
non-interactively) + a user symlink into `~/Library/Java/JavaVirtualMachines/` so
|
||||
`/usr/libexec/java_home -v 21` finds it. JDK 17 stays fine for `sdkmanager`/`avdmanager`.
|
||||
- **Cap 8 SDK pins:** compileSdk/targetSdk **36**, minSdk **24**, Gradle **8.14.3**, AGP **8.13.0**
|
||||
(`ui/android/variables.gradle`). Install `platforms;android-36` — Android Studio's newer
|
||||
`android-36.1` does NOT satisfy compileSdk 36.
|
||||
- **SDK is Android Studio's** at `~/Library/Android/sdk` (no `cmdline-tools` by default → `brew install
|
||||
--cask android-commandlinetools`, then `sdkmanager`/`avdmanager --sdk_root=$HOME/Library/Android/sdk`).
|
||||
Gradle finds it via **`ANDROID_HOME`** (`local.properties` is gitignored, machine-specific).
|
||||
- **Build recipe:** `cd ui && pnpm build && node_modules/.bin/cap sync android`; then `cd ui/android &&
|
||||
ANDROID_HOME=~/Library/Android/sdk JAVA_HOME=$(/usr/libexec/java_home -v 21) ./gradlew assembleDebug`
|
||||
→ `ui/android/app/build/outputs/apk/debug/app-debug.apk`. Prefer the local `ui/node_modules/.bin/cap`
|
||||
(dodges the corepack flake).
|
||||
- **Emulator smoke:** existing AVDs `Pixel_10` / `Pixel_4_Android_10_API_29` / `Pixel_Android_9`;
|
||||
`emulator -avd <name>`, `adb install -r <apk>`, `adb shell monkey -p ru.eruditgame.app -c
|
||||
android.intent.category.LAUNCHER 1`, `adb exec-out screencap -p > x.png`. The boot wait needs `sleep`
|
||||
→ run it as a **background** Bash task (foreground `sleep` is blocked).
|
||||
- **`sharp` is whitelisted** in `ui/pnpm-workspace.yaml` (`allowBuilds: sharp: true`) — `@capacitor/assets`
|
||||
uses it for `pnpm android:assets` (launcher icon/splash); else pnpm 11 raises `ERR_PNPM_IGNORED_BUILDS`.
|
||||
- **Bundled offline dicts come from the `scrabble-dictionary` release** (`scrabble-dawg-<DICT_VERSION>.tar.gz`,
|
||||
keyed on the `DICT_VERSION` Gitea var — the same source the backend image + CI `curl`), NOT
|
||||
`scrabble-solver/dawg` (those are the solver's pinned test fixtures).
|
||||
|
||||
## Wire / schema evolution (client ↔ gateway ↔ backend)
|
||||
|
||||
- **FBS is additive-only.** Add **trailing** fields ("added trailing — backward-compatible"). Never
|
||||
|
||||
+56
-17
@@ -45,6 +45,7 @@ installs away cleanly, and (c) opens offline-first as a soft guest with local pl
|
||||
| Payments in MVP | Hidden (deferred; reuse the distribution flag) |
|
||||
| appId (permanent) | **`ru.eruditgame.app`** |
|
||||
| App display name | **`Эрудит`** (Cyrillic) |
|
||||
| Toolchain (locked) | **Capacitor 8** (`@capacitor/*` `^8`); **compileSdk/targetSdk 36, minSdk 24**; **JDK 21** (required — `@capacitor/android` compiles at Java 21; AGP 8.13 / Gradle 8.14.3); Android SDK `platforms;android-36` + `build-tools;36.x` |
|
||||
|
||||
**Conventions:** all code/comments/commits/docs in English. Do NOT put stage/phase numbers in code,
|
||||
commits, or PR titles. Bake design into the main docs (`docs/ARCHITECTURE.md` etc.) — this repo
|
||||
@@ -57,23 +58,52 @@ client-only (no server change).
|
||||
|
||||
---
|
||||
|
||||
## Progress (as-built)
|
||||
|
||||
Kept current as parts land so a fresh session resumes without re-deriving. Verify against code.
|
||||
|
||||
- **A. Capacitor scaffolding — ✅ DONE & verified (2026-07-12).** Capacitor 8 native project generated
|
||||
under `ui/android/` (tracked), `@capacitor/*` deps + `capacitor.config.ts`, hardware Back in
|
||||
`ui/src/lib/native.ts` wired from `App.svelte`. `pnpm build` → `cap sync` → `./gradlew assembleDebug`
|
||||
builds a debug APK that installs and **cold-launches to the Login screen** on the Pixel_10 emulator.
|
||||
`svelte-check` 0 errors, `vitest` 584 passed. Build recipe + toolchain gotchas live in
|
||||
`.claude/CLAUDE.md` → "Native Android build (Capacitor)".
|
||||
**Remaining sub-item:** owner-supplied launcher icon (`ui/assets/icon.png`) → `pnpm android:assets`.
|
||||
- **B–G — pending.**
|
||||
|
||||
Open logistics (not code): the launcher icon (above); `~/.claude/bin/gitea-ci-watch.py` must exist
|
||||
before the first push/PR; RuStore publication prerequisites gate release only.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites to start DEVELOPMENT (on the other device)
|
||||
|
||||
Have all of these before writing code:
|
||||
|
||||
- **JDK 17** (Temurin/OpenJDK).
|
||||
- **JDK 21** (Temurin/OpenJDK) — **required** by Capacitor 8: `@capacitor/android` sets
|
||||
`sourceCompatibility/targetCompatibility = VERSION_21`, so a JDK 17 Gradle run fails with
|
||||
`invalid source release: 21`. (Homebrew: `brew install openjdk@21`.)
|
||||
- **Android SDK** — Android Studio (recommended: bundles the SDK manager + AVD emulator) *or*
|
||||
cmdline-tools. Install: `platform-tools`, `platforms;android-34`, `build-tools;34.0.0`.
|
||||
cmdline-tools. Capacitor 8 targets **compileSdk/targetSdk 36, minSdk 24**, so install:
|
||||
`platform-tools`, `platforms;android-36`, `build-tools;36.0.0` (a newer build-tools such as
|
||||
`36.1.0` is also accepted — AGP treats it as a minimum). Gradle 8.14.3 / AGP 8.13 arrive via the
|
||||
wrapper `cap add android` generates.
|
||||
- **Node 20+ and pnpm** (via corepack), matching the repo's `ui` toolchain.
|
||||
- **Git**, plus this repo's Gitea access. If this device will push/PR, set up the `tea` CLI login
|
||||
and the CI watcher exactly as the owner's global setup (`~/.claude` tooling:
|
||||
`python3 ~/.claude/bin/gitea-ci-watch.py`, `tea` against `gitea.lan`).
|
||||
- **Both repositories side by side** per `go.work`: clone `developer/scrabble-game` **and** the
|
||||
sibling `scrabble-solver` next to it. The sibling holds the committed dictionaries
|
||||
(`scrabble-solver/dawg/{ru_scrabble,ru_erudit,en_sowpods}.dawg`) that offline-first bundles, and
|
||||
the solver library the backend/CI use.
|
||||
- **A test target**: a physical Android device with USB debugging **or** an emulator AVD (e.g. Pixel,
|
||||
API 34).
|
||||
sibling `scrabble-solver` next to it — the solver **library** the backend/CI consume via the
|
||||
`go.work` replace. **The bundled dictionaries do NOT come from the solver.** The versioned,
|
||||
production dictionary set is published by `developer/scrabble-dictionary` as a **release artifact**
|
||||
`scrabble-dawg-<DICT_VERSION>.tar.gz` (one semver label per set); `backend/Dockerfile` and every CI
|
||||
job already `curl` exactly that tarball, keyed on the shared Gitea variable `DICT_VERSION`.
|
||||
Offline-first bundles the DAWGs from that same release (see D.1 / E), so the Android build needs
|
||||
`DICT_VERSION` + network access to the release, **not** `scrabble-solver`. (`scrabble-solver/dawg/*.dawg`
|
||||
are the solver's committed test fixtures — byte-identical to a build but pinned to the solver
|
||||
commit, not the versioned production set.)
|
||||
- **A test target**: a physical Android device with USB debugging **or** an emulator AVD (e.g. a
|
||||
Pixel AVD; any recent API image).
|
||||
- **No backend needed to build the APK** — it points at production `erudit-game.ru`. A local gateway
|
||||
is only needed to exercise the version gate locally (optional).
|
||||
|
||||
@@ -152,7 +182,7 @@ it and stay a local guest (do not overlay). Implementation seam: the reconciliat
|
||||
Ordered so each part is independently verifiable. The MVP now includes offline-first, so the sequence
|
||||
is: scaffold → native-correct → gate → offline-first → CI → docs → release.
|
||||
|
||||
### A. Capacitor scaffolding
|
||||
### A. Capacitor scaffolding — ✅ DONE
|
||||
|
||||
- `ui/package.json`: add deps `@capacitor/core`, `@capacitor/android`, `@capacitor/app`; devDeps
|
||||
`@capacitor/cli`, `@capacitor/assets`. Scripts: `"cap:sync": "cap sync android"`,
|
||||
@@ -339,9 +369,13 @@ and `ui/src/screens/NewGame.svelte:177-431`. This phase makes them reachable on
|
||||
with no network**.
|
||||
|
||||
1. **Bundle dictionaries in the APK.**
|
||||
- New build step `ui/scripts/bundle-dicts.mjs`: copy `../scrabble-solver/dawg/{ru_scrabble,ru_erudit,en_sowpods}.dawg`
|
||||
→ `ui/dist/dict/<variant>@<DICT_VERSION>.dawg` (dictKey naming). Run only in the native pipeline
|
||||
(after `pnpm build`, before `cap sync`). Web builds stay slim.
|
||||
- New build step `ui/scripts/bundle-dicts.mjs`: copy the three DAWGs from the unpacked
|
||||
**`scrabble-dictionary` release** (`scrabble-dawg-<DICT_VERSION>.tar.gz` → a dict dir passed via
|
||||
`DICT_DIR`; **NOT** `../scrabble-solver/dawg`) → `ui/dist/dict/<dictKey>.dawg` (the
|
||||
`dictKey(variant, version)` naming the loader's bundled tier fetches). Run only in the native
|
||||
pipeline (after `pnpm build`, before `cap sync`). Web builds stay slim. `VITE_DICT_VERSION` must
|
||||
equal the release's `DICT_VERSION` so the bundled `(variant, version)` matches what the loader
|
||||
requests.
|
||||
- Vite `define`: add `__DICT_VERSION__` from `VITE_DICT_VERSION` (mirrors `__APP_VERSION__`);
|
||||
declare it in `vite-env.d.ts`.
|
||||
- **Loader tier** — `ui/src/lib/dict/loader.ts` `load()`: between the IndexedDB tier (line 73-85)
|
||||
@@ -394,15 +428,20 @@ silently establishes a server guest and lights up online play.
|
||||
|
||||
New **manual** workflow `.gitea/workflows/android-build.yaml` (mirror `prod-deploy.yaml`'s
|
||||
`workflow_dispatch` + `confirm` gate; from `master`; not on PRs). Steps:
|
||||
1. Checkout this repo **and the sibling `scrabble-solver`** — copy the checkout + Node/pnpm setup
|
||||
block from `.gitea/workflows/ci.yaml` (the `ui` job). The sibling is needed for the bundled dicts.
|
||||
1. Checkout this repo; copy the Node/pnpm setup block from `.gitea/workflows/ci.yaml` (the `ui` job).
|
||||
Add the **"Fetch dictionary DAWGs"** step verbatim from `ci.yaml` (the `curl …
|
||||
scrabble-dictionary/releases/download/${DICT_VERSION}/scrabble-dawg-${DICT_VERSION}.tar.gz` +
|
||||
untar into `${GITHUB_WORKSPACE}/dawg`), keyed on the `DICT_VERSION` Gitea variable — **this**, not
|
||||
the `scrabble-solver` sibling, is the source of the bundled dicts. (`scrabble-solver` is not needed
|
||||
by the Android build: `ui` is a Node project outside `go.work`.)
|
||||
2. `pnpm install --frozen-lockfile`.
|
||||
3. `pnpm build` with the native env (below), `VITE_APP_VERSION` = `git describe --tags`,
|
||||
`VITE_DICT_VERSION` = the release dict version.
|
||||
4. `node ui/scripts/bundle-dicts.mjs` (copy the `.dawg` into `ui/dist/dict/`).
|
||||
5. Setup JDK 17 (`actions/setup-java@v4`, temurin 17).
|
||||
6. Install Android SDK via `sdkmanager` (pin `platform-tools`, `platforms;android-34`,
|
||||
`build-tools;34.0.0`); cache the SDK. **No emulator** — assemble only; on-device smoke is manual.
|
||||
4. `DICT_DIR=${GITHUB_WORKSPACE}/dawg node ui/scripts/bundle-dicts.mjs` (copy the release `.dawg`
|
||||
files into `ui/dist/dict/`).
|
||||
5. Setup JDK 21 (`actions/setup-java@v4`, temurin 21).
|
||||
6. Install Android SDK via `sdkmanager` (pin `platform-tools`, `platforms;android-36`,
|
||||
`build-tools;36.0.0`); cache the SDK. **No emulator** — assemble only; on-device smoke is manual.
|
||||
7. `npx cap sync android`.
|
||||
8. Decode the keystore from `ANDROID_KEYSTORE_BASE64`; compute `versionCode`/`versionName` from the
|
||||
tag (scheme below).
|
||||
|
||||
Reference in New Issue
Block a user