From 768546fdc7609319e4cd6b230c10dfea23aef646 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sun, 21 Jun 2026 21:13:33 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E7=A7=BB=E5=8A=A8=E5=8E=9F?= =?UTF-8?q?=E7=94=9F=E5=8C=85=E4=BA=A7=E7=89=A9=E9=AA=8C=E6=94=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增移动壳 APK 与 iOS simulator 包产物检查脚本 新增根级 mobile-shell:build-artifacts 验收入口 移动壳配置门禁反查产物验收脚本与真实包内容检查 同步宿主壳方案文档和项目决策记录 --- apps/mobile-shell/package.json | 1 + .../scripts/check-build-artifacts.mjs | 105 ++++++++++++++++++ apps/mobile-shell/scripts/check-config.mjs | 26 +++++ .../shared-memory/decision-log.md | 8 ++ ...ExpoReactNative与Tauri宿主壳方案-2026-06-17.md | 2 +- package.json | 1 + 6 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 apps/mobile-shell/scripts/check-build-artifacts.mjs diff --git a/apps/mobile-shell/package.json b/apps/mobile-shell/package.json index b0772a489..7a146bc03 100644 --- a/apps/mobile-shell/package.json +++ b/apps/mobile-shell/package.json @@ -11,6 +11,7 @@ "test": "vitest run -c vitest.config.ts", "build:android": "eas build --local --profile production --platform android --output ../../build/native/mobile/genarrative-mobile-android.apk", "build:ios": "eas build --local --profile production-simulator --platform ios --output ../../build/native/mobile/genarrative-mobile-ios-simulator.tar.gz", + "build-artifacts:smoke": "node scripts/check-build-artifacts.mjs", "build-config:smoke": "node scripts/check-eas-build-config.mjs", "config:smoke": "node scripts/check-expo-config.mjs", "export:smoke": "node scripts/check-expo-export.mjs", diff --git a/apps/mobile-shell/scripts/check-build-artifacts.mjs b/apps/mobile-shell/scripts/check-build-artifacts.mjs new file mode 100644 index 000000000..fab3c8501 --- /dev/null +++ b/apps/mobile-shell/scripts/check-build-artifacts.mjs @@ -0,0 +1,105 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import {spawnSync} from 'node:child_process'; + +const shellRoot = new URL('../', import.meta.url); +const repoRoot = path.resolve(shellRoot.pathname, '../..'); +const androidArtifactPath = path.join( + repoRoot, + 'build', + 'native', + 'mobile', + 'genarrative-mobile-android.apk', +); +const iosSimulatorArtifactPath = path.join( + repoRoot, + 'build', + 'native', + 'mobile', + 'genarrative-mobile-ios-simulator.tar.gz', +); + +function assertFile(filePath, label, minimumBytes) { + if (!fs.existsSync(filePath)) { + throw new Error(`${label} is missing: ${filePath}`); + } + + const stat = fs.statSync(filePath); + if (!stat.isFile() || stat.size < minimumBytes) { + throw new Error(`${label} must be a real build artifact`); + } +} + +function readHeader(filePath, length) { + const handle = fs.openSync(filePath, 'r'); + try { + const header = Buffer.alloc(length); + fs.readSync(handle, header, 0, header.length, 0); + return header; + } finally { + fs.closeSync(handle); + } +} + +function assertZipHeader(filePath, label) { + const header = readHeader(filePath, 4); + if ( + header[0] !== 0x50 || + header[1] !== 0x4b || + header[2] !== 0x03 || + header[3] !== 0x04 + ) { + throw new Error(`${label} must be a ZIP based artifact`); + } +} + +function assertGzipHeader(filePath, label) { + const header = readHeader(filePath, 2); + if (header[0] !== 0x1f || header[1] !== 0x8b) { + throw new Error(`${label} must be a gzip compressed tar artifact`); + } +} + +function run(command, args, label) { + const result = spawnSync(command, args, { + cwd: repoRoot, + encoding: 'utf8', + }); + if (result.error) { + throw new Error(`${label} failed to start: ${result.error.message}`); + } + if ((result.status ?? 0) !== 0) { + throw new Error(`${label} failed: ${(result.stderr || result.stdout).trim()}`); + } + return result.stdout; +} + +function assertIncludes(source, snippet, label) { + if (!source.includes(snippet)) { + throw new Error(`${label} missing ${snippet}`); + } +} + +assertFile(androidArtifactPath, 'Android mobile shell APK', 1024 * 1024); +assertZipHeader(androidArtifactPath, 'Android mobile shell APK'); +const androidListing = run('unzip', ['-l', androidArtifactPath], 'Android APK listing'); +assertIncludes(androidListing, 'AndroidManifest.xml', 'Android mobile shell APK'); +assertIncludes(androidListing, 'classes.dex', 'Android mobile shell APK'); +assertIncludes(androidListing, 'assets/index.android.bundle', 'Android mobile shell APK'); + +assertFile(iosSimulatorArtifactPath, 'iOS simulator mobile shell archive', 1024 * 1024); +assertGzipHeader(iosSimulatorArtifactPath, 'iOS simulator mobile shell archive'); +const iosListing = run( + 'tar', + ['-tzf', iosSimulatorArtifactPath], + 'iOS simulator archive listing', +); +assertIncludes(iosListing, '.app/Info.plist', 'iOS simulator mobile shell archive'); +assertIncludes(iosListing, '.app/Genarrative', 'iOS simulator mobile shell archive'); +assertIncludes( + iosListing, + '.app/main.jsbundle', + 'iOS simulator mobile shell archive', +); + +console.log('[mobile-shell:build-artifacts] OK'); diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 2a334f46a..d600bc5d3 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -14,6 +14,14 @@ const expoExportSmokePath = new URL( import.meta.url, ); const expoExportSmokeSource = fs.readFileSync(expoExportSmokePath, 'utf8'); +const buildArtifactsSmokePath = new URL( + '../scripts/check-build-artifacts.mjs', + import.meta.url, +); +const buildArtifactsSmokeSource = fs.readFileSync( + buildArtifactsSmokePath, + 'utf8', +); const nativeShellCheckPath = new URL( '../../../scripts/check-native-shells.mjs', import.meta.url, @@ -332,6 +340,7 @@ const requiredMobileShellSourceModules = [ 'shell/webViewPolicy.ts', ]; const requiredMobileShellScriptModules = [ + 'check-build-artifacts.mjs', 'check-config.mjs', 'check-eas-build-config.mjs', 'check-expo-config.mjs', @@ -807,6 +816,21 @@ for (const snippet of [ } } +for (const snippet of [ + 'genarrative-mobile-android.apk', + 'genarrative-mobile-ios-simulator.tar.gz', + 'AndroidManifest.xml', + 'assets/index.android.bundle', + '.app/Info.plist', + '.app/Genarrative', + '.app/main.jsbundle', + '[mobile-shell:build-artifacts] OK', +]) { + if (!buildArtifactsSmokeSource.includes(snippet)) { + throw new Error(`mobile shell build artifact smoke must verify ${snippet}`); + } +} + for (const snippet of [ "label: 'mobile-shell-eas-build-config-smoke'", "args: ['run', 'mobile-shell:build-config']", @@ -844,6 +868,7 @@ for (const [scriptName, expected] of Object.entries({ test: 'vitest run -c vitest.config.ts', 'build:android': `eas build --local --profile production --platform android --output ${androidBuildOutputPath}`, 'build:ios': `eas build --local --profile production-simulator --platform ios --output ${iosSimulatorBuildOutputPath}`, + 'build-artifacts:smoke': 'node scripts/check-build-artifacts.mjs', 'build-config:smoke': 'node scripts/check-eas-build-config.mjs', 'config:smoke': 'node scripts/check-expo-config.mjs', 'export:smoke': 'node scripts/check-expo-export.mjs', @@ -859,6 +884,7 @@ for (const [scriptName, expected] of Object.entries({ 'mobile-shell:build-config': 'npm --prefix apps/mobile-shell run build-config:smoke', 'mobile-shell:build:android': 'npm --prefix apps/mobile-shell run build:android', 'mobile-shell:build:ios': 'npm --prefix apps/mobile-shell run build:ios', + 'mobile-shell:build-artifacts': 'npm --prefix apps/mobile-shell run build-artifacts:smoke', 'mobile-shell:config': 'npm --prefix apps/mobile-shell run config:smoke', 'mobile-shell:export': 'npm --prefix apps/mobile-shell run export:smoke', })) { diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 89191e830..5cc85cc2a 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -16,6 +16,14 @@ --- +## 2026-06-21 移动原生包产物必须显式验收 + +- 背景:`npm run check:native-shells` 会跑 Expo production bundle 和 EAS profile smoke,但不在普通开发机上强制执行 Android APK 或 iOS simulator 包构建;真实构建完成后仍需要一个固定命令证明输出不是空文件、错误压缩包或非原生包。 +- 决策:移动壳新增 `npm run mobile-shell:build-artifacts`,读取 `build/native/mobile/genarrative-mobile-android.apk` 和 `build/native/mobile/genarrative-mobile-ios-simulator.tar.gz`。APK 必须是 ZIP 格式并包含 `AndroidManifest.xml`、`classes.dex`、`assets/index.android.bundle`;iOS simulator 包必须是 gzip tar 并包含 `.app/Info.plist`、`.app/Genarrative`、`.app/main.jsbundle`。该命令只在真实移动构建后运行,不替代 `check:native-shells` 的普通门禁。 +- 影响范围:`apps/mobile-shell/scripts/check-build-artifacts.mjs`、`apps/mobile-shell/package.json`、根 `package.json`、移动端分发构建流程。 +- 验证方式:无移动构建产物时运行 `npm run mobile-shell:build-artifacts` 应明确失败;真实构建后运行同一命令必须通过。普通改动继续运行 `npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。 +- 关联文档:`docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md`。 + ## 2026-06-21 原生壳生成物必须被 gitignore 覆盖 - 背景:移动壳和桌面壳验收会生成 Expo `.expo/`、Expo export smoke、Tauri `target/`、Tauri schema、自动生成权限目录和根目录 `build/native/` 分发产物;只检查这些路径未被 Git 追踪,不能防止后续误删 `.gitignore` 条目后把生成物暴露给开发者手动误加。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index 1e5a4f35d..03481493a 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -526,7 +526,7 @@ GameBridge 禁止: 2026-06-18 追加:移动壳 Metro export 烟测进入统一验收。`npm run check:native-shells` 会执行 `npm run mobile-shell:export`,用 Expo CLI 分别为 Android 和 iOS 生成 production bundle,校验 Metro metadata 版本、单平台 `fileMetadata`、Hermes `AppEntry-*.hbc` bundle 路径、assets 数组、生产 H5 URL 和原生宿主上下文 token 后清理临时 `.expo-export-smoke/` 目录。该烟测不生成 APK、AAB 或 IPA,也不写商店资料,只证明移动壳入口、Expo/RN 依赖和平台差异代码能被真实生产 bundler 打包。 -2026-06-20 追加:移动壳新增 EAS 原生包构建 profile。`apps/mobile-shell/eas.json` 固定 EAS CLI 下限、使用本地 `app.json` 版本字段,并提供 Android `production` 本地构建脚本产出可安装 APK、iOS `production-simulator` 本地构建脚本产出 release simulator 包;两者都设置 `EXPO_NO_DOTENV=1`,避免本机 `.env` 把开发 H5 URL 或私密配置带进可分发包。真实 `mobile-shell:build:android` 输出到根目录 `build/native/mobile/genarrative-mobile-android.apk`,真实 `mobile-shell:build:ios` 输出到 `build/native/mobile/genarrative-mobile-ios-simulator.tar.gz`,该目录沿用根 `build/` 的 gitignore,只作为本机或 CI 可收集产物目录。当前仍不写商店提交 profile、签名凭据来源、自动递增、OTA runtimeVersion 或 releaseChannel;`npm run check:native-shells` 只跑 `mobile-shell:build-config` 校验构建 profile、脚本、本地 `eas-cli` devDependency、固定输出路径和从 `apps/mobile-shell` 目录解析到的 EAS CLI 版本,真实 `mobile-shell:build:android` / `mobile-shell:build:ios` 作为具备 Android SDK、Xcode / simulator 环境和 EAS 本地构建依赖时的分发构建命令。 +2026-06-20 追加:移动壳新增 EAS 原生包构建 profile。`apps/mobile-shell/eas.json` 固定 EAS CLI 下限、使用本地 `app.json` 版本字段,并提供 Android `production` 本地构建脚本产出可安装 APK、iOS `production-simulator` 本地构建脚本产出 release simulator 包;两者都设置 `EXPO_NO_DOTENV=1`,避免本机 `.env` 把开发 H5 URL 或私密配置带进可分发包。真实 `mobile-shell:build:android` 输出到根目录 `build/native/mobile/genarrative-mobile-android.apk`,真实 `mobile-shell:build:ios` 输出到 `build/native/mobile/genarrative-mobile-ios-simulator.tar.gz`,该目录沿用根 `build/` 的 gitignore,只作为本机或 CI 可收集产物目录。当前仍不写商店提交 profile、签名凭据来源、自动递增、OTA runtimeVersion 或 releaseChannel;`npm run check:native-shells` 只跑 `mobile-shell:build-config` 校验构建 profile、脚本、本地 `eas-cli` devDependency、固定输出路径和从 `apps/mobile-shell` 目录解析到的 EAS CLI 版本,真实 `mobile-shell:build:android` / `mobile-shell:build:ios` 作为具备 Android SDK、Xcode / simulator 环境和 EAS 本地构建依赖时的分发构建命令。真实构建完成后必须运行 `npm run mobile-shell:build-artifacts`,校验 Android APK 是 ZIP 格式且包含 `AndroidManifest.xml`、`classes.dex` 和 `assets/index.android.bundle`,iOS simulator 包是 gzip tar 且包含 `.app/Info.plist`、`.app/Genarrative` 和 `.app/main.jsbundle`,避免把空文件、错误压缩包或非原生包当作移动端壳产物。 2026-06-18 追加:登录 / 支付能力伪声明进入门禁。`auth.requestLogin` 和 `payment.request` 保留在共享 HostBridge 契约中供未来真实接入,但 Expo 与 Tauri 壳在没有真实 SDK、渠道流程和后端契约前不得声明这些 capability,也不得把它们写入 `hostCapabilities`;请求到达壳层时必须返回明确 `unsupported_method`,让 H5 fallback 承接。两端壳测试会直接覆盖这两个 method,避免后续半接入时返回伪成功。 diff --git a/package.json b/package.json index 7fda88060..9bd1f1cfe 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "mobile-shell:build-config": "npm --prefix apps/mobile-shell run build-config:smoke", "mobile-shell:build:android": "npm --prefix apps/mobile-shell run build:android", "mobile-shell:build:ios": "npm --prefix apps/mobile-shell run build:ios", + "mobile-shell:build-artifacts": "npm --prefix apps/mobile-shell run build-artifacts:smoke", "mobile-shell:config": "npm --prefix apps/mobile-shell run config:smoke", "mobile-shell:export": "npm --prefix apps/mobile-shell run export:smoke", "desktop-shell:dev": "npm --prefix apps/desktop-shell run dev",