diff --git a/.gitignore b/.gitignore index dac847875..1e3aea8d2 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ /platform/android/build /platform/android/MapLibreAndroid/src/main/assets/sdk_versions/com.mapbox.mapboxsdk /platform/android/MapLibreAndroid/android-binaries-*.zip +/artifacts/maplibre-particle/ *.code-workspace local.properties diff --git a/docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v1.md b/docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v1.md new file mode 100644 index 000000000..c5555c9f8 --- /dev/null +++ b/docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v1.md @@ -0,0 +1,66 @@ +# STEP-080 — Android AAR 与 RN Test Host 标准构建流水线 v1 + +状态:完成 + +日期:2026-09-13 + +## 目标与基线 + +- 开始 HEAD / `origin/particle/main`:`d490578b973508d267c590dd604b033507c9b859`。 +- 固定交付版本:RN package / Native AAR 均为 `13.4.1-particle.3`,Maven coordinate 为 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.3`。 +- 任务开始前已有 Native `VERSION` 升级、JNI 头文件修正、STEP-062 文档 dirty、历史 untracked task/build/cache/tgz 和 Test Host generated Android/Expo/modules;均未清理、暂存或提交。 + +## 必做项 + +- [x] 新增 Linux producer `tools/particle_android_pipeline/linux_build_publish.sh`:报告 HEAD/branch/dirty;版本完全相等校验;SDK 解析;OpenGL Release 四 ABI 完整构建与 Maven Local publish;Maven artifact、旧 `.2` 保护、AAR/tgz bytes/SHA-256 校验;版本隔离 bundle/manifest。 +- [x] Linux 所有 noisy native build、Gradle publish、RN TypeScript typecheck 与 npm pack 均强制经 build-output-throttle;runner 不可达即 fail-closed。 +- [x] 默认 Linux 流程会生成可传输 AAR/POM/module/tgz 与 `artifacts/maplibre-particle//build-manifest.json`;目录已有 manifest 或不同 partial file 时 fail-closed,不覆盖历史 handoff。 +- [x] 明确提供 `--reuse-existing`:仅能消费已完整存在的 Maven Local 发布物并在 manifest 标识 `PASS_REUSED_EXISTING_MAVEN_LOCAL`;不会冒充重新 AAR 构建。`--preflight` 无写入。 +- [x] 新增 Windows consumer `tools/particle_android_pipeline/windows_build_testhost.ps1`:manifest 是唯一 artifact/version 输入,校验 SHA、安装 AAR/POM/module、同步 clean Test Host tracked source、固定 tgz 与 Maven coordinate、显式 prebuild/Debug APK build、输出 APK bytes/SHA-256。 +- [x] Windows 默认不做安装、启动或 ADB;只有 `-Install` 与 `-Install -Run` 才使用固定设备 `192.168.1.146`。支持 `-DryRun`,先打印版本、AAR、tgz、Host、Maven Local 与 Gradle command。 +- [x] 新增中文 README;版本升级必须先同步 `platform/android/VERSION` 与 RN `package.json`,pipeline 只校验,绝不暗改版本。 +- [x] `.gitignore` 忽略 `/artifacts/maplibre-particle/`,生成 AAR/tgz/manifest 不进入 scoped commit。 + +## 允许修改 + +- `tools/particle_android_pipeline/` 的 Linux/Windows 脚本及说明。 +- `.gitignore`、本 Step、完成报告与 `粒子开发记录.md`。 +- 运行时写入 Git ignore 的 `local.properties`、`.codex-build-logs/`、`artifacts/`。 + +## 禁止修改 + +- Core、Renderer、QBIN、Binding、JNI 业务语义。 +- Native/RN 版本值、既有 dirty/untracked、历史 Maven Local `.2` 发布物。 +- Windows APK/ADB/设备、Jest、iOS/Metal;本 Step 不重复完整 Linux AAR 编译。 + +## 构建与验证 + +以下 Linux 验证均在 `particle-codex` tmux session 内完成: + +- [x] `bash -n tools/particle_android_pipeline/linux_build_publish.sh`:PASS。 +- [x] `linux_build_publish.sh --preflight`:PASS;SDK 解析为 `/root/Android/Sdk`,无 build/package/artifact 写入。 +- [x] `linux_build_publish.sh --reuse-existing`:PASS;不运行完整 AAR build/publish,验证现存 `.3` Maven Local 的 AAR/POM/module,保护 `.2` 字节不变;经限流完成 RN package typecheck 与 npm pack。 +- [x] manifest JSON contract:AAR/POM/module/tgz 的存在、绝对路径、bytes、SHA-256、版本和 Maven coordinate 全部 PASS。 +- [x] tarball `package/package.json` version 与 Native `13.4.1-particle.3` 一致:PASS。 +- [x] `git diff --check -- .gitignore`:PASS。 +- [ ] Windows PowerShell parser/static validation:本 Linux 环境无 `pwsh`,明确 `POWERSHELL_PARSE_UNAVAILABLE_NO_PWSH`;未伪称 PASS。 + +## 当前结果 + +本轮 bundle 已在 Git ignore 路径生成: + +```text +artifacts/maplibre-particle/13.4.1-particle.3/build-manifest.json +``` + +manifest 记录构建 HEAD、分支、dirty 状态、版本、四 ABI、OpenGL、Maven coordinate、AAR/POM/module/tgz 的 path/bytes/SHA-256 和 Linux validation result。验证使用的 existing AAR SHA-256 为 `9ef03eb0d0c03457adc04847cfa69fb67d85191daecd64d3b76db2ad8cc688cd`,tgz SHA-256 为 `6c8af6ff19adb4e25785b54bea3edd1dc7d0502d9158cf1c519d84b69d08ee55`。 + +## 未完成与阻塞 + +- 未重复执行完整四 ABI AAR 构建与 Maven Local publish:任务明确禁止为验证重复完整 AAR 编译;默认脚本已固化该受限命令,验证仅复用现存完整 `.3` publication。 +- Linux 无 `pwsh`,因此未能本机解析 Windows PowerShell。Windows `-DryRun`、npm/prebuild/Gradle APK、安装、启动和设备验收均未执行,须在 Windows 11 使用该 manifest 完成;这不是 Linux 流水线实现阻塞。 +- Jest、Windows APK/ADB/设备、iOS/Metal 均未执行。 + +## 下一步 + +Windows 11 将完整 Linux bundle 复制到本机后,先运行 `windows_build_testhost.ps1 -ManifestPath -DryRun`,核对路径与 SHA;再无设备开关地运行同一脚本完成 Debug APK。只有用户明确选择时才附加 `-Install` 或 `-Run`。 diff --git a/docs/报告/STEP-080_Android_AAR与RNTestHost标准构建流水线_Codex完成报告_v1.md b/docs/报告/STEP-080_Android_AAR与RNTestHost标准构建流水线_Codex完成报告_v1.md new file mode 100644 index 000000000..6a720bc22 --- /dev/null +++ b/docs/报告/STEP-080_Android_AAR与RNTestHost标准构建流水线_Codex完成报告_v1.md @@ -0,0 +1,45 @@ +# STEP-080 Android AAR 与 RN Test Host 标准构建流水线 Codex 完成报告 v1 + +## 结果 + +已完成 Android AAR → RN package → build-manifest → Windows RN Test Host 的固定交付流水线。正式默认 Linux 命令是: + +```bash +tools/particle_android_pipeline/linux_build_publish.sh +``` + +它固定 OpenGL Release、四 ABI、`publishOpenglreleasePublicationToMavenLocal`、RN package TypeScript typecheck 与 npm tarball,并以完全版本相等为 hard gate。成功时输出 Git ignore 的版本隔离 bundle: + +```text +artifacts/maplibre-particle//build-manifest.json +``` + +manifest 是 Windows 唯一的 artifact/version 输入,包含 HEAD、branch、dirty 状态、RN/Native version、Maven coordinate、AAR/POM/module/tgz 的绝对路径/bytes/SHA-256、时间、四 ABI、OpenGL 和 Linux validation result。 + +Windows consumer 支持: + +```powershell +tools\particle_android_pipeline\windows_build_testhost.ps1 -ManifestPath -DryRun +tools\particle_android_pipeline\windows_build_testhost.ps1 -ManifestPath +``` + +它在写入前验证 manifest/bundle SHA,拒绝不同 SHA 的 Maven Local 覆盖,拒绝 dirty Test Host tracked source,固定 Test Host RN dependency 到 manifest tgz 和 native substitution 到 manifest coordinate。默认只构建 Debug APK 并输出其 path/bytes/SHA-256;默认不执行 ADB。`-Install` 与 `-Install -Run` 是唯一设备入口,固定使用 `192.168.1.146`。 + +## Linux 验证 + +所有 Linux 验证均在 `particle-codex` tmux 内完成。 + +- Bash syntax 与无写入 `--preflight`:PASS。 +- 使用现存完整 Maven Local `13.4.1-particle.3` 的 `--reuse-existing`:PASS。该模式没有重新编译 AAR,也没有执行 Gradle publish;它验证 AAR/POM/module,确认历史 `.2` AAR/POM/module 逐字节未变,经 build-output-throttle 完成 package TypeScript typecheck 与 npm pack。 +- 生成 manifest 的 JSON、AAR/POM/module/tgz 路径、bytes、SHA-256、版本与 Maven coordinate:PASS。 +- tgz 内 `package/package.json` version 与 Native version:PASS。 +- `git diff --check -- .gitignore`:PASS。 + +此次实际 handoff 的 AAR SHA-256 是 `9ef03eb0d0c03457adc04847cfa69fb67d85191daecd64d3b76db2ad8cc688cd`;tgz SHA-256 是 `6c8af6ff19adb4e25785b54bea3edd1dc7d0502d9158cf1c519d84b69d08ee55`。 + +## 未执行与限制 + +- 为遵守任务边界,未再次运行完整四 ABI AAR build/Maven publish;默认脚本已保留该完整流程,复验只使用明确标记的 existing Maven Local mode。 +- Linux 没有 `pwsh`,Windows PowerShell parser/static validation 记录为 unavailable,而非 PASS。 +- 未运行 Windows APK/ADB/安装/设备、Jest、iOS/Metal。 +- Native `VERSION`、既有 JNI 修正、STEP-062 dirty、历史 untracked/build/cache/tgz 均未纳入本 Step 的提交;仅新增流水线、忽略规则与本 Step 文档。 diff --git a/tools/particle_android_pipeline/README.md b/tools/particle_android_pipeline/README.md new file mode 100644 index 000000000..bf821fcfc --- /dev/null +++ b/tools/particle_android_pipeline/README.md @@ -0,0 +1,77 @@ +# Android AAR 与 RN Test Host 标准流水线 + +本目录固定 QBIN V4 Android Test Host 的交付链。Linux 是唯一的 AAR/RN package 生产端;Windows 11 只消费 Linux bundle 并构建 Test Host APK。正式实现不改变 Core、Renderer、QBIN、Binding 业务语义。 + +当前固定版本: + +- RN package:`13.4.1-particle.3` +- Native AAR:`13.4.1-particle.3` +- Maven coordinate:`cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.3` + +以后正常流程只有: + +```text +Linux: linux_build_publish.sh + ↓ build-manifest.json +Windows: windows_build_testhost.ps1 -DryRun + windows_build_testhost.ps1 +``` + +版本升级时,必须先显式同步更新 `platform/android/VERSION` 和 `packages/maplibre-particle-rn/package.json` 的 `version`。Linux 脚本只做完全相等的 fail-closed 校验,绝不猜测、覆盖或修复版本;Windows 也只接受 `particle.3` 或更高的显式 particle 版本。 + +## Linux:生产 bundle + +在仓库根目录运行: + +```bash +tools/particle_android_pipeline/linux_build_publish.sh +``` + +脚本先记录 HEAD、分支与 dirty 条目数(只报告,不清理),解析 Android SDK(`ANDROID_HOME`、`ANDROID_SDK_ROOT`,再到 `platform/android/local.properties`),并只为已存在且被 Git ignore 的 Gradle `local.properties` 写同一 SDK 路径。它固定运行 OpenGL Release 的四 ABI `make apackage`,随后运行: + +```text +:MapLibreAndroid:publishOpenglreleasePublicationToMavenLocal +``` + +所有 noisy build/typecheck/package 命令都经 build-output-throttle;找不到其 `run_limited.py` 即失败,可用 `BUILD_OUTPUT_THROTTLE=/绝对路径/run_limited.py` 显式指定。默认路径是本项目 Codex 环境的 skill 路径。 + +脚本验证 Maven Local 的 `.aar/.pom/.module`、POM 版本与 `13.4.1-particle.2` 历史目录字节不变;之后 typecheck RN package、生成 npm tgz。成功 bundle 写入(并由 Git ignore): + +```text +artifacts/maplibre-particle// + android-sdk-particle-opengl-.aar + android-sdk-particle-opengl-.pom + android-sdk-particle-opengl-.module + navsea-maplibre-particle-rn-.tgz + build-manifest.json +``` + +每个文件都带 bytes/SHA-256;manifest 另有 HEAD、branch、dirty 状态、版本、Maven coordinate、时间、四 ABI、renderer 和 Linux validation result。目录已有同版本 handoff 文件时立即失败,避免覆盖历史 AAR/tgz。 + +如果 Native `.3` 已由一次受控完整构建发布到 Maven Local,且仅需补建可传输的 handoff bundle,可显式使用: + +```bash +tools/particle_android_pipeline/linux_build_publish.sh --reuse-existing +``` + +这会在 manifest 明确标成 `PASS_REUSED_EXISTING_MAVEN_LOCAL`,不宣称重新构建;正常发布仍必须使用无参数命令。无写入的检查用 `--preflight`。 + +## Windows 11:消费 manifest 并构建 Test Host + +将整个 Linux `` bundle 原样传到 Windows 后,在 PowerShell 运行: + +```powershell +tools\particle_android_pipeline\windows_build_testhost.ps1 ` + -ManifestPath C:\handoff\13.4.1-particle.3\build-manifest.json -DryRun + +tools\particle_android_pipeline\windows_build_testhost.ps1 ` + -ManifestPath C:\handoff\13.4.1-particle.3\build-manifest.json +``` + +`-DryRun` 只校验 manifest 与 bundle 文件 SHA-256,并打印版本、AAR、tgz、Test Host 路径、Maven Local 和 Gradle 命令,不写 Maven Local、不改 package、不跑 npm/Gradle。 + +正常 Windows 流程以 manifest 为唯一 artifact/version 输入:校验 RN/Native/Maven coordinate、把 AAR/POM/module 安装到 Windows Maven Local(同路径已有不同 SHA 即失败)、在 clean Test Host tracked source 上同步 manifest `gitHead` 的源码/fixture、把 RN dependency 固定到 manifest tgz、把 native substitution(含已生成 Android `build.gradle` 的旧 marker)固定到 manifest coordinate、执行 `npm install --ignore-scripts`、明确执行 Expo Android prebuild,并构建 `:app:assembleDebug`。会输出 APK 绝对路径、bytes 与 SHA-256。 + +脚本不清理 checkout,也拒绝 dirty 的 Test Host tracked source;不会改写 Windows 用户自有 SDK 或 Gradle `local.properties`。默认绝不调用 ADB。只有显式 `-Install` 才会安装,只有 `-Install -Run` 才会启动,且两者固定 `adb -s 192.168.1.146`。 + +Linux 不运行 Windows/APK/ADB/设备步骤;本机没有 `pwsh` 时,只能记录 Windows 脚本未在 Linux 解析,不能把它写成 PASS。 diff --git a/tools/particle_android_pipeline/linux_build_publish.sh b/tools/particle_android_pipeline/linux_build_publish.sh new file mode 100755 index 000000000..38bb38fe3 --- /dev/null +++ b/tools/particle_android_pipeline/linux_build_publish.sh @@ -0,0 +1,283 @@ +#!/usr/bin/env bash +# Linux producer for the particle Android AAR and RN package handoff bundle. +# The generated artifacts are intentionally ignored by Git. +set -euo pipefail + +readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +readonly REPOSITORY_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)" +readonly ANDROID_ROOT="${REPOSITORY_ROOT}/platform/android" +readonly RN_PACKAGE_ROOT="${REPOSITORY_ROOT}/packages/maplibre-particle-rn" +readonly ARTIFACT_ROOT="${REPOSITORY_ROOT}/artifacts/maplibre-particle" +readonly THROTTLE_RUNNER="${BUILD_OUTPUT_THROTTLE:-/root/.codex/skills/codex-build-output-throttle/build-output-throttle/scripts/run_limited.py}" +readonly MAVEN_REPOSITORY="${MAVEN_LOCAL_REPOSITORY:-${HOME}/.m2/repository}" +readonly GROUP_PATH="cc/navsea/maplibre" +readonly ARTIFACT_ID="android-sdk-particle-opengl" +readonly EXPECTED_RENDERER="opengl" +readonly EXPECTED_ABIS="armeabi-v7a,arm64-v8a,x86,x86_64" + +reuse_existing=false +preflight_only=false + +usage() { + cat <<'USAGE' +Usage: tools/particle_android_pipeline/linux_build_publish.sh [--reuse-existing] [--preflight] + +Default: build four-ABI OpenGL Release AAR, publish it to Maven Local, typecheck the +RN package, pack the npm tarball, and write artifacts/maplibre-particle//build-manifest.json. + +--reuse-existing Do not compile or publish. Require a complete matching Maven Local + publication and use it to create a handoff bundle. This is for a + previously completed native build, never a substitute for a release build. +--preflight Perform only fail-closed version/SDK/tooling checks. No build, publish, + package, or artifact write occurs. +USAGE +} + +die() { + printf 'PIPELINE_ERROR: %s\n' "$*" >&2 + exit 1 +} + +note() { + printf 'PIPELINE: %s\n' "$*" +} + +require_command() { + command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" +} + +sha256_file() { + sha256sum "$1" | awk '{print $1}' +} + +file_bytes() { + stat -c '%s' "$1" +} + +while (($#)); do + case "$1" in + --reuse-existing) reuse_existing=true ;; + --preflight) preflight_only=true ;; + -h|--help) usage; exit 0 ;; + *) die "unknown argument: $1" ;; + esac + shift +done + +[[ "$preflight_only" == false || "$reuse_existing" == false ]] || die "--preflight and --reuse-existing cannot be combined" +[[ -d "$ANDROID_ROOT" ]] || die "Android project is missing: $ANDROID_ROOT" +[[ -d "$RN_PACKAGE_ROOT" ]] || die "RN package is missing: $RN_PACKAGE_ROOT" +[[ -x "$ANDROID_ROOT/gradlew" ]] || die "Gradle wrapper is missing or not executable" +[[ -f "$THROTTLE_RUNNER" ]] || die "build-output-throttle runner not found: $THROTTLE_RUNNER (set BUILD_OUTPUT_THROTTLE to its run_limited.py path)" + +require_command git +require_command node +require_command npm +require_command python3 +require_command sha256sum +require_command tar +require_command stat + +native_version="$(tr -d '\r\n' < "$ANDROID_ROOT/VERSION")" +rn_version="$(node -p "require(process.argv[1]).version" "$RN_PACKAGE_ROOT/package.json")" +[[ -n "$native_version" ]] || die "platform/android/VERSION is empty" +[[ "$native_version" == "$rn_version" ]] || die "version mismatch: native=$native_version rn=$rn_version; update both explicitly before running the pipeline" +[[ "$native_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+-particle\.[0-9]+$ ]] || die "unsupported particle version format: $native_version" + +git_head="$(git -C "$REPOSITORY_ROOT" rev-parse HEAD)" +git_branch="$(git -C "$REPOSITORY_ROOT" branch --show-current)" +[[ -n "$git_branch" ]] || die "detached HEAD is not a releasable pipeline input" +dirty_count="$(git -C "$REPOSITORY_ROOT" status --porcelain=v1 | wc -l | tr -d ' ')" +if [[ "$dirty_count" == 0 ]]; then + dirty_state="clean" +else + dirty_state="dirty" +fi +note "gitHead=$git_head branch=$git_branch dirty=$dirty_state entries=$dirty_count (reported only; no cleanup is performed)" +note "version=$native_version renderer=$EXPECTED_RENDERER abis=$EXPECTED_ABIS" + +resolve_android_sdk() { + local candidate="" + for candidate in "${ANDROID_HOME:-}" "${ANDROID_SDK_ROOT:-}"; do + if [[ -n "$candidate" && -d "$candidate" ]]; then + printf '%s\n' "$candidate" + return 0 + fi + done + + local properties_file="$ANDROID_ROOT/local.properties" + if [[ -f "$properties_file" ]]; then + candidate="$(sed -n 's@^[[:space:]]*sdk\.dir[[:space:]]*=[[:space:]]*@@p' "$properties_file" | tail -n 1)" + candidate="${candidate//\\:/:}" + candidate="${candidate//\\\\/\\}" + if [[ -n "$candidate" && -d "$candidate" ]]; then + printf '%s\n' "$candidate" + return 0 + fi + fi + die "Android SDK cannot be resolved: set ANDROID_HOME or ANDROID_SDK_ROOT, or configure $properties_file" +} + +android_sdk="$(resolve_android_sdk)" +note "androidSdk=$android_sdk" + +ensure_untracked_sdk_properties() { + local target="$1" + local parent + parent="$(dirname "$target")" + [[ -d "$parent" ]] || return 0 + if git -C "$REPOSITORY_ROOT" ls-files --error-unmatch "${target#"$REPOSITORY_ROOT/"}" >/dev/null 2>&1; then + die "refusing to write tracked machine-specific SDK configuration: $target" + fi + if [[ -f "$target" ]] && ! git -C "$REPOSITORY_ROOT" check-ignore -q "${target#"$REPOSITORY_ROOT/"}"; then + die "refusing to overwrite non-ignored SDK configuration: $target" + fi + mkdir -p "$parent" + printf 'sdk.dir=%s\n' "${android_sdk//\\/\\\\}" > "$target" + note "ensured ignored SDK configuration: $target" +} + +if [[ "$preflight_only" == false ]]; then + # Generated Test Host Android projects may be present on Linux. Do not create a host + # Android tree merely for this setting and never write a tracked local.properties. + ensure_untracked_sdk_properties "$ANDROID_ROOT/local.properties" + ensure_untracked_sdk_properties "$REPOSITORY_ROOT/test/android/local.properties" + ensure_untracked_sdk_properties "$REPOSITORY_ROOT/integration/react-native/MapLibreParticleRNTestHost/android/local.properties" +fi + +publication_dir="$MAVEN_REPOSITORY/$GROUP_PATH/$ARTIFACT_ID/$native_version" +readonly AAR_NAME="$ARTIFACT_ID-$native_version.aar" +readonly POM_NAME="$ARTIFACT_ID-$native_version.pom" +readonly MODULE_NAME="$ARTIFACT_ID-$native_version.module" +aar_source="$publication_dir/$AAR_NAME" +pom_source="$publication_dir/$POM_NAME" +module_source="$publication_dir/$MODULE_NAME" +legacy_publication_dir="$MAVEN_REPOSITORY/$GROUP_PATH/$ARTIFACT_ID/13.4.1-particle.2" + +if [[ "$preflight_only" == true ]]; then + note "PRECHECK_PASS version and SDK resolution succeeded; no files were written" + exit 0 +fi + +# .2 is a protected historical handoff. Snapshot its three contract files before native work. +for legacy_file in "$legacy_publication_dir/android-sdk-particle-opengl-13.4.1-particle.2.aar" "$legacy_publication_dir/android-sdk-particle-opengl-13.4.1-particle.2.pom" "$legacy_publication_dir/android-sdk-particle-opengl-13.4.1-particle.2.module"; do + [[ -f "$legacy_file" ]] || die "protected legacy Maven Local artifact is missing: $legacy_file" +done +legacy_before="$(mktemp)" +trap 'rm -f "$legacy_before"' EXIT +sha256sum "$legacy_publication_dir"/android-sdk-particle-opengl-13.4.1-particle.2.{aar,pom,module} > "$legacy_before" + +if [[ "$reuse_existing" == false ]]; then + note "building four ABI OpenGL Release AAR" + python3 "$THROTTLE_RUNNER" --log-dir "$REPOSITORY_ROOT/.codex-build-logs/particle_android_pipeline" -- \ + env ANDROID_HOME="$android_sdk" ANDROID_SDK_ROOT="$android_sdk" RENDERER=opengl BUILDTYPE=Release make -C "$ANDROID_ROOT" apackage + note "publishing OpenGL Release publication to Maven Local" + python3 "$THROTTLE_RUNNER" --log-dir "$REPOSITORY_ROOT/.codex-build-logs/particle_android_pipeline" -- \ + env ANDROID_HOME="$android_sdk" ANDROID_SDK_ROOT="$android_sdk" "$ANDROID_ROOT/gradlew" -p "$ANDROID_ROOT" -Pmaplibre.abis=all :MapLibreAndroid:publishOpenglreleasePublicationToMavenLocal --console=plain +else + note "reuse-existing selected: requiring existing Maven Local publication; native build/publish intentionally skipped" +fi + +for artifact_file in "$aar_source" "$pom_source" "$module_source"; do + [[ -f "$artifact_file" ]] || die "required Maven Local artifact is missing after publication: $artifact_file" +done +grep -Fq "$native_version" "$pom_source" || die "POM version does not match current version: $pom_source" +cmp --silent "$legacy_before" <(sha256sum "$legacy_publication_dir"/android-sdk-particle-opengl-13.4.1-particle.2.{aar,pom,module}) || die "protected 13.4.1-particle.2 Maven Local artifacts changed" +note "protected 13.4.1-particle.2 Maven Local artifacts remain byte-identical" + +artifact_dir="$ARTIFACT_ROOT/$native_version" +[[ ! -e "$artifact_dir/build-manifest.json" ]] || die "refusing to overwrite prior handoff bundle: $artifact_dir (bump the explicit version or move the old bundle aside)" +mkdir -p "$artifact_dir" +for existing_file in "$artifact_dir"/*; do + [[ -e "$existing_file" ]] || continue + case "$(basename "$existing_file")" in + "$AAR_NAME"|"$POM_NAME"|"$MODULE_NAME"|"navsea-maplibre-particle-rn-$native_version.tgz") ;; + *) die "unexpected file in incomplete handoff directory; refusing to mix artifacts: $existing_file" ;; + esac +done +copy_handoff_file() { + local source="$1" + local destination="$2" + if [[ -e "$destination" ]]; then + cmp --silent "$source" "$destination" || die "refusing to overwrite different partial handoff file: $destination" + note "resuming matching partial handoff file without overwrite: $destination" + return 0 + fi + cp --preserve=mode,timestamps "$source" "$destination" +} +copy_handoff_file "$aar_source" "$artifact_dir/$AAR_NAME" +copy_handoff_file "$pom_source" "$artifact_dir/$POM_NAME" +copy_handoff_file "$module_source" "$artifact_dir/$MODULE_NAME" + +note "typechecking RN package" +python3 "$THROTTLE_RUNNER" --log-dir "$REPOSITORY_ROOT/.codex-build-logs/particle_android_pipeline" -- \ + npx tsc --noEmit -p "$RN_PACKAGE_ROOT/tsconfig.json" + +expected_tgz="navsea-maplibre-particle-rn-$native_version.tgz" +[[ ! -e "$artifact_dir/$expected_tgz" ]] || die "refusing to overwrite prior npm tarball: $artifact_dir/$expected_tgz" +note "packing RN package" +python3 "$THROTTLE_RUNNER" --log-dir "$REPOSITORY_ROOT/.codex-build-logs/particle_android_pipeline" -- \ + bash -lc 'cd "$1" && npm pack --pack-destination "$2"' -- "$RN_PACKAGE_ROOT" "$artifact_dir" +[[ -f "$artifact_dir/$expected_tgz" ]] || die "npm pack did not create expected tarball: $artifact_dir/$expected_tgz" +tgz_version="$(tar -xOf "$artifact_dir/$expected_tgz" package/package.json | node -e 'let input=""; process.stdin.on("data", chunk => input += chunk); process.stdin.on("end", () => process.stdout.write(JSON.parse(input).version));')" +[[ "$tgz_version" == "$native_version" ]] || die "tarball version mismatch: tgz=$tgz_version native=$native_version" + +export MANIFEST_PATH="$artifact_dir/build-manifest.json" +export MANIFEST_GIT_HEAD="$git_head" +export MANIFEST_BRANCH="$git_branch" +export MANIFEST_DIRTY_STATE="$dirty_state" +export MANIFEST_DIRTY_COUNT="$dirty_count" +export MANIFEST_VERSION="$native_version" +export MANIFEST_COORDINATE="cc.navsea.maplibre:$ARTIFACT_ID:$native_version" +export MANIFEST_ABIS="$EXPECTED_ABIS" +export MANIFEST_RENDERER="$EXPECTED_RENDERER" +export MANIFEST_NATIVE_RESULT="$([[ "$reuse_existing" == true ]] && printf 'PASS_REUSED_EXISTING_MAVEN_LOCAL' || printf 'PASS_BUILT_AND_PUBLISHED')" +export MANIFEST_AAR_PATH="$(realpath "$artifact_dir/$AAR_NAME")" +export MANIFEST_POM_PATH="$(realpath "$artifact_dir/$POM_NAME")" +export MANIFEST_MODULE_PATH="$(realpath "$artifact_dir/$MODULE_NAME")" +export MANIFEST_TGZ_PATH="$(realpath "$artifact_dir/$expected_tgz")" +export MANIFEST_AAR_BYTES="$(file_bytes "$artifact_dir/$AAR_NAME")" +export MANIFEST_POM_BYTES="$(file_bytes "$artifact_dir/$POM_NAME")" +export MANIFEST_MODULE_BYTES="$(file_bytes "$artifact_dir/$MODULE_NAME")" +export MANIFEST_TGZ_BYTES="$(file_bytes "$artifact_dir/$expected_tgz")" +export MANIFEST_AAR_SHA256="$(sha256_file "$artifact_dir/$AAR_NAME")" +export MANIFEST_POM_SHA256="$(sha256_file "$artifact_dir/$POM_NAME")" +export MANIFEST_MODULE_SHA256="$(sha256_file "$artifact_dir/$MODULE_NAME")" +export MANIFEST_TGZ_SHA256="$(sha256_file "$artifact_dir/$expected_tgz")" +export MANIFEST_TIMESTAMP="$(date --utc +%Y-%m-%dT%H:%M:%SZ)" + +node <<'NODE' +const fs = require('fs'); +const path = require('path'); +const env = process.env; +const file = (kind) => ({ + path: env[`MANIFEST_${kind}_PATH`], + relativePath: path.basename(env[`MANIFEST_${kind}_PATH`]), + bytes: Number(env[`MANIFEST_${kind}_BYTES`]), + sha256: env[`MANIFEST_${kind}_SHA256`], +}); +const manifest = { + schemaVersion: 1, + gitHead: env.MANIFEST_GIT_HEAD, + branch: env.MANIFEST_BRANCH, + dirtyState: env.MANIFEST_DIRTY_STATE, + dirtyEntryCount: Number(env.MANIFEST_DIRTY_COUNT), + rnPackageVersion: env.MANIFEST_VERSION, + nativeVersion: env.MANIFEST_VERSION, + mavenCoordinate: env.MANIFEST_COORDINATE, + renderer: env.MANIFEST_RENDERER, + abis: env.MANIFEST_ABIS.split(','), + aar: file('AAR'), + pom: file('POM'), + module: file('MODULE'), + tgz: file('TGZ'), + buildTimestampUtc: env.MANIFEST_TIMESTAMP, + linuxValidationResult: env.MANIFEST_NATIVE_RESULT, +}; +fs.writeFileSync(env.MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}\n`, { encoding: 'utf8', flag: 'wx' }); +NODE + +note "AAR bytes=$(file_bytes "$artifact_dir/$AAR_NAME") sha256=$(sha256_file "$artifact_dir/$AAR_NAME") path=$(realpath "$artifact_dir/$AAR_NAME")" +note "TGZ bytes=$(file_bytes "$artifact_dir/$expected_tgz") sha256=$(sha256_file "$artifact_dir/$expected_tgz") path=$(realpath "$artifact_dir/$expected_tgz")" +note "manifest=$(realpath "$artifact_dir/build-manifest.json")" +note "PIPELINE_PASS linuxValidationResult=$MANIFEST_NATIVE_RESULT" diff --git a/tools/particle_android_pipeline/windows_build_testhost.ps1 b/tools/particle_android_pipeline/windows_build_testhost.ps1 new file mode 100644 index 000000000..067db347a --- /dev/null +++ b/tools/particle_android_pipeline/windows_build_testhost.ps1 @@ -0,0 +1,154 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$ManifestPath, + [string]$RepositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path, + [string]$MavenRepository = $(if ($env:USERPROFILE) { Join-Path $env:USERPROFILE '.m2\repository' } else { '' }), + [switch]$DryRun, + [switch]$Install, + [switch]$Run +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +function Fail([string]$Message) { throw "PIPELINE_ERROR: $Message" } + +function Get-Sha256([string]$Path) { return (Get-FileHash -Algorithm SHA256 -LiteralPath $Path).Hash.ToLowerInvariant() } + +function Assert-FileContract($Entry, [string]$ManifestDirectory, [string]$Name) { + if ($null -eq $Entry -or [string]::IsNullOrWhiteSpace($Entry.relativePath) -or [string]::IsNullOrWhiteSpace($Entry.sha256)) { + Fail "manifest $Name contract is incomplete" + } + $candidate = Join-Path $ManifestDirectory $Entry.relativePath + if (-not (Test-Path -LiteralPath $candidate -PathType Leaf)) { Fail "manifest $Name is missing beside manifest: $candidate" } + $item = Get-Item -LiteralPath $candidate + if ([int64]$item.Length -ne [int64]$Entry.bytes) { Fail "manifest $Name byte count mismatch: $candidate" } + if ((Get-Sha256 $candidate) -ne $Entry.sha256.ToLowerInvariant()) { Fail "manifest $Name SHA-256 mismatch: $candidate" } + return $candidate +} + +function Copy-MavenFile([string]$Source, [string]$Destination, [string]$ExpectedHash) { + if (Test-Path -LiteralPath $Destination -PathType Leaf) { + if ((Get-Sha256 $Destination) -ne $ExpectedHash.ToLowerInvariant()) { + Fail "refusing to overwrite different Maven Local file: $Destination" + } + Write-Host "PIPELINE: Maven Local already has verified file $Destination" + return + } + if ($DryRun) { + Write-Host "DRYRUN: Copy-Item $Source -> $Destination" + return + } + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $Destination) | Out-Null + Copy-Item -LiteralPath $Source -Destination $Destination + if ((Get-Sha256 $Destination) -ne $ExpectedHash.ToLowerInvariant()) { Fail "Maven Local copy SHA-256 mismatch: $Destination" } +} + +if ($Install -and $DryRun) { Fail '-Install cannot be combined with -DryRun' } +if ($Run -and -not $Install) { Fail '-Run requires -Install' } +if (-not (Test-Path -LiteralPath $ManifestPath -PathType Leaf)) { Fail "manifest not found: $ManifestPath" } +if (-not (Test-Path -LiteralPath $RepositoryRoot -PathType Container)) { Fail "repository root not found: $RepositoryRoot" } +if ([string]::IsNullOrWhiteSpace($MavenRepository)) { Fail 'MavenRepository cannot be resolved; pass -MavenRepository explicitly' } + +$manifestFullPath = (Resolve-Path -LiteralPath $ManifestPath).Path +$manifestDirectory = Split-Path -Parent $manifestFullPath +$manifest = Get-Content -Raw -LiteralPath $manifestFullPath | ConvertFrom-Json +if ($manifest.schemaVersion -ne 1) { Fail "unsupported manifest schemaVersion: $($manifest.schemaVersion)" } +if ([string]::IsNullOrWhiteSpace($manifest.gitHead) -or $manifest.gitHead -notmatch '^[0-9a-f]{40}$') { Fail 'manifest gitHead is invalid' } +if ($manifest.rnPackageVersion -ne $manifest.nativeVersion) { Fail "manifest version mismatch: RN=$($manifest.rnPackageVersion) Native=$($manifest.nativeVersion)" } +if ($manifest.nativeVersion -notmatch '^\d+\.\d+\.\d+-particle\.(?:[3-9]|[1-9]\d+)$') { Fail "manifest does not name particle.3 or a later explicit particle version: $($manifest.nativeVersion)" } +if ($manifest.mavenCoordinate -ne "cc.navsea.maplibre:android-sdk-particle-opengl:$($manifest.nativeVersion)") { Fail "unexpected Maven coordinate: $($manifest.mavenCoordinate)" } +if ($manifest.renderer -ne 'opengl') { Fail "unexpected renderer: $($manifest.renderer)" } +if (($manifest.abis -join ',') -ne 'armeabi-v7a,arm64-v8a,x86,x86_64') { Fail "unexpected ABI set: $($manifest.abis -join ',')" } + +$aar = Assert-FileContract $manifest.aar $manifestDirectory 'aar' +$pom = Assert-FileContract $manifest.pom $manifestDirectory 'pom' +$module = Assert-FileContract $manifest.module $manifestDirectory 'module' +$tgz = Assert-FileContract $manifest.tgz $manifestDirectory 'tgz' +$hostPath = Join-Path $RepositoryRoot 'integration\react-native\MapLibreParticleRNTestHost' +$pluginPath = Join-Path $hostPath 'plugins\withParticleNativeAndroid.js' +$packageJsonPath = Join-Path $hostPath 'package.json' +if (-not (Test-Path -LiteralPath $hostPath -PathType Container)) { Fail "Test Host source is missing: $hostPath" } +if (-not (Test-Path -LiteralPath $pluginPath -PathType Leaf)) { Fail "Test Host plugin is missing: $pluginPath" } +if (-not (Test-Path -LiteralPath $packageJsonPath -PathType Leaf)) { Fail "Test Host package.json is missing: $packageJsonPath" } + +$mavenVersionDir = Join-Path $MavenRepository "cc\navsea\maplibre\android-sdk-particle-opengl\$($manifest.nativeVersion)" +$mavenAar = Join-Path $mavenVersionDir ([IO.Path]::GetFileName($aar)) +$mavenPom = Join-Path $mavenVersionDir ([IO.Path]::GetFileName($pom)) +$mavenModule = Join-Path $mavenVersionDir ([IO.Path]::GetFileName($module)) +$gradleCommand = ".\gradlew.bat :app:assembleDebug --console=plain" + +Write-Host "PIPELINE: version=$($manifest.nativeVersion)" +Write-Host "PIPELINE: AAR=$aar" +Write-Host "PIPELINE: TGZ=$tgz" +Write-Host "PIPELINE: TestHost=$hostPath" +Write-Host "PIPELINE: MavenLocal=$mavenVersionDir" +Write-Host "PIPELINE: GradleCommand=$gradleCommand" + +if ($DryRun) { + Write-Host "DRYRUN: would verify/update Maven Local, Test Host package dependency, native substitution, npm install, Expo prebuild, and Debug APK build" + exit 0 +} + +Copy-MavenFile $aar $mavenAar $manifest.aar.sha256 +Copy-MavenFile $pom $mavenPom $manifest.pom.sha256 +Copy-MavenFile $module $mavenModule $manifest.module.sha256 + +# Do not reset or clean the checkout. A dirty tracked Test Host is an ambiguous source input. +$relativeHostPath = 'integration/react-native/MapLibreParticleRNTestHost' +$hostStatus = & git -C $RepositoryRoot status --porcelain -- $relativeHostPath +if ($LASTEXITCODE -ne 0) { Fail 'unable to inspect Test Host Git status' } +if ($hostStatus) { Fail 'Test Host tracked sources are dirty; synchronize or commit them before pipeline consumption' } +& git -C $RepositoryRoot fetch origin particle/main +if ($LASTEXITCODE -ne 0) { Fail 'git fetch origin particle/main failed' } +& git -C $RepositoryRoot restore --source $manifest.gitHead --worktree -- $relativeHostPath +if ($LASTEXITCODE -ne 0) { Fail "unable to synchronize Test Host tracked sources at manifest gitHead $($manifest.gitHead)" } + +$package = Get-Content -Raw -LiteralPath $packageJsonPath | ConvertFrom-Json +if ($null -eq $package.dependencies.'@navsea/maplibre-particle-rn') { Fail 'Test Host does not declare @navsea/maplibre-particle-rn dependency' } +$package.dependencies.'@navsea/maplibre-particle-rn' = "file:$tgz" +$package | ConvertTo-Json -Depth 20 | Set-Content -Encoding utf8NoBOM -LiteralPath $packageJsonPath + +$plugin = Get-Content -Raw -LiteralPath $pluginPath +$updatedPlugin = [regex]::Replace($plugin, 'cc\.navsea\.maplibre:android-sdk-particle-opengl:[^"`\s]+', $manifest.mavenCoordinate) +if ($updatedPlugin -eq $plugin) { Fail 'Test Host native substitution coordinate was not found in plugin source' } +Set-Content -Encoding utf8NoBOM -LiteralPath $pluginPath -Value $updatedPlugin + +Push-Location $hostPath +try { + & npm install --ignore-scripts --save-exact $tgz + if ($LASTEXITCODE -ne 0) { Fail 'npm install of manifest tarball failed' } + & npx expo prebuild --platform android + if ($LASTEXITCODE -ne 0) { Fail 'Expo Android prebuild failed' } + if (-not (Test-Path -LiteralPath 'android\gradlew.bat' -PathType Leaf)) { Fail 'Expo prebuild did not produce android\\gradlew.bat' } + # A non-clean prebuild preserves Windows local.properties. If Expo sees an existing + # marker it may retain an older generated substitution, so refresh only that coordinate. + $generatedBuildGradle = Join-Path $hostPath 'android\build.gradle' + if (-not (Test-Path -LiteralPath $generatedBuildGradle -PathType Leaf)) { Fail 'Expo prebuild did not produce android\\build.gradle' } + $generatedGradle = Get-Content -Raw -LiteralPath $generatedBuildGradle + $updatedGeneratedGradle = [regex]::Replace($generatedGradle, 'cc\.navsea\.maplibre:android-sdk-particle-opengl:[^"`\s]+', $manifest.mavenCoordinate) + if ($updatedGeneratedGradle -eq $generatedGradle) { Fail 'generated Android Maven substitution coordinate was not found' } + Set-Content -Encoding utf8NoBOM -LiteralPath $generatedBuildGradle -Value $updatedGeneratedGradle + Push-Location android + try { + & .\gradlew.bat :app:assembleDebug --console=plain + if ($LASTEXITCODE -ne 0) { Fail 'Debug Test Host APK build failed' } + } finally { Pop-Location } +} finally { Pop-Location } + +$apk = Join-Path $hostPath 'android\app\build\outputs\apk\debug\app-debug.apk' +if (-not (Test-Path -LiteralPath $apk -PathType Leaf)) { Fail "expected Debug APK is missing: $apk" } +$apkItem = Get-Item -LiteralPath $apk +Write-Host "PIPELINE: APK=$apk bytes=$($apkItem.Length) sha256=$(Get-Sha256 $apk)" + +if ($Install) { + & adb -s 192.168.1.146 install -r $apk + if ($LASTEXITCODE -ne 0) { Fail 'ADB install failed' } + if ($Run) { + & adb -s 192.168.1.146 shell am start -n org.maplibre.particle.rntesthost/.MainActivity + if ($LASTEXITCODE -ne 0) { Fail 'ADB activity start failed' } + } +} + +Write-Host 'PIPELINE_PASS: Windows Test Host Debug APK build completed without default device actions' diff --git a/粒子开发记录.md b/粒子开发记录.md index 4226ad836..acf4d3cc6 100644 --- a/粒子开发记录.md +++ b/粒子开发记录.md @@ -804,3 +804,9 @@ Android V3-08 单次综合验收已完成并关闭。Native `13.4.1-particle.2` - Test Host 已保留 V3 区并新增 V4 Wind/Wave/Current/Rain 四组冻结 QBIN T0/T1。8 个 raw asset 仅从冻结 WeatherV2 source 复制并随 Host 打包;`expo-asset` materialize 后只传本地 `file://` URI,运行时无 source-server、网络或 gzip 依赖。 - V4 dataset 真实表达 Wind/Current U `VELOCITY_COMPONENT` ColorBinding 与 U/V particle,Wave height/direction,Rain precipitation/Particle N/A;load、submit result、binding、midpoint seek(不宣称 mix)、PLAY/PAUSE、正 speed、clear 均有独立 UI。 - `particle-codex` 限流验证:Host/package TypeScript、TurboModule combine/codegen、8 文件 bytes/SHA-256、Linux runner target、Contract/Transport/V4/Binding/QBIN/Runtime/V3 filter 与 `git diff --check` 均 PASS。Android Gradle/APK/install/ADB/设备/Jest/iOS-Metal 未运行;状态 `IMPLEMENTED / WAITING FOR WINDOWS 11 ANDROID VALIDATION`。 + +### 2026-09-13 STEP-080 Android AAR 与 RN Test Host 标准构建流水线 + +- 固化 Linux `linux_build_publish.sh`:仅接受 RN/Native 完全一致版本,默认固定 OpenGL Release 四 ABI build、Maven Local publish、AAR/POM/module 校验、RN package typecheck/npm pack,以及 AAR/tgz bytes+SHA-256 的版本隔离 build-manifest;noisy Linux build/typecheck/package 一律经 build-output-throttle,缺 runner 立即 fail-closed。 +- 固化 Windows `windows_build_testhost.ps1`:manifest 是唯一 artifact/version 输入,支持先 `-DryRun`,再把 bundle 校验安装到 Windows Maven Local、固定 tgz/coordinate、显式 prebuild 与 Debug APK build。默认没有 ADB/设备;`-Install`/`-Run` 显式开关才可访问 `192.168.1.146`。 +- 在 `particle-codex` 使用已存在的 Native `.3` Maven Local publication 运行 `--reuse-existing`:AAR/POM/module、`.2` 历史保护、RN typecheck、npm tgz、manifest 路径/bytes/SHA/version 全部 PASS;未重复完整 AAR 编译、未执行 Gradle APK/ADB/设备/Jest。Linux 无 `pwsh`,Windows PowerShell parser 未本机执行,已明确记录而未伪称 PASS。