From fd5595ebbc6b3b12a66fea76e608cc51670debcd Mon Sep 17 00:00:00 2001 From: tei Date: Sat, 19 Sep 2026 07:50:31 +0900 Subject: [PATCH] build: add unified particle release pipeline --- packages/maplibre-particle-rn/CHANGELOG.md | 3 + tools/particle_release_pipeline/README.md | 62 ++++++ .../finalize_release.py | 178 +++++++++++++++++ .../prepare_release.py | 116 +++++++++++ .../publish_swiftpm.py | 88 +++++++++ tools/particle_release_pipeline/release.sh | 187 ++++++++++++++++++ 6 files changed, 634 insertions(+) create mode 100644 tools/particle_release_pipeline/README.md create mode 100755 tools/particle_release_pipeline/finalize_release.py create mode 100755 tools/particle_release_pipeline/prepare_release.py create mode 100755 tools/particle_release_pipeline/publish_swiftpm.py create mode 100755 tools/particle_release_pipeline/release.sh diff --git a/packages/maplibre-particle-rn/CHANGELOG.md b/packages/maplibre-particle-rn/CHANGELOG.md index 3843527aa..4754bde33 100644 --- a/packages/maplibre-particle-rn/CHANGELOG.md +++ b/packages/maplibre-particle-rn/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +## 13.4.1-particle.6(历史 iOS 过渡发行) + +- 已公开的 RN `.6` 与 iOS Metal `.6` 用于 iOS 接入验证;RN `.6` 仍显式依赖 Android Native `.5`,因此 `.6` 不是 Android/iOS 统一发行版,且不得覆盖重发。 - 新增 iOS `WeatherRuntimeBridge` RN 0.85 codegen TurboModule,实现既有 V3/V4 与视觉控制 ABI;每个命令从当前 Fabric/UIManager view registry 重新解析 MLRN MapView,不缓存 map/runtime 指针。 - 新增 iOS pod/autolinking/codegen provider。pod 复用 `MapLibreReactNative` 的唯一 MapLibre product,不嵌入第二份 framework。 - iOS Podfile 的 post-install 现在必须同时调用 `$MLRN.post_install(installer)` 和 `$MLRN_PARTICLE.post_install(installer)`;两者消费同一 `$MLRN_SPM_SPEC`,以让 MLRN、App 与 Particle pod target 共享一个 MapLibre package/product。 diff --git a/tools/particle_release_pipeline/README.md b/tools/particle_release_pipeline/README.md new file mode 100644 index 000000000..cbf3c4bec --- /dev/null +++ b/tools/particle_release_pipeline/README.md @@ -0,0 +1,62 @@ +# MapLibre Particle 统一发行流水线 + +本目录用于在 Mac release builder 上统一生成并发布 Android Native、React Native package 和 iOS Metal XCFramework。 + +## 唯一输入 + +只输入 Particle revision,例如: + + tools/particle_release_pipeline/release.sh 7 + +版本自动推导: + +- Android Native / RN:13.4.1-particle.7 +- iOS SwiftPM:6.28.0-particle.7 + +以后升级到 .8 时只输入 8,不再手工维护多个平台版本号。 + +## 固定构建环境 + +- macOS release builder +- JDK 17 +- Gradle Wrapper 9.5.1 +- Android SDK 36 +- Android NDK 28.2.13676358 +- CMake 3.31.6 +- Android renderer:OpenGL +- iOS renderer:Metal +- iOS binary build:bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic + +## 发布目标 + +- Android Maven:https://pbf.jpcn.ccwu.cc/maven +- RN tgz:https://pbf.jpcn.ccwu.cc/npm +- iOS binary:https://pbf.jpcn.ccwu.cc/swift/maplibre-particle +- SwiftPM Git:https://pbf.jpcn.ccwu.cc/swift/maplibre-particle-spm.git + +## 不可变规则 + +同一个版本号禁止覆盖。任一公网目标已经存在时,prepare/publish 阶段直接失败;必须增加 Particle revision。 + +流水线从同一个版本冻结 Git commit 构建 Android 与 iOS。发布后必须从公网重新下载 artifact,并与本地产物逐字节比较;只有 public readback 通过才生成最终 release manifest 和 tag。 + +## 阶段 + +可单独执行: + + release.sh 7 prepare + release.sh 7 build + release.sh 7 publish + release.sh 7 finalize + +完整执行: + + release.sh 7 + +prepare 会产生版本冻结 commit;finalize 会产生最终 manifest commit、tag,并推送 particle/main。发布工作必须在干净 release worktree 中运行。 + +## .6 历史说明 + +13.4.1-particle.6 是 iOS 接入过渡发行。公开 RN .6 仍依赖 Android Native .5,因此不得把 .6 重新定义或覆盖成统一双平台发行。 + +第一个由本流水线定义的 Android/iOS 统一发行是 Particle revision 7。 diff --git a/tools/particle_release_pipeline/finalize_release.py b/tools/particle_release_pipeline/finalize_release.py new file mode 100755 index 000000000..b6ccf5adb --- /dev/null +++ b/tools/particle_release_pipeline/finalize_release.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import hashlib +import json +import os +import re +import sys +from datetime import datetime, timezone +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +PUBLIC = "https://pbf.jpcn.ccwu.cc" + + +def sha256(path: Path) -> str: + h = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + h.update(chunk) + return h.hexdigest() + + +def base_version(value: str) -> str: + return re.sub(r"-particle\.\d+$", "", value.strip()) + + +def artifact(path: Path, url: str) -> dict: + return { + "fileName": path.name, + "sizeBytes": path.stat().st_size, + "sha256": sha256(path), + "url": url, + } + + +def main() -> None: + if len(sys.argv) != 4: + raise SystemExit( + "usage: finalize_release.py " + ) + + revision = int(sys.argv[1]) + source_commit = sys.argv[2] + artifact_dir = Path(sys.argv[3]) + + android_base = base_version((ROOT / "platform/android/VERSION").read_text()) + ios_base = base_version((ROOT / "platform/ios/VERSION").read_text()) + android_version = f"{android_base}-particle.{revision}" + ios_version = f"{ios_base}-particle.{revision}" + + aar_name = f"android-sdk-particle-opengl-{android_version}.aar" + tgz_name = f"navsea-maplibre-particle-rn-{android_version}.tgz" + ios_name = "MapLibre.dynamic.xcframework.zip" + + aar = artifact( + artifact_dir / aar_name, + f"{PUBLIC}/maven/cc/navsea/maplibre/android-sdk-particle-opengl/" + f"{android_version}/{aar_name}", + ) + tgz = artifact( + artifact_dir / tgz_name, + f"{PUBLIC}/npm/{tgz_name}", + ) + ios = artifact( + artifact_dir / ios_name, + f"{PUBLIC}/swift/maplibre-particle/{ios_version}/{ios_name}", + ) + + manifest = { + "schemaVersion": 2, + "releaseState": "PUBLISHED_PUBLIC_READBACK_PASS", + "particleRevision": revision, + "sourceCommit": source_commit, + "generatedAtUtc": datetime.now(timezone.utc).isoformat(), + "reactNative": { + "name": "@navsea/maplibre-particle-rn", + "version": android_version, + "artifact": tgz, + "mapLibreReactNative": "11.3.6", + }, + "android": { + "version": android_version, + "coordinate": ( + "cc.navsea.maplibre:android-sdk-particle-opengl:" + + android_version + ), + "renderer": "opengl", + "artifact": aar, + "ndk": os.environ.get("PARTICLE_NDK_VERSION"), + "gradle": os.environ.get("PARTICLE_GRADLE_VERSION"), + "jdk": os.environ.get("PARTICLE_JDK_VERSION"), + }, + "ios": { + "distributionVersion": ios_version, + "renderer": "metal", + "moduleName": "MapLibre", + "artifact": ios, + "swiftPackage": { + "url": f"{PUBLIC}/swift/maplibre-particle-spm.git", + "requirement": "exactVersion", + "version": ios_version, + "productName": "MapLibre", + }, + "xcode": os.environ.get("PARTICLE_XCODE_VERSION"), + "bazel": os.environ.get("PARTICLE_BAZEL_VERSION"), + }, + } + + (ROOT / "release/maplibre-particle/release-manifest.json").write_text( + json.dumps(manifest, ensure_ascii=False, indent=2) + "\n" + ) + + ios_manifest = { + "schemaVersion": 2, + "status": "PUBLISHED_PUBLIC_READBACK_PASS", + "distributionVersion": ios_version, + "moduleName": "MapLibre", + "artifact": ios, + "sourceCommit": source_commit, + "swiftPackage": manifest["ios"]["swiftPackage"], + "notes": ( + "Metal renderer release built with " + "bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic" + ), + } + (ROOT / "release/maplibre-particle/ios/release-manifest.json").write_text( + json.dumps(ios_manifest, ensure_ascii=False, indent=2) + "\n" + ) + + package_template = f"""// swift-tools-version: 5.7 +import PackageDescription + +let package = Package( + name: "MapLibreDistribution", + products: [ + .library(name: "MapLibre", targets: ["MapLibre"]), + ], + targets: [ + .binaryTarget( + name: "MapLibre", + url: "{ios['url']}", + checksum: "{ios['sha256']}" + ), + ] +) +""" + (ROOT / "release/maplibre-particle/ios/Package.swift.template").write_text( + package_template + ) + + release_readme = f"""# MapLibre Particle iOS 发行 + +当前正式发行:{ios_version} + +- Renderer:Metal +- Product / Module:MapLibre +- Binary:{ios['url']} +- SHA-256:{ios['sha256']} +- SwiftPM:{PUBLIC}/swift/maplibre-particle-spm.git +- Exact Version:{ios_version} +- Source Commit:{source_commit} + +构建命令固定为: + +bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic + +不得用默认 OpenGL XCFramework 覆盖同版本。新版本必须使用新的 Particle revision。 +""" + (ROOT / "release/maplibre-particle/ios/README.md").write_text( + release_readme + ) + + print(json.dumps(manifest, ensure_ascii=False, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/tools/particle_release_pipeline/prepare_release.py b/tools/particle_release_pipeline/prepare_release.py new file mode 100755 index 000000000..ed2acdef0 --- /dev/null +++ b/tools/particle_release_pipeline/prepare_release.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +ANDROID_VERSION_FILE = ROOT / "platform/android/VERSION" +IOS_VERSION_FILE = ROOT / "platform/ios/VERSION" +RN_ROOT = ROOT / "packages/maplibre-particle-rn" + + +def die(message: str) -> None: + raise SystemExit(f"PREPARE_RELEASE_ERROR: {message}") + + +def base_version(value: str) -> str: + return re.sub(r"-particle\.\d+$", "", value.strip()) + + +def replace_current_particle(path: Path, base: str, target: str) -> None: + text = path.read_text() + pattern = rf"{re.escape(base)}-particle\.\d+" + updated, count = re.subn(pattern, target, text) + if count == 0: + die(f"no particle version reference found in {path}") + path.write_text(updated) + +def update_changelog(target: str, revision: int) -> None: + path = RN_ROOT / "CHANGELOG.md" + text = path.read_text() + heading = f"## {target}(Android / iOS 统一发行)" + if heading in text: + return + marker = "## Unreleased\n\n" + if marker not in text: + die("CHANGELOG missing Unreleased heading") + entry = ( + marker + + heading + + "\n\n" + + "- Mac unified release pipeline 统一生成 Android Native、RN package 与 iOS SwiftPM/XCFramework;" + + f"共享 Particle revision {revision}。\n" + ) + path.write_text(text.replace(marker, entry, 1)) + + +def update_testhost_spm(ios_version: str) -> None: + path = ROOT / "integration/react-native/MapLibreParticleRNTestHost/ios/Podfile" + text = path.read_text() + text = re.sub( + r'url: "[^"]+"', + 'url: "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle-spm.git"', + text, + count=1, + ) + text = re.sub( + r'version: "[^"]+"', + f'version: "{ios_version}"', + text, + count=1, + ) + path.write_text(text) + +def main() -> None: + if len(sys.argv) != 2 or not sys.argv[1].isdigit(): + die("usage: prepare_release.py ") + revision = int(sys.argv[1]) + if revision < 1: + die("revision must be positive") + + android_base = base_version(ANDROID_VERSION_FILE.read_text()) + ios_base = base_version(IOS_VERSION_FILE.read_text()) + android_version = f"{android_base}-particle.{revision}" + ios_version = f"{ios_base}-particle.{revision}" + + ANDROID_VERSION_FILE.write_text(android_version + "\n") + + package_path = RN_ROOT / "package.json" + package = json.loads(package_path.read_text()) + package["version"] = android_version + package_path.write_text(json.dumps(package, ensure_ascii=False, indent=2) + "\n") + + for relative in ( + "packages/maplibre-particle-rn/android/build.gradle", + "packages/maplibre-particle-rn/app.plugin.js", + "integration/react-native/MapLibreParticleRNTestHost/plugins/withParticleNativeAndroid.js", + ): + replace_current_particle(ROOT / relative, android_base, android_version) + + readme = RN_ROOT / "README.md" + readme_text = readme.read_text() + readme_text = re.sub( + rf"{re.escape(android_base)}-particle\.\d+", + android_version, + readme_text, + ) + readme_text = re.sub( + r"必须指向同一 `\.\d+`", + f"必须指向同一 `.{revision}`", + readme_text, + ) + readme.write_text(readme_text) + + update_changelog(android_version, revision) + update_testhost_spm(ios_version) + + print(f"PARTICLE_REVISION={revision}") + print(f"ANDROID_RN_VERSION={android_version}") + print(f"IOS_DISTRIBUTION_VERSION={ios_version}") + + +if __name__ == "__main__": + main() diff --git a/tools/particle_release_pipeline/publish_swiftpm.py b/tools/particle_release_pipeline/publish_swiftpm.py new file mode 100755 index 000000000..711cf22e8 --- /dev/null +++ b/tools/particle_release_pipeline/publish_swiftpm.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +PUBLIC_ROOT = "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle" +GIT_REMOTE = "nas:/volume1/work/pbf/swift/maplibre-particle-spm.git" +WORK = Path.home() / "dev/MapLibreParticleDistribution" + + +def run(*args: str, cwd: Path | None = None) -> str: + result = subprocess.run( + args, + cwd=cwd, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + return result.stdout + + +def main() -> None: + if len(sys.argv) != 3: + raise SystemExit("usage: publish_swiftpm.py ") + version, checksum = sys.argv[1], sys.argv[2] + if not WORK.exists(): + run("git", "clone", GIT_REMOTE, str(WORK)) + + run("git", "fetch", "origin", cwd=WORK) + run("git", "checkout", "main", cwd=WORK) + run("git", "pull", "--ff-only", "origin", "main", cwd=WORK) + + tag_check = subprocess.run( + ["git", "ls-remote", "--tags", "origin", f"refs/tags/{version}"], + cwd=WORK, + text=True, + stdout=subprocess.PIPE, + check=True, + ).stdout.strip() + if tag_check: + raise SystemExit(f"SwiftPM tag already exists: {version}") + + url = f"{PUBLIC_ROOT}/{version}/MapLibre.dynamic.xcframework.zip" + package = f'''// swift-tools-version: 5.7 +import PackageDescription + +let package = Package( + name: "MapLibreDistribution", + products: [ + .library(name: "MapLibre", targets: ["MapLibre"]), + ], + targets: [ + .binaryTarget( + name: "MapLibre", + url: "{url}", + checksum: "{checksum}" + ), + ] +) +''' + + (WORK / "Package.swift").write_text(package) + (WORK / "README.md").write_text( + "# MapLibre Particle SwiftPM Distribution\n\n" + "该仓库只保存 NavSea MapLibre Particle iOS binary distribution 的 SwiftPM manifest。\n" + "产品和模块名固定为 MapLibre,供 MapLibre React Native 11.3.6 消费。\n\n" + f"当前版本:{version}。\n" + f"对应 binary:{url}\n" + ) + run("git", "add", "Package.swift", "README.md", cwd=WORK) + run("git", "commit", "-m", f"release: MapLibre Particle iOS {version}", cwd=WORK) + run("git", "tag", "-a", version, "-m", f"MapLibre Particle iOS {version}", cwd=WORK) + run("git", "push", "origin", "main", cwd=WORK) + run("git", "push", "origin", version, cwd=WORK) + run( + "ssh", "nas", + "git --git-dir=/volume1/work/pbf/swift/maplibre-particle-spm.git " + "symbolic-ref HEAD refs/heads/main && " + "git --git-dir=/volume1/work/pbf/swift/maplibre-particle-spm.git update-server-info", + ) + print(f"SWIFTPM_PUBLISHED={version}") + + +if __name__ == "__main__": + main() diff --git a/tools/particle_release_pipeline/release.sh b/tools/particle_release_pipeline/release.sh new file mode 100755 index 000000000..414bb5fdd --- /dev/null +++ b/tools/particle_release_pipeline/release.sh @@ -0,0 +1,187 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd -- "$SCRIPT_DIR/../.." && pwd)" +REV="${1:-}" +PHASE="${2:-all}" + +die() { echo "RELEASE_ERROR: $*" >&2; exit 1; } +note() { echo "RELEASE: $*"; } +require() { command -v "$1" >/dev/null 2>&1 || die "missing command: $1"; } + +[[ "$REV" =~ ^[0-9]+$ ]] || die "usage: release.sh [prepare|build|publish|finalize|all]" +[[ "$PHASE" =~ ^(prepare|build|publish|finalize|all)$ ]] || die "invalid phase: $PHASE" +[[ "$(uname -s)" == "Darwin" ]] || die "unified release builder must run on macOS" + +export JAVA_HOME="${JAVA_HOME:-/usr/local/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home}" +export ANDROID_HOME="${ANDROID_HOME:-$HOME/Library/Android/sdk}" +export ANDROID_SDK_ROOT="$ANDROID_HOME" +export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/28.2.13676358" +export PATH="$JAVA_HOME/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmake/3.31.6/bin:$PATH" +if [[ -d /usr/local/opt/gnu-sed/libexec/gnubin ]]; then + export PATH="/usr/local/opt/gnu-sed/libexec/gnubin:$PATH" +fi + +for cmd in git python3 node npm java curl ssh scp shasum bazel xcodebuild ruby tar cmp awk sed; do require "$cmd"; done +[[ -d "$ANDROID_NDK_HOME" ]] || die "NDK missing: $ANDROID_NDK_HOME" +[[ -x "$ROOT/platform/android/gradlew" ]] || die "Gradle wrapper missing" + +ANDROID_BASE="$(sed -E 's/-particle\.[0-9]+$//' "$ROOT/platform/android/VERSION")" +IOS_BASE="$(sed -E 's/-particle\.[0-9]+$//' "$ROOT/platform/ios/VERSION")" +ANDROID_VERSION="$ANDROID_BASE-particle.$REV" +IOS_VERSION="$IOS_BASE-particle.$REV" +RN_ROOT="$ROOT/packages/maplibre-particle-rn" +ARTIFACT_DIR="$ROOT/artifacts/maplibre-particle/$ANDROID_VERSION" +M2_DIR="$HOME/.m2/repository/cc/navsea/maplibre/android-sdk-particle-opengl/$ANDROID_VERSION" +AAR_NAME="android-sdk-particle-opengl-$ANDROID_VERSION.aar" +TGZ_NAME="navsea-maplibre-particle-rn-$ANDROID_VERSION.tgz" +IOS_ZIP_NAME="MapLibre.dynamic.xcframework.zip" +ANDROID_URL="https://pbf.jpcn.ccwu.cc/maven/cc/navsea/maplibre/android-sdk-particle-opengl/$ANDROID_VERSION/$AAR_NAME" +RN_URL="https://pbf.jpcn.ccwu.cc/npm/$TGZ_NAME" +IOS_URL="https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/$IOS_VERSION/$IOS_ZIP_NAME" +SPM_URL="https://pbf.jpcn.ccwu.cc/swift/maplibre-particle-spm.git" + +public_status() { curl -L -s -o /dev/null -w '%{http_code}' "$1"; } +require_absent() { + local code; code="$(public_status "$1")" + [[ "$code" == "404" ]] || die "target already published (HTTP $code): $1" +} +require_clean() { + [[ -z "$(git -C "$ROOT" status --porcelain=v1)" ]] || die "release worktree must be clean" +} +require_target_versions() { + [[ "$(tr -d '\r\n' < "$ROOT/platform/android/VERSION")" == "$ANDROID_VERSION" ]] || die "Android VERSION mismatch" + [[ "$(node -p "require(process.argv[1]).version" "$RN_ROOT/package.json")" == "$ANDROID_VERSION" ]] || die "RN version mismatch" + grep -Fq "android-sdk-particle-opengl:$ANDROID_VERSION" "$RN_ROOT/android/build.gradle" || die "RN Android coordinate mismatch" + grep -Fq "android-sdk-particle-opengl:$ANDROID_VERSION" "$RN_ROOT/app.plugin.js" || die "Expo plugin coordinate mismatch" +} + +prepare_release() { + require_clean + require_absent "$ANDROID_URL" + require_absent "$RN_URL" + require_absent "$IOS_URL" + [[ -z "$(git ls-remote --tags "$SPM_URL" "refs/tags/$IOS_VERSION")" ]] || die "SwiftPM tag already exists: $IOS_VERSION" + python3 "$SCRIPT_DIR/prepare_release.py" "$REV" + git -C "$ROOT" diff --check + require_target_versions + git -C "$ROOT" add \ + platform/android/VERSION \ + packages/maplibre-particle-rn/package.json \ + packages/maplibre-particle-rn/android/build.gradle \ + packages/maplibre-particle-rn/app.plugin.js \ + packages/maplibre-particle-rn/README.md \ + packages/maplibre-particle-rn/CHANGELOG.md \ + integration/react-native/MapLibreParticleRNTestHost/plugins/withParticleNativeAndroid.js \ + integration/react-native/MapLibreParticleRNTestHost/ios/Podfile + if ! git -C "$ROOT" diff --cached --quiet; then + git -C "$ROOT" commit -m "release: prepare unified particle.$REV" + fi + require_clean + note "prepared sourceCommit=$(git -C "$ROOT" rev-parse HEAD)" +} + +build_release() { + require_clean + require_target_versions + git -C "$ROOT" submodule update --init --recursive + rm -rf "$ARTIFACT_DIR" + mkdir -p "$ARTIFACT_DIR" + printf 'sdk.dir=%s\n' "$ANDROID_HOME" > "$ROOT/platform/android/local.properties" + note "building Android OpenGL four-ABI release" + (cd "$ROOT/platform/android" && RENDERER=opengl BUILDTYPE=Release make apackage) + note "publishing Android artifact to Maven Local" + "$ROOT/platform/android/gradlew" -p "$ROOT/platform/android" -Pmaplibre.abis=all \ + :MapLibreAndroid:publishOpenglreleasePublicationToMavenLocal --console=plain + for f in "$AAR_NAME" "android-sdk-particle-opengl-$ANDROID_VERSION.pom" "android-sdk-particle-opengl-$ANDROID_VERSION.module"; do + [[ -f "$M2_DIR/$f" ]] || die "missing Maven Local artifact: $M2_DIR/$f" + cp "$M2_DIR/$f" "$ARTIFACT_DIR/" + done + + note "building iOS Metal XCFramework" + (cd "$ROOT" && bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic) + IOS_SOURCE="$ROOT/bazel-out/darwin_x86_64-fastbuild/bin/platform/ios/$IOS_ZIP_NAME" + [[ -f "$IOS_SOURCE" ]] || die "iOS XCFramework zip missing" + cp "$IOS_SOURCE" "$ARTIFACT_DIR/$IOS_ZIP_NAME" + + note "packing RN package" + (cd "$RN_ROOT" && npm pack --pack-destination "$ARTIFACT_DIR") + [[ -f "$ARTIFACT_DIR/$TGZ_NAME" ]] || die "RN tgz missing" + tar -xOf "$ARTIFACT_DIR/$TGZ_NAME" package/package.json | node -e 'let s="";process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let j=JSON.parse(s);if(j.version!==process.argv[1])process.exit(2)})' "$ANDROID_VERSION" + tar -xOf "$ARTIFACT_DIR/$TGZ_NAME" package/android/build.gradle | grep -Fq "android-sdk-particle-opengl:$ANDROID_VERSION" || die "packed RN points to wrong Android version" + ruby -c "$RN_ROOT/MapLibreParticleRN.podspec" >/dev/null + shasum -a 256 "$ARTIFACT_DIR/$AAR_NAME" "$ARTIFACT_DIR/$TGZ_NAME" "$ARTIFACT_DIR/$IOS_ZIP_NAME" + note "build complete sourceCommit=$(git -C "$ROOT" rev-parse HEAD)" +} + +verify_public_file() { + local local_file="$1" url="$2" tmp + tmp="$(mktemp)" + curl -fL "$url?nocache=$(date +%s)" -o "$tmp" + cmp -s "$local_file" "$tmp" || { rm -f "$tmp"; die "public readback mismatch: $url"; } + rm -f "$tmp" +} + +publish_release() { + require_clean + require_target_versions + [[ -f "$ARTIFACT_DIR/$AAR_NAME" && -f "$ARTIFACT_DIR/$TGZ_NAME" && -f "$ARTIFACT_DIR/$IOS_ZIP_NAME" ]] || die "build artifacts missing" + require_absent "$ANDROID_URL" + require_absent "$RN_URL" + require_absent "$IOS_URL" + ssh nas "test ! -e /volume1/work/pbf/maven/cc/navsea/maplibre/android-sdk-particle-opengl/$ANDROID_VERSION" || die "remote Android version exists" + ssh nas "test ! -e /volume1/work/pbf/npm/$TGZ_NAME" || die "remote RN tgz exists" + ssh nas "test ! -e /volume1/work/pbf/swift/maplibre-particle/$IOS_VERSION" || die "remote iOS version exists" + ssh nas "mkdir -p /volume1/work/pbf/maven/cc/navsea/maplibre/android-sdk-particle-opengl/$ANDROID_VERSION /volume1/work/pbf/swift/maplibre-particle/$IOS_VERSION" + scp "$M2_DIR"/* "nas:/volume1/work/pbf/maven/cc/navsea/maplibre/android-sdk-particle-opengl/$ANDROID_VERSION/" + scp "$ARTIFACT_DIR/$TGZ_NAME" "nas:/volume1/work/pbf/npm/$TGZ_NAME" + scp "$ARTIFACT_DIR/$IOS_ZIP_NAME" "nas:/volume1/work/pbf/swift/maplibre-particle/$IOS_VERSION/$IOS_ZIP_NAME" + IOS_SHA="$(shasum -a 256 "$ARTIFACT_DIR/$IOS_ZIP_NAME" | awk '{print $1}')" + printf '%s %s\n' "$IOS_SHA" "$IOS_ZIP_NAME" > "$ARTIFACT_DIR/$IOS_ZIP_NAME.sha256" + scp "$ARTIFACT_DIR/$IOS_ZIP_NAME.sha256" "nas:/volume1/work/pbf/swift/maplibre-particle/$IOS_VERSION/$IOS_ZIP_NAME.sha256" + verify_public_file "$ARTIFACT_DIR/$AAR_NAME" "$ANDROID_URL" + verify_public_file "$ARTIFACT_DIR/$TGZ_NAME" "$RN_URL" + verify_public_file "$ARTIFACT_DIR/$IOS_ZIP_NAME" "$IOS_URL" + python3 "$SCRIPT_DIR/publish_swiftpm.py" "$IOS_VERSION" "$IOS_SHA" + git ls-remote "$SPM_URL" "refs/tags/$IOS_VERSION" | grep -q . || die "public SwiftPM tag missing" + + export PARTICLE_NDK_VERSION="28.2.13676358" + export PARTICLE_GRADLE_VERSION="$("$ROOT/platform/android/gradlew" -p "$ROOT/platform/android" --version | awk '/^Gradle /{print $2; exit}')" + export PARTICLE_JDK_VERSION="$(java -version 2>&1 | head -1)" + export PARTICLE_XCODE_VERSION="$(xcodebuild -version | tr '\n' ' ')" + export PARTICLE_BAZEL_VERSION="$(bazel --version)" + python3 "$SCRIPT_DIR/finalize_release.py" "$REV" "$(git -C "$ROOT" rev-parse HEAD)" "$ARTIFACT_DIR" + git -C "$ROOT" diff --check + note "publish and public readback passed" +} + +finalize_release() { + git -C "$ROOT" add \ + release/maplibre-particle/release-manifest.json \ + release/maplibre-particle/ios/release-manifest.json \ + release/maplibre-particle/ios/Package.swift.template \ + release/maplibre-particle/ios/README.md + if ! git -C "$ROOT" diff --cached --quiet; then git -C "$ROOT" commit -m "release: finalize unified particle.$REV"; fi + TAG="maplibre-particle/rn/$ANDROID_VERSION" + [[ -z "$(git -C "$ROOT" tag -l "$TAG")" ]] || die "local tag exists: $TAG" + git -C "$ROOT" fetch origin particle/main + git -C "$ROOT" merge-base --is-ancestor origin/particle/main HEAD || die "origin/particle/main is not an ancestor of release HEAD" + git -C "$ROOT" tag -a "$TAG" -m "Unified Android/iOS Particle release $ANDROID_VERSION" + git -C "$ROOT" push origin HEAD:particle/main + git -C "$ROOT" push origin "$TAG" + note "RELEASE_PASS version=$ANDROID_VERSION ios=$IOS_VERSION tag=$TAG" +} + +case "$PHASE" in + prepare) prepare_release ;; + build) build_release ;; + publish) publish_release ;; + finalize) finalize_release ;; + all) + prepare_release + build_release + publish_release + finalize_release + ;; +esac