chore: add Android RN build pipeline
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
This commit is contained in:
77
tools/particle_android_pipeline/README.md
Normal file
77
tools/particle_android_pipeline/README.md
Normal file
@@ -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/<version>/
|
||||
android-sdk-particle-opengl-<version>.aar
|
||||
android-sdk-particle-opengl-<version>.pom
|
||||
android-sdk-particle-opengl-<version>.module
|
||||
navsea-maplibre-particle-rn-<version>.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 `<version>` 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。
|
||||
283
tools/particle_android_pipeline/linux_build_publish.sh
Executable file
283
tools/particle_android_pipeline/linux_build_publish.sh
Executable file
@@ -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/<version>/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 "<version>$native_version</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"
|
||||
154
tools/particle_android_pipeline/windows_build_testhost.ps1
Normal file
154
tools/particle_android_pipeline/windows_build_testhost.ps1
Normal file
@@ -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'
|
||||
Reference in New Issue
Block a user