补齐移动原生包产物验收

新增移动壳 APK 与 iOS simulator 包产物检查脚本

新增根级 mobile-shell:build-artifacts 验收入口

移动壳配置门禁反查产物验收脚本与真实包内容检查

同步宿主壳方案文档和项目决策记录
This commit is contained in:
2026-06-21 21:13:33 +08:00
parent facbd29f06
commit 768546fdc7
6 changed files with 142 additions and 1 deletions
+1
View File
@@ -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",
@@ -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');
@@ -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',
})) {
@@ -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` 条目后把生成物暴露给开发者手动误加。
@@ -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,避免后续半接入时返回伪成功。
+1
View File
@@ -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",