diff --git a/.cnb.yml b/.cnb.yml index 6d167e5..6213f94 100644 --- a/.cnb.yml +++ b/.cnb.yml @@ -90,9 +90,21 @@ main: <<: *node-env ifModify: - web/** + # 触发条件包含脚本自身与 assets 目录布局:改这段逻辑、或改 Gradle 里 + # `assets.srcDirs` 指到哪,本来不会跑任何验证,而它坏掉的形态是 APK 里 + # 没有客户端、启动即白屏 —— 只有跑一遍才知道。 + - android/scripts/build-web-assets.sh + - android/scripts/verify-web-assets.sh + - android/app/build.gradle.kts + - android/app/src/main/assets/** stages: - name: stage-for-shell script: sh android/scripts/build-web-assets.sh + # 只证明脚本写出了 assets 还不够,还要证明「写出来的东西真的会被打进 APK」。 + # 脚本只知道它写到哪,Gradle 只知道哪些目录算 assets,两者之间没有任何 + # 东西把它们连起来 —— 这两条路径此前就漂移过。 + - name: verify-assets-are-packaged + script: sh android/scripts/verify-web-assets.sh # 发布镜像到制品库 `docker.cnb.cool/hectorqin/reader`。 # @@ -167,3 +179,18 @@ main: - web/** stages: - *client-check + + # PR 阶段只跑不依赖 JDK/SDK 的那半(秒级)。桥接契约断裂与渲染模式词表漂移 + # 是最容易出、最难在本地发现的错误 —— 两半分别用 Kotlin 和 TypeScript 写, + # 互相没法类型检查 —— 而它们恰好都不需要编译就能查出来。完整 assembleDebug + # 仍只在 main 的 push 上跑,PR 不该为一个 SDK 镜像等几分钟。 + - name: android-contract-checks + <<: *node-env + ifModify: + - android/** + - web/src/android-bridge.d.ts + - web/src/formats/types.ts + - web/src/ui/native-page.ts + stages: + - name: shell-contract + script: sh android/scripts/check-shell.sh diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 298d95e..fc5a1b3 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -72,10 +72,18 @@ android { * and H5 clients from becoming two implementations: one bundle, two * hosts. * - * The directory has to exist for a clean checkout to build. The - * placeholder committed under `web-assets` is what makes that work. + * `src/main/assets/web-assets` is where + * android/scripts/build-web-assets.sh stages that bundle, and the + * directory is gitignored except for a README.txt placeholder. + * + * The placeholder is not decoration. Without it the directory is + * absent from a clean checkout, and the previous version of this line + * pointed at a *third* path (`android/app/web-assets`) that nothing + * ever created — so the staged bundle was silently not an asset + * source, and the APK built fine with no client inside it. That is a + * worse failure than a red build: nothing points at it. */ - assets.srcDirs("src/main/assets", project.layout.projectDirectory.dir("web-assets").asFile) + assets.srcDirs("src/main/assets", "src/main/assets/web-assets") } } diff --git a/android/app/src/main/assets/web-assets/README.txt b/android/app/src/main/assets/web-assets/README.txt new file mode 100644 index 0000000..f87ef24 --- /dev/null +++ b/android/app/src/main/assets/web-assets/README.txt @@ -0,0 +1,6 @@ +This file exists so the directory it lives in is tracked by git. + +Gradle's `assets.srcDirs` points at this directory, and a clean checkout has to +be buildable without first running the web build. The real bundle is written +here by android/scripts/build-web-assets.sh and is gitignored, so without a +placeholder the directory simply does not exist after `git clone`. diff --git a/android/scripts/build-web-assets.sh b/android/scripts/build-web-assets.sh index b03b181..276441c 100755 --- a/android/scripts/build-web-assets.sh +++ b/android/scripts/build-web-assets.sh @@ -30,8 +30,12 @@ fi npm run build echo "==> staging assets into $dest" -rm -rf "$dest" +# Clear the previous bundle but keep README.txt. That file is tracked, and a +# blanket `rm -rf "$dest"` deletes it — leaving a dirty working tree after every +# build and turning `git status` into noise that hides real changes. It is also +# the only reason the directory exists in a clean checkout. mkdir -p "$dest" +find "$dest" -mindepth 1 ! -name 'README.txt' -exec rm -rf {} + # Only the built output. Copying the sources would ship the whole TypeScript tree # inside the APK for no benefit. cp -R "$web_dir/dist/." "$dest/" diff --git a/android/scripts/verify-web-assets.sh b/android/scripts/verify-web-assets.sh new file mode 100755 index 0000000..686dd90 --- /dev/null +++ b/android/scripts/verify-web-assets.sh @@ -0,0 +1,78 @@ +#!/bin/sh +# Proves that the staged web bundle is actually the one Gradle will pack. +# +# Why this is separate from build-web-assets.sh: that script knows one fact — +# where it writes the bundle. Gradle knows a second, independent fact — which +# directories count as assets. Nothing in the build connects them, and a mismatch +# fails in the quietest way possible: the APK builds successfully, ships with no +# client in it, and shows a blank screen on launch. Worse than a red build, +# because nothing points at it. +# +# The two facts have already drifted: `build.gradle.kts` declared +# `android/app/web-assets`, which no script ever wrote to, while the bundle went +# to `android/app/src/main/assets/web-assets` — and the placeholder the comment +# relied on had been deleted, so a clean checkout had no assets directory at all. +# +# Run after build-web-assets.sh in the same job; checks the staged output plus +# the Gradle declaration that consumes it. +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +android_dir=$(dirname -- "$script_dir") +repo_dir=$(dirname -- "$android_dir") +dest="$android_dir/app/src/main/assets/web-assets" +gradle_file="$android_dir/app/build.gradle.kts" +fail=0 + +note() { printf '%s\n' "$1"; } +err() { printf 'FAIL: %s\n' "$1" >&2; fail=1; } + +# 1. The bundle has to be there, and it has to be the built client rather than a +# placeholder. An empty directory passes a naive existence check and still +# produces a white-screen APK, which is exactly the case this guards. +note "==> checking the staged bundle exists" +if [ ! -d "$dest" ]; then + err "$dest does not exist; build-web-assets.sh did not stage anything" +else + if [ ! -f "$dest/index.html" ]; then + err "$dest has no index.html; the APK would launch to a blank screen" + fi + + # Vite emits the entry bundle at a path derived from vite.config.ts + # (`entryFileNames: assets/client.js`). Matching that exact name means a change + # to the bundler config shows up here instead of in a blank screen. + if [ ! -f "$dest/assets/client.js" ]; then + err "$dest/assets/client.js is missing; check entryFileNames in web/vite.config.ts" + fi + + count=$(find "$dest" -type f ! -name 'README.txt' | wc -l | tr -d ' ') + note " staged files (excluding the placeholder): $count" + if [ "$count" -lt 2 ]; then + err "only $count real file(s) staged; expected the built client" + fi +fi + +# 2. Gradle must declare the directory the bundle was actually written to. +# Comparing the literal path is the point: it catches a rename on either side, +# which a build-time smoke test would not. +note "==> checking Gradle consumes that directory" +if ! grep -q '"src/main/assets/web-assets"' "$gradle_file"; then + err "$gradle_file does not list src/main/assets/web-assets in assets.srcDirs; the staged bundle would be ignored" +fi + +# 3. The placeholder has to be committed, or the directory is absent in a clean +# checkout and assets.srcDirs silently points at nothing. +note "==> checking the assets directory is tracked" +if [ ! -f "$dest/README.txt" ]; then + err "$dest/README.txt is missing; a clean checkout would have no assets directory" +fi +if ! git -C "$repo_dir" ls-files --error-unmatch \ + "android/app/src/main/assets/web-assets/README.txt" >/dev/null 2>&1; then + err "the placeholder exists on disk but is not tracked by git; it will not survive a clone" +fi + +if [ "$fail" -ne 0 ]; then + note "==> web assets checks failed" + exit 1 +fi +note "==> web assets checks passed"