243 lines
12 KiB
Bash
Executable File
243 lines
12 KiB
Bash
Executable File
#!/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 <particle-revision> [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 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 \
|
|
packages/maplibre-particle-rn/MapLibreParticleRN.podspec \
|
|
integration/react-native/MapLibreParticleRNTestHost/package.json \
|
|
integration/react-native/MapLibreParticleRNTestHost/plugins/withParticleNativeAndroid.js
|
|
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 (Bazel opt)"
|
|
(cd "$ROOT" && bazel build -c opt --//:renderer=metal //platform/ios:MapLibre.dynamic)
|
|
IOS_SOURCE="$ROOT/bazel-out/darwin_x86_64-opt/bin/platform/ios/$IOS_ZIP_NAME"
|
|
[[ "$IOS_SOURCE" == *"-opt/"* ]] || die "iOS release artifact is not from Bazel opt output: $IOS_SOURCE"
|
|
[[ -f "$IOS_SOURCE" ]] || die "iOS XCFramework zip missing: $IOS_SOURCE"
|
|
local ios_listing
|
|
ios_listing="$(unzip -Z1 "$IOS_SOURCE")"
|
|
grep -Fq "/ios-arm64/" <<<"$ios_listing" || die "iOS XCFramework missing ios-arm64 device slice"
|
|
if grep -Eiq "simulator|ios-arm64_x86_64" <<<"$ios_listing"; then
|
|
die "iOS XCFramework unexpectedly contains simulator slice"
|
|
fi
|
|
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"
|
|
|
|
local source_commit
|
|
local android_root android_final android_tmp
|
|
local rn_final rn_tmp
|
|
local ios_root ios_final ios_tmp
|
|
local aar_sha rn_sha ios_sha
|
|
local remote_aar_sha remote_rn_sha remote_ios_sha
|
|
local pom_name module_name
|
|
|
|
source_commit="${PARTICLE_RELEASE_SOURCE_COMMIT:-$(git -C "$ROOT" rev-parse HEAD)}"
|
|
[[ "$source_commit" =~ ^[0-9a-f]{40}$ ]] || die "invalid frozen release source SHA"
|
|
git -C "$ROOT" cat-file -e "$source_commit^{commit}" || die "release source commit unavailable"
|
|
git -C "$ROOT" merge-base --is-ancestor "$source_commit" HEAD || die "release source commit is not an ancestor of current HEAD"
|
|
android_root="/volume1/work/pbf/maven/cc/navsea/maplibre/android-sdk-particle-opengl"
|
|
android_final="$android_root/$ANDROID_VERSION"
|
|
android_tmp="$android_root/.$ANDROID_VERSION.uploading"
|
|
rn_final="/volume1/work/pbf/npm/$TGZ_NAME"
|
|
rn_tmp="/volume1/work/pbf/npm/.$TGZ_NAME.uploading"
|
|
ios_root="/volume1/work/pbf/swift/maplibre-particle"
|
|
ios_final="$ios_root/$IOS_VERSION"
|
|
ios_tmp="$ios_root/.$IOS_VERSION.uploading"
|
|
pom_name="android-sdk-particle-opengl-$ANDROID_VERSION.pom"
|
|
module_name="android-sdk-particle-opengl-$ANDROID_VERSION.module"
|
|
|
|
ssh nas "test ! -e '$android_final'" || die "remote Android version exists"
|
|
ssh nas "test ! -e '$rn_final'" || die "remote RN tgz exists"
|
|
ssh nas "test ! -e '$ios_final'" || die "remote iOS version exists"
|
|
|
|
aar_sha="$(shasum -a 256 "$ARTIFACT_DIR/$AAR_NAME" | awk '{print $1}')"
|
|
rn_sha="$(shasum -a 256 "$ARTIFACT_DIR/$TGZ_NAME" | awk '{print $1}')"
|
|
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"
|
|
|
|
note "uploading release artifacts through SSH staging paths"
|
|
ssh nas "rm -rf '$android_tmp' '$ios_tmp' '$rn_tmp'; mkdir -p '$android_tmp' '$ios_tmp'"
|
|
tar -C "$M2_DIR" -cf - . | ssh nas "tar -C '$android_tmp' -xf -"
|
|
ssh nas "cat > '$rn_tmp'" < "$ARTIFACT_DIR/$TGZ_NAME"
|
|
ssh nas "cat > '$ios_tmp/$IOS_ZIP_NAME'" < "$ARTIFACT_DIR/$IOS_ZIP_NAME"
|
|
ssh nas "cat > '$ios_tmp/$IOS_ZIP_NAME.sha256'" < "$ARTIFACT_DIR/$IOS_ZIP_NAME.sha256"
|
|
|
|
remote_aar_sha="$(ssh nas "sha256sum '$android_tmp/$AAR_NAME' | awk '{print \$1}'")"
|
|
remote_rn_sha="$(ssh nas "sha256sum '$rn_tmp' | awk '{print \$1}'")"
|
|
remote_ios_sha="$(ssh nas "sha256sum '$ios_tmp/$IOS_ZIP_NAME' | awk '{print \$1}'")"
|
|
[[ "$remote_aar_sha" == "$aar_sha" ]] || die "NAS Android checksum mismatch"
|
|
[[ "$remote_rn_sha" == "$rn_sha" ]] || die "NAS RN checksum mismatch"
|
|
[[ "$remote_ios_sha" == "$ios_sha" ]] || die "NAS iOS checksum mismatch"
|
|
|
|
note "atomically promoting NAS staging paths"
|
|
ssh nas "test ! -e '$android_final' && test ! -e '$rn_final' && test ! -e '$ios_final' && mv '$android_tmp' '$android_final' && mv '$rn_tmp' '$rn_final' && mv '$ios_tmp' '$ios_final'"
|
|
|
|
verify_public_file "$ARTIFACT_DIR/$AAR_NAME" "$ANDROID_URL"
|
|
verify_public_file "$M2_DIR/$pom_name" "https://pbf.jpcn.ccwu.cc/maven/cc/navsea/maplibre/android-sdk-particle-opengl/$ANDROID_VERSION/$pom_name"
|
|
verify_public_file "$M2_DIR/$module_name" "https://pbf.jpcn.ccwu.cc/maven/cc/navsea/maplibre/android-sdk-particle-opengl/$ANDROID_VERSION/$module_name"
|
|
verify_public_file "$ARTIFACT_DIR/$TGZ_NAME" "$RN_URL"
|
|
verify_public_file "$ARTIFACT_DIR/$IOS_ZIP_NAME" "$IOS_URL"
|
|
verify_public_file "$ARTIFACT_DIR/$IOS_ZIP_NAME.sha256" "$IOS_URL.sha256"
|
|
|
|
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" "$source_commit" "$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"
|
|
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"
|
|
if [[ -n "$(git -C "$ROOT" tag -l "$TAG")" ]]; then
|
|
[[ "$(git -C "$ROOT" rev-list -n 1 "$TAG")" == "$(git -C "$ROOT" rev-parse HEAD)" ]] || die "local tag points to a different commit: $TAG"
|
|
else
|
|
git -C "$ROOT" tag -a "$TAG" -m "Unified Android/iOS Particle release $ANDROID_VERSION"
|
|
fi
|
|
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
|