补齐移动原生包产物验收
新增移动壳 APK 与 iOS simulator 包产物检查脚本 新增根级 mobile-shell:build-artifacts 验收入口 移动壳配置门禁反查产物验收脚本与真实包内容检查 同步宿主壳方案文档和项目决策记录
This commit is contained in:
@@ -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,避免后续半接入时返回伪成功。
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user