Add open Shorebird CI and release tooling

This commit is contained in:
Tony
2026-06-25 20:08:23 +08:00
parent d8e7cfb237
commit b5d0b7b233
45 changed files with 8451 additions and 101 deletions
+249
View File
@@ -0,0 +1,249 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
APP_DIR="${APP_DIR:-$ROOT/testapps/license_flavor_patch_test}"
FLUTTER_BIN="${FLUTTER_BIN:-$ROOT/flutter/bin/flutter}"
ADB_BIN="${ADB_BIN:-adb}"
PACKAGE="${ANDROID_PACKAGE:-com.example.licenseflavorpatchtest.license_flavor_patch_test}"
ACTIVITY="${ANDROID_ACTIVITY:-com.example.licenseflavorpatchtest.license_flavor_patch_test.MainActivity}"
RELEASE_VERSION="${ANDROID_RELEASE_VERSION:-1.0+1}"
LOCAL_ENGINE_SRC_PATH="${LOCAL_ENGINE_SRC_PATH:-$ROOT/flutter/engine/src}"
LOCAL_ENGINE="${LOCAL_ENGINE:-android_release_arm64}"
LOCAL_ENGINE_HOST="${LOCAL_ENGINE_HOST:-host_release_arm64}"
WORK_DIR="${ANDROID_RUNTIME_SMOKE_WORK_DIR:-$(mktemp -d "${TMPDIR:-/tmp}/open-shorebird-android-runtime.XXXXXX")}"
SEED_REMOTE_DIR="${ANDROID_SEED_REMOTE_DIR:-/data/local/tmp/open-shorebird-android-runtime-seed}"
TARGET_PLATFORM="${ANDROID_TARGET_PLATFORM:-android-arm64}"
if [[ "${KEEP_ANDROID_RUNTIME_SMOKE_ARTIFACTS:-0}" != "1" ]]; then
trap 'rm -rf "$WORK_DIR"' EXIT
fi
ADB=("$ADB_BIN")
if [[ -n "${ANDROID_SERIAL:-}" ]]; then
ADB+=("-s" "$ANDROID_SERIAL")
fi
adb_cmd() {
"${ADB[@]}" "$@"
}
adb_shell() {
adb_cmd shell "$@"
}
python_bin() {
if command -v python3 >/dev/null 2>&1; then
printf '%s\n' python3
else
printf '%s\n' python
fi
}
require_tool() {
local tool="$1"
if ! command -v "$tool" >/dev/null 2>&1; then
echo "$tool is required" >&2
exit 127
fi
}
build_apk() {
local license="$1"
local output="$2"
if [[ "${SKIP_ANDROID_BUILDS:-0}" == "1" ]]; then
local env_name
case "$license" in
free) env_name=ANDROID_FREE_APK ;;
pro) env_name=ANDROID_PRO_APK ;;
*)
echo "Unsupported license variant: $license" >&2
exit 64
;;
esac
local existing="${!env_name:-}"
if [[ -z "$existing" || ! -f "$existing" ]]; then
echo "SKIP_ANDROID_BUILDS=1 requires $env_name to point at an APK" >&2
exit 66
fi
cp "$existing" "$output"
return
fi
(
cd "$APP_DIR"
"$FLUTTER_BIN" build apk --release \
--target-platform "$TARGET_PLATFORM" \
--local-engine-src-path="$LOCAL_ENGINE_SRC_PATH" \
--local-engine="$LOCAL_ENGINE" \
--local-engine-host="$LOCAL_ENGINE_HOST" \
--dart-define="LICENSE_TYPE=$license"
)
cp "$APP_DIR/build/app/outputs/flutter-apk/app-release.apk" "$output"
}
extract_libapp() {
local apk="$1"
local output="$2"
local py
py="$(python_bin)"
"$py" - "$apk" "$output" <<'PY'
import pathlib
import sys
import zipfile
apk_path = pathlib.Path(sys.argv[1])
output_path = pathlib.Path(sys.argv[2])
with zipfile.ZipFile(apk_path) as archive:
candidates = [
name for name in archive.namelist()
if name.endswith("/libapp.so") and "arm64-v8a/" in name
]
if not candidates:
raise SystemExit(f"missing arm64 libapp.so in {apk_path}")
output_path.write_bytes(archive.read(candidates[0]))
PY
}
wait_for_text() {
local expected="$1"
local dump="$WORK_DIR/window.xml"
for _ in $(seq 1 "${ANDROID_UI_WAIT_ATTEMPTS:-80}"); do
adb_shell uiautomator dump /sdcard/open_shorebird_window.xml >/dev/null 2>&1 || true
adb_cmd exec-out cat /sdcard/open_shorebird_window.xml >"$dump" 2>/dev/null || true
if grep -q "$expected" "$dump"; then
return 0
fi
sleep 0.25
done
echo "Timed out waiting for Android UI text: $expected" >&2
echo "Last uiautomator dump:" >&2
sed -n '1,120p' "$dump" >&2 || true
return 70
}
start_app() {
adb_shell am force-stop "$PACKAGE" >/dev/null 2>&1 || true
adb_shell am start -W -n "$PACKAGE/$ACTIVITY" >/dev/null
}
can_seed_with_run_as() {
adb_shell run-as "$PACKAGE" sh -c 'test -d files' >/dev/null 2>&1
}
can_seed_with_root() {
if adb_shell sh -c 'test "$(id -u)" = "0"' >/dev/null 2>&1; then
return 0
fi
adb_cmd root >/dev/null 2>&1 || true
adb_cmd wait-for-device >/dev/null 2>&1 || true
adb_shell sh -c 'test "$(id -u)" = "0"' >/dev/null 2>&1
}
seed_with_run_as() {
adb_shell run-as "$PACKAGE" sh -c \
"mkdir -p files && rm -rf files/shorebird_updater && cp -R '$SEED_REMOTE_DIR/shorebird_updater' files/"
}
seed_with_root() {
local target="/data/data/$PACKAGE/files"
local owner
owner="$(adb_shell sh -c "stat -c '%u:%g' '$target'" | tr -d '\r')"
adb_shell sh -c \
"rm -rf '$target/shorebird_updater' && cp -R '$SEED_REMOTE_DIR/shorebird_updater' '$target/' && chown -R '$owner' '$target/shorebird_updater'"
}
prepare_seed() {
local libapp="$1"
local seed_root="$WORK_DIR/seed/shorebird_updater"
local size
size="$(wc -c <"$libapp" | tr -d ' ')"
rm -rf "$WORK_DIR/seed"
mkdir -p "$seed_root/patches/1"
cp "$libapp" "$seed_root/patches/1/dlc.vmcode"
cat >"$seed_root/state.json" <<EOF
{
"client_id": "android-runtime-smoke",
"release_version": "$RELEASE_VERSION",
"queued_events": []
}
EOF
cat >"$seed_root/pointers.json" <<'EOF'
{
"next_boot_patch": 1,
"last_booted_patch": null,
"currently_booting_patch": null,
"boot_started_at": null
}
EOF
cat >"$seed_root/patches/1/state.json" <<EOF
{
"kind": "Installed",
"signature": null,
"size": $size
}
EOF
adb_shell rm -rf "$SEED_REMOTE_DIR" >/dev/null 2>&1 || true
adb_shell mkdir -p "$SEED_REMOTE_DIR" >/dev/null
adb_cmd push "$seed_root" "$SEED_REMOTE_DIR/" >/dev/null
echo "seeded_patch_size=$size"
}
require_tool "$ADB_BIN"
require_tool "$FLUTTER_BIN"
require_tool java
adb_cmd start-server >/dev/null
device_count="$(adb_cmd devices | awk 'NR > 1 && $2 == "device" { count++ } END { print count + 0 }')"
if [[ "$device_count" -lt 1 ]]; then
echo "No Android device or emulator is connected." >&2
echo "Start an emulator/device, then rerun this script. Use ANDROID_SERIAL to pick a device." >&2
exit 69
fi
if [[ "$device_count" -gt 1 && -z "${ANDROID_SERIAL:-}" ]]; then
echo "Multiple Android devices are connected; set ANDROID_SERIAL." >&2
adb_cmd devices -l >&2
exit 64
fi
free_apk="$WORK_DIR/free.apk"
pro_apk="$WORK_DIR/pro.apk"
pro_libapp="$WORK_DIR/pro-libapp.so"
build_apk free "$free_apk"
build_apk pro "$pro_apk"
extract_libapp "$pro_apk" "$pro_libapp"
adb_cmd uninstall "$PACKAGE" >/dev/null 2>&1 || true
adb_cmd install -r "$free_apk" >/dev/null
start_app
wait_for_text 'license:free'
wait_for_text 'pro-feature:off'
echo "android_base_status=license:free/pro-feature:off"
prepare_seed "$pro_libapp"
if can_seed_with_run_as; then
seed_with_run_as
echo "android_seed_mode=run-as"
elif can_seed_with_root; then
seed_with_root
echo "android_seed_mode=root"
else
cat >&2 <<EOF
Unable to seed the Android app-private updater directory.
Use a debuggable build/device where 'adb shell run-as $PACKAGE' works, or use
a rooted emulator/device where 'adb root' works. The smoke seed target is:
/data/data/$PACKAGE/files/shorebird_updater
EOF
exit 77
fi
start_app
wait_for_text 'license:pro'
wait_for_text 'pro-feature:enabled'
echo "android_patch_status=license:pro/pro-feature:enabled"
adb_shell am force-stop "$PACKAGE" >/dev/null 2>&1 || true
echo "android_runtime_patch_smoke=passed"
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat >&2 <<'EOF'
usage: assemble_artifact_mirror.sh <downloaded-workflow-artifacts-dir> <output-mirror-root>
Copies every publish-ready shorebird/ mirror subtree from downloaded GitHub
Actions artifacts into <output-mirror-root>. Engine archives are extracted and
scanned for nested mirror/shorebird trees. Existing files may be reused only
when their bytes match.
EOF
}
if [[ "$#" -ne 2 ]]; then
usage
exit 64
fi
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
INPUT_DIR="$1"
OUTPUT_DIR="$2"
if [[ ! -d "$INPUT_DIR" ]]; then
echo "input artifact directory does not exist: $INPUT_DIR" >&2
exit 66
fi
PYTHON_BIN=python3
if ! command -v "$PYTHON_BIN" >/dev/null 2>&1; then
PYTHON_BIN=python
fi
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/open-shorebird-mirror.XXXXXX")"
trap 'rm -rf "$TMP_DIR"' EXIT
mkdir -p "$OUTPUT_DIR"
FOUND_TREES=0
copy_shorebird_tree() {
local tree="$1"
local source_file rel_file target_file
FOUND_TREES=$((FOUND_TREES + 1))
while IFS= read -r -d '' source_file; do
rel_file="${source_file#"$tree"/}"
target_file="$OUTPUT_DIR/shorebird/$rel_file"
mkdir -p "$(dirname "$target_file")"
if [[ -e "$target_file" ]]; then
if ! cmp -s "$source_file" "$target_file"; then
echo "conflicting mirror file: shorebird/$rel_file" >&2
echo " existing: $target_file" >&2
echo " incoming: $source_file" >&2
exit 70
fi
continue
fi
cp -p "$source_file" "$target_file"
done < <(find "$tree" -type f -print0)
}
scan_for_shorebird_trees() {
local search_root="$1"
local tree
while IFS= read -r -d '' tree; do
copy_shorebird_tree "$tree"
done < <(find "$search_root" -type d -name shorebird -print0)
}
scan_for_shorebird_trees "$INPUT_DIR"
archive_index=0
while IFS= read -r -d '' archive_path; do
archive_index=$((archive_index + 1))
extract_dir="$TMP_DIR/archive-$archive_index"
mkdir -p "$extract_dir"
"$PYTHON_BIN" "$ROOT/scripts/safe_extract_tar.py" "$archive_path" "$extract_dir"
scan_for_shorebird_trees "$extract_dir"
done < <(find "$INPUT_DIR" -type f \( -name '*.tar.gz' -o -name '*.tgz' \) -print0)
if [[ "$FOUND_TREES" -eq 0 ]]; then
echo "no shorebird/ mirror subtrees found under $INPUT_DIR" >&2
exit 65
fi
while IFS= read -r -d '' mirror_file; do
if [[ "$mirror_file" == *.sha256 ]]; then
continue
fi
sidecar="$mirror_file.sha256"
if [[ ! -f "$sidecar" ]]; then
"$ROOT/scripts/write_sha256.sh" "$mirror_file" "$sidecar"
fi
done < <(find "$OUTPUT_DIR/shorebird" -type f -print0)
"$PYTHON_BIN" "$ROOT/scripts/validate_artifact_mirror.py" "$OUTPUT_DIR"
echo "assembled artifact mirror at $OUTPUT_DIR"
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
set -euo pipefail
MIN_FREE_DISK_GB="${CI_MIN_FREE_DISK_GB:-0}"
CHECK_PATH="${CI_CAPACITY_PATH:-$PWD}"
AVAILABLE_DISK_KB_OVERRIDE="${CI_AVAILABLE_DISK_KB_OVERRIDE:-}"
fail() {
echo "error: $*" >&2
exit 70
}
case "$MIN_FREE_DISK_GB" in
''|*[!0-9]*)
fail "CI_MIN_FREE_DISK_GB must be a non-negative integer, got '$MIN_FREE_DISK_GB'"
;;
esac
if [[ "$MIN_FREE_DISK_GB" == "0" ]]; then
echo "CI capacity check skipped because CI_MIN_FREE_DISK_GB=0"
exit 0
fi
if [[ ! -e "$CHECK_PATH" ]]; then
fail "capacity check path does not exist: $CHECK_PATH"
fi
available_disk_kb() {
if [[ -n "$AVAILABLE_DISK_KB_OVERRIDE" ]]; then
case "$AVAILABLE_DISK_KB_OVERRIDE" in
*[!0-9]*)
fail "CI_AVAILABLE_DISK_KB_OVERRIDE must be an integer, got '$AVAILABLE_DISK_KB_OVERRIDE'"
;;
esac
printf '%s\n' "$AVAILABLE_DISK_KB_OVERRIDE"
return
fi
df -Pk "$CHECK_PATH" | awk 'NR == 2 { print $4 }'
}
available_kb="$(available_disk_kb)"
case "$available_kb" in
''|*[!0-9]*)
fail "could not determine available disk space for $CHECK_PATH"
;;
esac
required_kb=$((MIN_FREE_DISK_GB * 1024 * 1024))
available_gb=$((available_kb / 1024 / 1024))
echo "CI capacity: ${available_gb} GiB free at $CHECK_PATH; required: ${MIN_FREE_DISK_GB} GiB"
if (( available_kb < required_kb )); then
fail "runner has insufficient free disk for the heavy SDK/engine build. Use a larger or self-hosted runner label, or free disk before this step."
fi
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ "${GITHUB_ACTIONS:-}" != "true" ]]; then
echo "Refusing to free disk outside GitHub Actions." >&2
exit 64
fi
if [[ "$(uname -s)" != "Linux" ]]; then
echo "Skipping Linux disk cleanup on $(uname -s)."
exit 0
fi
if [[ "${CI_FREE_DISK_SPACE:-1}" == "0" ]]; then
echo "Skipping disk cleanup because CI_FREE_DISK_SPACE=0."
df -h
exit 0
fi
if [[ "${RUNNER_ENVIRONMENT:-github-hosted}" != "github-hosted" &&
"${CI_FREE_DISK_SPACE_FORCE:-0}" != "1" ]]; then
echo "Skipping disk cleanup on ${RUNNER_ENVIRONMENT} runner."
echo "Set CI_FREE_DISK_SPACE_FORCE=1 to opt in on non-hosted runners."
df -h
exit 0
fi
echo "Disk before cleanup:"
df -h
# GitHub-hosted Ubuntu images include large toolchains that are unrelated to
# Dart SDK, Flutter engine, and updater builds. Remove only well-known cache
# directories on ephemeral GitHub Actions runners.
for path in \
/opt/ghc \
/opt/hostedtoolcache/CodeQL \
/usr/local/.ghcup \
/usr/local/lib/android/sdk \
/usr/local/share/boost \
/usr/share/dotnet; do
if [[ -e "$path" ]]; then
echo "Removing $path"
sudo rm -rf "$path"
fi
done
echo "Disk after cleanup:"
df -h
+238
View File
@@ -0,0 +1,238 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SOURCE_APP_DIR="${SOURCE_APP_DIR:-$ROOT/testapps/license_flavor_patch_test}"
FLUTTER_BIN="${FLUTTER_BIN:-$ROOT/flutter/bin/flutter}"
APP_ID="${SHOREBIRD_APP_ID:-license-flavor-patch-test}"
LOCAL_ENGINE_SRC_PATH="${LOCAL_ENGINE_SRC_PATH:-$ROOT/flutter/engine/src}"
DEFAULT_LOCAL_ENGINE="linux_release_x64"
if [[ ! -d "$LOCAL_ENGINE_SRC_PATH/out/$DEFAULT_LOCAL_ENGINE" &&
-d "$LOCAL_ENGINE_SRC_PATH/out/host_release" ]]; then
DEFAULT_LOCAL_ENGINE="host_release"
fi
LOCAL_ENGINE="${LOCAL_ENGINE:-$DEFAULT_LOCAL_ENGINE}"
LOCAL_ENGINE_HOST="${LOCAL_ENGINE_HOST:-$LOCAL_ENGINE}"
WORK_DIR="${LINUX_RUNTIME_SMOKE_WORK_DIR:-$(mktemp -d "${TMPDIR:-/tmp}/open-shorebird-linux-runtime.XXXXXX")}"
APP_COPY="$WORK_DIR/app"
HOME_DIR="$WORK_DIR/home"
if [[ "${KEEP_LINUX_RUNTIME_SMOKE_ARTIFACTS:-0}" != "1" ]]; then
trap 'rm -rf "$WORK_DIR"' EXIT
fi
require_tool() {
local tool="$1"
if ! command -v "$tool" >/dev/null 2>&1; then
echo "$tool is required" >&2
exit 127
fi
}
python_bin() {
if command -v python3 >/dev/null 2>&1; then
printf '%s\n' python3
else
printf '%s\n' python
fi
}
copy_app_fixture() {
mkdir -p "$APP_COPY"
(
cd "$SOURCE_APP_DIR"
tar \
--exclude='./build' \
--exclude='./.dart_tool' \
--exclude='./android/.gradle' \
--exclude='./ios/Pods' \
--exclude='./macos/Flutter/ephemeral' \
-cf - .
) | tar -C "$APP_COPY" -xf -
}
ensure_linux_platform() {
if [[ -d "$APP_COPY/linux" ]]; then
return
fi
(
cd "$APP_COPY"
"$FLUTTER_BIN" create --platforms=linux --project-name=license_flavor_patch_test .
)
}
build_linux_bundle() {
local license="$1"
local output="$2"
(
cd "$APP_COPY"
"$FLUTTER_BIN" build linux --release \
--local-engine-src-path="$LOCAL_ENGINE_SRC_PATH" \
--local-engine="$LOCAL_ENGINE" \
--local-engine-host="$LOCAL_ENGINE_HOST" \
--dart-define="LICENSE_TYPE=$license"
)
local bundle
bundle="$(find "$APP_COPY/build/linux" -path '*/release/bundle' -type d -print -quit)"
if [[ -z "$bundle" || ! -x "$bundle/license_flavor_patch_test" ]]; then
echo "Failed to find Linux release bundle under $APP_COPY/build/linux" >&2
exit 66
fi
rm -rf "$output"
cp -a "$bundle" "$output"
}
release_version_for_bundle() {
local bundle="$1"
local version_json="$bundle/data/flutter_assets/version.json"
local py
py="$(python_bin)"
"$py" - "$version_json" <<'PY'
import json
import pathlib
import sys
version = json.loads(pathlib.Path(sys.argv[1]).read_text())
build_name = str(version.get("version", ""))
build_number = str(version.get("build_number", ""))
if build_number:
print(f"{build_name}+{build_number}")
else:
print(build_name)
PY
}
run_saved_app() {
local label="$1"
local bundle="$2"
local stdout="$WORK_DIR/$label.stdout"
local stderr="$WORK_DIR/$label.stderr"
local tmp_status="$WORK_DIR/tmp/license_flavor_patch_status.txt"
local home_status="$HOME_DIR/Library/Application Support/license_flavor_patch_status.txt"
rm -f "$tmp_status" "$home_status"
mkdir -p "$WORK_DIR/tmp" "$HOME_DIR"
local -a app_command=(env HOME="$HOME_DIR" TMPDIR="$WORK_DIR/tmp" "$bundle/license_flavor_patch_test")
if [[ "${LINUX_RUNTIME_SMOKE_XVFB:-auto}" != "0" && -z "${DISPLAY:-}" && "$(command -v xvfb-run || true)" != "" ]]; then
app_command=(xvfb-run -a "${app_command[@]}")
fi
"${app_command[@]}" >"$stdout" 2>"$stderr" &
local pid=$!
for _ in $(seq 1 "${LINUX_RUNTIME_WAIT_ATTEMPTS:-120}"); do
if [[ -f "$tmp_status" || -f "$home_status" ]]; then
break
fi
if ! kill -0 "$pid" 2>/dev/null; then
break
fi
sleep 0.25
done
if kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
else
wait "$pid" 2>/dev/null || true
fi
echo "${label}_stdout=$stdout"
echo "${label}_stderr=$stderr"
if [[ -f "$tmp_status" ]]; then
echo "${label}_status_file=$tmp_status"
cat "$tmp_status"
elif [[ -f "$home_status" ]]; then
echo "${label}_status_file=$home_status"
cat "$home_status"
else
echo "${label}_status_file_missing" >&2
echo "--- $label stderr ---" >&2
sed -n '1,180p' "$stderr" >&2 || true
return 70
fi
}
read_launch_status() {
local tmp_status="$WORK_DIR/tmp/license_flavor_patch_status.txt"
local home_status="$HOME_DIR/Library/Application Support/license_flavor_patch_status.txt"
if [[ -f "$tmp_status" ]]; then
cat "$tmp_status"
elif [[ -f "$home_status" ]]; then
cat "$home_status"
fi
}
require_status() {
local label="$1"
local expected_license="$2"
local expected_feature="$3"
local status
status="$(read_launch_status)"
if ! grep -q "license:$expected_license" <<<"$status" ||
! grep -q "pro-feature:$expected_feature" <<<"$status"; then
echo "$label Linux app did not report expected status." >&2
echo "Expected: license:$expected_license / pro-feature:$expected_feature" >&2
echo "Actual:" >&2
printf '%s\n' "$status" >&2
exit 70
fi
}
seed_patch() {
local pro_bundle="$1"
local release_version="$2"
local patch_file="$pro_bundle/lib/libapp.so"
local state_root="$HOME_DIR/.shorebird_cache/shorebird_updater/$APP_ID"
local size
size="$(wc -c <"$patch_file" | tr -d ' ')"
rm -rf "$state_root"
mkdir -p "$state_root/patches/1"
cp "$patch_file" "$state_root/patches/1/dlc.vmcode"
cat >"$state_root/state.json" <<EOF
{
"client_id": "linux-runtime-smoke",
"release_version": "$release_version",
"queued_events": []
}
EOF
cat >"$state_root/pointers.json" <<'EOF'
{
"next_boot_patch": 1,
"last_booted_patch": null,
"currently_booting_patch": null,
"boot_started_at": null
}
EOF
cat >"$state_root/patches/1/state.json" <<EOF
{
"kind": "Installed",
"signature": null,
"size": $size
}
EOF
echo "linux_seeded_patch=$state_root/patches/1/dlc.vmcode"
echo "linux_seeded_patch_size=$size"
}
require_tool "$FLUTTER_BIN"
require_tool tar
copy_app_fixture
ensure_linux_platform
free_bundle="$WORK_DIR/free-bundle"
pro_bundle="$WORK_DIR/pro-bundle"
build_linux_bundle free "$free_bundle"
build_linux_bundle pro "$pro_bundle"
release_version="$(release_version_for_bundle "$free_bundle")"
run_saved_app base "$free_bundle"
require_status base free off
seed_patch "$pro_bundle" "$release_version"
run_saved_app patch "$free_bundle"
require_status patch pro enabled
echo "linux_runtime_patch_smoke=passed"
+11 -1
View File
@@ -6,6 +6,7 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DART_BIN="${DART_BIN:-dart}"
FLUTTER_BIN="${FLUTTER_BIN:-flutter}"
GO_BIN="${GO_BIN:-go}"
CARGO_BIN="${CARGO_BIN:-cargo}"
run() {
echo
@@ -23,6 +24,7 @@ require_command() {
require_command git
require_command "$DART_BIN"
require_command "$GO_BIN"
require_command "$CARGO_BIN"
if [[ "$PLATFORM" == "macos" ]]; then
if command -v xcodebuild >/dev/null 2>&1; then
@@ -37,9 +39,13 @@ run "$ROOT/scripts/write_gclient.sh" "$PLATFORM"
run "$ROOT/scripts/sync_open_sources.sh"
export PATH="$ROOT/depot_tools:$PATH"
export DEPOT_TOOLS_UPDATE="${DEPOT_TOOLS_UPDATE:-0}"
if [[ "${SKIP_GCLIENT_SYNC:-0}" != "1" ]]; then
require_command gclient
run gclient sync --no-history
if [[ "${INCLUDE_ENGINE_DEPS:-0}" == "1" && -f "$ROOT/flutter/.gclient" ]]; then
run bash -lc "cd '$ROOT/flutter' && gclient sync --no-history"
fi
else
echo
echo "==> skipping gclient sync because SKIP_GCLIENT_SYNC=1"
@@ -51,8 +57,12 @@ if [[ "${SKIP_TESTS:-0}" == "1" ]]; then
exit 0
fi
run bash -lc "cd '$ROOT/shorebird/packages/shorebird_cli' && '$DART_BIN' pub get && '$DART_BIN' test test/src/user_config_test.dart test/src/shorebird_env_test.dart test/src/shorebird_cli_command_runner_test.dart test/src/commands/init_command_test.dart"
run bash -lc "cd '$ROOT/shorebird' && '$DART_BIN' pub get"
run bash -lc "cd '$ROOT/shorebird/packages/shorebird_cli' && '$DART_BIN' pub get && '$DART_BIN' test test/src/user_config_test.dart test/src/shorebird_env_test.dart test/src/shorebird_cli_command_runner_test.dart test/src/commands/doctor_command_test.dart test/src/commands/init_command_test.dart test/src/cache_test.dart test/src/shorebird_process_test.dart test/src/network_checker_test.dart test/src/shorebird_web_console_test.dart test/src/auth/auth_test.dart test/src/commands/login_command_test.dart test/src/commands/login_ci_command_test.dart test/src/commands/release/aar_releaser_test.dart test/src/shorebird_validator_test.dart test/src/shorebird_flutter_test.dart test/src/shorebird_artifacts_test.dart test/src/artifact_builder/artifact_builder_test.dart test/src/config/shorebird_yaml_test.dart test/src/commands/patch/ios_patcher_test.dart"
run bash -lc "cd '$ROOT/shorebird' && '$DART_BIN' test packages/shorebird_code_push_client/test/src/code_push_client_test.dart"
run bash -lc "cd '$ROOT/shorebird' && '$DART_BIN' test packages/artifact_proxy/test/artifact_proxy_test.dart packages/artifact_proxy/test/server_bin_test.dart packages/artifact_proxy/test/src/artifact_manifest_client_test.dart"
run bash -lc "cd '$ROOT/shorebird/packages/open_aot_patch_tools' && '$DART_BIN' pub get && '$DART_BIN' test"
run "$CARGO_BIN" test --manifest-path "$ROOT/updater/library/Cargo.toml"
run bash -lc "cd '$ROOT/shorebird-server' && '$GO_BIN' test ./..."
AOT_PATCH_BUILD_DIR="${AOT_PATCH_BUILD_DIR:-}"
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
from pathlib import Path, PurePosixPath
import shutil
import sys
import tarfile
def fail(message: str) -> None:
print(f"error: {message}", file=sys.stderr)
raise SystemExit(70)
def safe_member_name(member: tarfile.TarInfo) -> str | None:
name = member.name
if member.isdir():
name = name.rstrip("/")
if not name or name == ".":
return None
if "\\" in name or "\x00" in name:
return None
if any(ord(character) < 32 for character in name):
return None
candidate = PurePosixPath(name)
if candidate.is_absolute():
return None
if any(part in ("", ".", "..") for part in candidate.parts):
return None
if candidate.parts and ":" in candidate.parts[0]:
return None
return candidate.as_posix()
def validate_members(archive_path: Path, members: list[tarfile.TarInfo]) -> None:
member_types: dict[str, str] = {}
for member in members:
safe_name = safe_member_name(member)
if safe_name is None:
fail(f"{archive_path}: unsafe archive member path {member.name!r}")
if not (member.isdir() or member.isfile()):
fail(f"{archive_path}: unsupported archive member type {member.name!r}")
member_type = "dir" if member.isdir() else "file"
previous_type = member_types.get(safe_name)
if previous_type is None:
member_types[safe_name] = member_type
continue
if previous_type != "dir" or member_type != "dir":
fail(f"{archive_path}: duplicate archive member path {member.name!r}")
def ensure_within_root(archive_path: Path, extract_root: Path, target: Path, name: str) -> None:
try:
target.relative_to(extract_root)
except ValueError:
fail(f"{archive_path}: archive member escapes extraction root {name!r}")
def extract_safe_tar_archive(archive_path: Path, extract_dir: Path) -> None:
extract_root = extract_dir.resolve()
extracted_files: set[Path] = set()
try:
with tarfile.open(archive_path, "r:*") as archive:
members = archive.getmembers()
validate_members(archive_path, members)
for member in members:
safe_name = safe_member_name(member)
assert safe_name is not None
target = (extract_root / safe_name).resolve()
ensure_within_root(archive_path, extract_root, target, member.name)
if member.isdir():
if target in extracted_files:
fail(f"{archive_path}: directory collides with file {member.name!r}")
target.mkdir(parents=True, exist_ok=True)
continue
for parent in target.parents:
if parent == extract_root:
break
if parent in extracted_files:
fail(f"{archive_path}: file parent collides with file {member.name!r}")
if target.exists() and not target.is_file():
fail(f"{archive_path}: file collides with directory {member.name!r}")
target.parent.mkdir(parents=True, exist_ok=True)
source = archive.extractfile(member)
if source is None:
fail(f"{archive_path}: unable to read archive member {member.name!r}")
with source, target.open("wb") as output:
shutil.copyfileobj(source, output)
target.chmod(member.mode & 0o777)
extracted_files.add(target)
except tarfile.TarError as error:
fail(f"{archive_path}: invalid tar archive: {error}")
def main() -> int:
parser = argparse.ArgumentParser(
description="Safely extract a tar archive containing only files and directories.",
)
parser.add_argument("archive", type=Path)
parser.add_argument("extract_dir", type=Path)
args = parser.parse_args()
args.extract_dir.mkdir(parents=True, exist_ok=True)
extract_safe_tar_archive(args.archive, args.extract_dir)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+127 -47
View File
@@ -5,13 +5,99 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DART_SRC="${DART_SRC:-$ROOT/dart-sdk-new}"
DART_TARGET="$ROOT/flutter/engine/src/flutter/third_party/dart"
UPDATER_SRC="${UPDATER_SRC:-$ROOT/updater}"
UPDATER_URL="${UPDATER_URL:-https://github.com/shorebirdtech/updater.git}"
UPDATER_URL="${UPDATER_URL:-}"
TARGET="$ROOT/flutter/engine/src/flutter/third_party/updater"
is_git_checkout() {
git -C "$1" rev-parse --git-dir >/dev/null 2>&1
}
relative_path() {
python3 - "$1" "$2" <<'PY'
import os
import sys
print(os.path.relpath(sys.argv[2], os.path.dirname(sys.argv[1])))
PY
}
real_path() {
python3 - "$1" <<'PY'
import os
import sys
print(os.path.realpath(sys.argv[1]))
PY
}
link_checkout() {
local target="$1"
local source="$2"
local label="$3"
local rel_target
rel_target="$(relative_path "$target" "$source")"
ln -s "$rel_target" "$target"
echo "[open-source-sync] linked $label checkout into Flutter engine."
}
is_clean_git_checkout() {
[[ -z "$(git -C "$1" status --porcelain)" ]]
}
reject_forbidden_remotes() {
local source="$1"
local label="$2"
shift 2
local remotes
remotes="$(git -C "$source" remote -v 2>/dev/null || true)"
if [[ -z "$remotes" ]]; then
return
fi
local forbidden
for forbidden in "$@"; do
if grep -Fq "$forbidden" <<<"$remotes"; then
echo "$label source checkout uses forbidden remote fragment '$forbidden': $source" >&2
echo "$remotes" >&2
exit 1
fi
done
}
ensure_source_link() {
local target="$1"
local source="$2"
local label="$3"
if [[ ! -d "$source" ]] || ! is_git_checkout "$source"; then
echo "$label source checkout is missing: $source" >&2
exit 1
fi
if [[ -L "$target" ]]; then
local target_real
local source_real
target_real="$(real_path "$target")"
source_real="$(real_path "$source")"
if [[ "$target_real" != "$source_real" ]]; then
echo "$label target symlink points at $target_real, expected $source_real" >&2
exit 1
fi
echo "[open-source-sync] $label target already links to the workspace checkout."
elif is_git_checkout "$target"; then
if ! is_clean_git_checkout "$target"; then
echo "$label target is a dirty git checkout and cannot be replaced: $target" >&2
exit 1
fi
rm -rf "$target"
link_checkout "$target" "$source" "$label"
elif [[ -e "$target" ]]; then
echo "$label target exists but is not a symlink or git checkout: $target" >&2
exit 1
else
link_checkout "$target" "$source" "$label"
fi
}
echo "[open-source-sync] dart source: $DART_SRC"
echo "[open-source-sync] dart target: $DART_TARGET"
echo "[open-source-sync] updater source: $UPDATER_SRC"
@@ -20,60 +106,54 @@ echo "[open-source-sync] target: $TARGET"
mkdir -p "$(dirname "$DART_TARGET")"
mkdir -p "$(dirname "$TARGET")"
if [[ -L "$DART_TARGET" ]]; then
echo "[open-source-sync] Dart target is already a symlink."
elif is_git_checkout "$DART_TARGET"; then
echo "[open-source-sync] Dart target is already a git checkout."
elif [[ -e "$DART_TARGET" ]]; then
echo "Dart target exists but is not a symlink or git checkout: $DART_TARGET" >&2
exit 1
elif is_git_checkout "$DART_SRC"; then
rel_target="$(python3 - "$DART_TARGET" "$DART_SRC" <<'PY'
import os
import sys
print(os.path.relpath(sys.argv[2], os.path.dirname(sys.argv[1])))
PY
)"
ln -s "$rel_target" "$DART_TARGET"
echo "[open-source-sync] linked Dart SDK checkout into Flutter engine."
else
echo "Dart source checkout is missing: $DART_SRC" >&2
exit 1
fi
ensure_source_link "$DART_TARGET" "$DART_SRC" "Dart SDK"
reject_forbidden_remotes \
"$DART_SRC" \
"Dart SDK" \
"github.com/dart-lang/sdk" \
"dart.googlesource.com/sdk"
if [[ ! -f "$DART_TARGET/runtime/vm/dart_api_impl.h" ]]; then
echo "Dart checkout is missing runtime/vm/dart_api_impl.h" >&2
exit 1
fi
if [[ -L "$TARGET" ]]; then
echo "[open-source-sync] updater target is already a symlink."
exit 0
fi
if is_git_checkout "$TARGET"; then
echo "[open-source-sync] updating existing updater checkout."
git -C "$TARGET" fetch --tags origin
git -C "$TARGET" checkout "${UPDATER_REVISION:-main}"
if [[ "${UPDATER_REVISION:-main}" == "main" ]]; then
git -C "$TARGET" pull --ff-only
if is_git_checkout "$UPDATER_SRC"; then
ensure_source_link "$TARGET" "$UPDATER_SRC" "updater submodule"
reject_forbidden_remotes \
"$UPDATER_SRC" \
"updater submodule" \
"github.com/shorebirdtech/updater" \
"github.com/shorebirdtech/shorebird-updater"
elif [[ -n "$UPDATER_URL" ]]; then
if [[ "$UPDATER_URL" == *github.com/shorebirdtech/updater* ||
"$UPDATER_URL" == *github.com/shorebirdtech/shorebird-updater* ]]; then
echo "UPDATER_URL points at a forbidden official Shorebird updater remote: $UPDATER_URL" >&2
exit 1
fi
if [[ -L "$TARGET" ]]; then
rm "$TARGET"
fi
if is_git_checkout "$TARGET"; then
echo "[open-source-sync] updating existing updater checkout."
git -C "$TARGET" remote set-url origin "$UPDATER_URL"
git -C "$TARGET" fetch --tags origin
git -C "$TARGET" checkout "${UPDATER_REVISION:-main}"
if [[ "${UPDATER_REVISION:-main}" == "main" ]]; then
git -C "$TARGET" pull --ff-only
fi
elif [[ -e "$TARGET" ]]; then
echo "target exists but is not a symlink or git checkout: $TARGET" >&2
exit 1
else
echo "[open-source-sync] cloning updater checkout from explicit UPDATER_URL."
git clone "$UPDATER_URL" "$TARGET"
git -C "$TARGET" checkout "${UPDATER_REVISION:-main}"
fi
elif [[ -e "$TARGET" ]]; then
echo "target exists but is not a symlink or git checkout: $TARGET" >&2
exit 1
elif is_git_checkout "$UPDATER_SRC"; then
rel_target="$(python3 - "$TARGET" "$UPDATER_SRC" <<'PY'
import os
import sys
print(os.path.relpath(sys.argv[2], os.path.dirname(sys.argv[1])))
PY
)"
ln -s "$rel_target" "$TARGET"
echo "[open-source-sync] linked updater submodule into Flutter engine."
else
echo "[open-source-sync] cloning public updater checkout."
git clone "$UPDATER_URL" "$TARGET"
git -C "$TARGET" checkout "${UPDATER_REVISION:-main}"
echo "updater source checkout is missing: $UPDATER_SRC" >&2
echo "Set UPDATER_SRC to a local fork or set UPDATER_URL explicitly." >&2
exit 1
fi
if [[ ! -f "$TARGET/library/include/updater_engine.h" ]]; then
+210
View File
@@ -0,0 +1,210 @@
#!/usr/bin/env python3
"""Validate an assembled open Shorebird artifact mirror."""
from __future__ import annotations
import argparse
import hashlib
from pathlib import Path, PurePosixPath
import re
import sys
import zipfile
REQUIRED_PATCH_ZIPS = {
"patch-linux-x64.zip": "patch",
"patch-darwin-x64.zip": "patch",
"patch-darwin-arm64.zip": "patch",
"patch-windows-x64.zip": "patch.exe",
}
def digest_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as file:
for chunk in iter(lambda: file.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def is_plain_file(path: Path) -> bool:
return path.is_file() and not path.is_symlink()
def parse_sidecar(path: Path) -> tuple[str, str]:
text = path.read_text(encoding="utf-8").strip()
parts = text.split()
if len(parts) != 2:
raise ValueError(f"expected '<sha256> <filename>', got {text!r}")
digest, filename = parts
if len(digest) != 64 or any(char not in "0123456789abcdef" for char in digest):
raise ValueError(f"invalid sha256 digest {digest!r}")
return digest, filename
def is_safe_relative_path(path: str) -> bool:
if not path or path == ".":
return False
if "\\" in path or "\x00" in path or path.endswith("/"):
return False
if any(ord(character) < 32 for character in path):
return False
candidate = PurePosixPath(path)
if candidate.is_absolute():
return False
if any(part in ("", ".", "..") for part in candidate.parts):
return False
if candidate.parts and ":" in candidate.parts[0]:
return False
return True
def validate_sidecars(shorebird_root: Path) -> list[str]:
errors: list[str] = []
for artifact_path in sorted(shorebird_root.rglob("*")):
if artifact_path.is_symlink():
errors.append(f"{artifact_path.relative_to(shorebird_root.parent)}: symlink entries are not allowed")
continue
if not artifact_path.is_file():
continue
if artifact_path.suffix == ".sha256":
artifact_path_without_suffix = Path(str(artifact_path)[: -len(".sha256")])
if not is_plain_file(artifact_path_without_suffix):
errors.append(f"{artifact_path.relative_to(shorebird_root.parent)}: orphan sidecar")
continue
sidecar_path = Path(f"{artifact_path}.sha256")
if not is_plain_file(sidecar_path):
errors.append(f"{artifact_path.relative_to(shorebird_root.parent)}: missing sidecar")
continue
try:
sidecar_digest, sidecar_filename = parse_sidecar(sidecar_path)
except ValueError as error:
errors.append(f"{sidecar_path.relative_to(shorebird_root.parent)}: {error}")
continue
actual_digest = digest_file(artifact_path)
if sidecar_digest != actual_digest:
errors.append(
f"{sidecar_path.relative_to(shorebird_root.parent)}: digest mismatch "
f"{sidecar_digest} != {actual_digest}"
)
if sidecar_filename != artifact_path.name:
errors.append(
f"{sidecar_path.relative_to(shorebird_root.parent)}: filename mismatch "
f"{sidecar_filename!r} != {artifact_path.name!r}"
)
return errors
def validate_manifest_overrides(mirror_root: Path, manifest_paths: list[Path]) -> list[str]:
override_pattern = re.compile(r"^\s*-\s*'?(?P<path>[^'#\n]+?)'?\s*(?:#.*)?$")
shorebird_root = mirror_root / "shorebird"
errors: list[str] = []
for manifest_path in manifest_paths:
engine_revision = manifest_path.parent.name
for line in manifest_path.read_text(encoding="utf-8").splitlines():
match = override_pattern.match(line)
if not match:
continue
artifact_path = match.group("path").replace("$engine", engine_revision)
if not is_safe_relative_path(artifact_path):
errors.append(
f"{manifest_path.relative_to(mirror_root)} -> "
f"unsafe artifact override path: {artifact_path}"
)
continue
resolved_artifact_path = shorebird_root / artifact_path
if not is_plain_file(resolved_artifact_path):
errors.append(
f"{manifest_path.relative_to(mirror_root)} -> shorebird/{artifact_path}"
)
continue
if resolved_artifact_path.stat().st_size <= 0:
errors.append(
f"{manifest_path.relative_to(mirror_root)} -> "
f"shorebird/{artifact_path}: artifact override is empty"
)
return errors
def validate_patch_zips(mirror_root: Path, manifest_paths: list[Path]) -> list[str]:
errors: list[str] = []
for manifest_path in manifest_paths:
engine_dir = manifest_path.parent
for zip_name, expected_entry in REQUIRED_PATCH_ZIPS.items():
zip_path = engine_dir / zip_name
display_path = zip_path.relative_to(mirror_root)
if not is_plain_file(zip_path):
errors.append(f"{display_path}: missing")
continue
try:
with zipfile.ZipFile(zip_path) as archive:
names = archive.namelist()
if names != [expected_entry]:
errors.append(
f"{display_path}: expected only {expected_entry!r}, got {names!r}"
)
continue
if archive.getinfo(expected_entry).file_size <= 0:
errors.append(f"{display_path}: {expected_entry} is empty")
except zipfile.BadZipFile:
errors.append(f"{display_path}: invalid zip")
return errors
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("mirror_root", type=Path)
return parser.parse_args()
def main() -> int:
args = parse_args()
mirror_root = args.mirror_root
shorebird_root = mirror_root / "shorebird"
if not mirror_root.is_dir():
print(f"missing mirror root: {mirror_root}", file=sys.stderr)
return 66
if not shorebird_root.is_dir():
print(f"mirror root is missing shorebird/: {mirror_root}", file=sys.stderr)
return 70
manifest_paths = sorted(shorebird_root.glob("*/artifacts_manifest.yaml"))
if not manifest_paths:
print(
"artifact mirror is missing shorebird/<engine>/artifacts_manifest.yaml",
file=sys.stderr,
)
return 70
errors: list[str] = []
sidecar_errors = validate_sidecars(shorebird_root)
if sidecar_errors:
errors.append("invalid checksum sidecars:\n" + "\n".join(f" {e}" for e in sidecar_errors))
override_errors = validate_manifest_overrides(mirror_root, manifest_paths)
if override_errors:
errors.append(
"invalid files referenced by artifacts_manifest.yaml:\n"
+ "\n".join(f" {path}" for path in override_errors)
)
patch_errors = validate_patch_zips(mirror_root, manifest_paths)
if patch_errors:
errors.append(
"invalid CLI patch-tool artifacts:\n"
+ "\n".join(f" {error}" for error in patch_errors)
)
if errors:
print("artifact mirror validation failed:\n" + "\n".join(errors), file=sys.stderr)
return 70
print(f"artifact mirror validated: {mirror_root}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+249
View File
@@ -0,0 +1,249 @@
#!/usr/bin/env python3
"""Validate a JSON release manifest against downloaded CI artifacts."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path, PurePosixPath
import sys
def digest_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as file:
for chunk in iter(lambda: file.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def is_plain_file(path: Path) -> bool:
return path.is_file() and not path.is_symlink()
def parse_sidecar(path: Path) -> tuple[str, str]:
text = path.read_text(encoding="utf-8").strip()
parts = text.split()
if len(parts) != 2:
raise ValueError(f"expected '<sha256> <filename>', got {text!r}")
digest, filename = parts
if len(digest) != 64 or any(char not in "0123456789abcdef" for char in digest):
raise ValueError(f"invalid sha256 digest {digest!r}")
return digest, filename
def is_safe_relative_path(path: str) -> bool:
if not path or path == ".":
return False
if "\\" in path or "\x00" in path or path.endswith("/"):
return False
if any(ord(character) < 32 for character in path):
return False
candidate = PurePosixPath(path)
if candidate.is_absolute():
return False
if any(part in ("", ".", "..") for part in candidate.parts):
return False
if candidate.parts and ":" in candidate.parts[0]:
return False
return True
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument(
"--github-sha",
default="",
help="Require the manifest github_sha field to match this commit SHA.",
)
parser.add_argument("input_dir", type=Path)
parser.add_argument("manifest", type=Path)
return parser.parse_args()
def main() -> int:
args = parse_args()
input_dir = args.input_dir
manifest_path = args.manifest
if not input_dir.is_dir():
print(f"missing input directory: {input_dir}", file=sys.stderr)
return 66
if not manifest_path.is_file():
print(f"missing release manifest: {manifest_path}", file=sys.stderr)
return 66
try:
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
except json.JSONDecodeError as error:
print(f"invalid release manifest JSON: {error}", file=sys.stderr)
return 70
errors: list[str] = []
if manifest.get("format_version") != 1:
errors.append(f"format_version is {manifest.get('format_version')!r}; expected 1")
if not isinstance(manifest.get("github_sha", ""), str):
errors.append("github_sha must be a string")
elif args.github_sha and manifest.get("github_sha") != args.github_sha:
errors.append(
f"github_sha is {manifest.get('github_sha')!r}; expected {args.github_sha!r}"
)
artifacts = manifest.get("artifacts")
if not isinstance(artifacts, list):
errors.append("artifacts must be a list")
artifacts = []
expected_count = manifest.get("artifact_count")
if expected_count != len(artifacts):
errors.append(
f"artifact_count is {expected_count!r}; expected {len(artifacts)}"
)
seen_paths: set[str] = set()
seen_sidecars: set[str] = set()
for index, artifact in enumerate(artifacts):
if not isinstance(artifact, dict):
errors.append(f"artifacts[{index}] must be an object")
continue
artifact_path_text = artifact.get("path")
artifact_group = artifact.get("artifact_group")
filename = artifact.get("filename")
sidecar_path_text = artifact.get("sidecar")
expected_digest = artifact.get("sha256")
expected_size = artifact.get("size")
if not isinstance(artifact_path_text, str):
errors.append(f"artifacts[{index}].path must be a string")
continue
if artifact_path_text in seen_paths:
errors.append(f"{artifact_path_text}: duplicate artifact path")
seen_paths.add(artifact_path_text)
if not is_safe_relative_path(artifact_path_text):
errors.append(f"{artifact_path_text}: unsafe artifact path")
continue
artifact_relative = PurePosixPath(artifact_path_text)
if not isinstance(artifact_group, str):
errors.append(f"{artifact_path_text}: artifact_group must be a string")
elif artifact_group != artifact_relative.parts[0]:
errors.append(
f"{artifact_path_text}: artifact_group {artifact_group!r} "
f"does not match path group {artifact_relative.parts[0]!r}"
)
if not isinstance(filename, str):
errors.append(f"{artifact_path_text}: filename must be a string")
elif filename != artifact_relative.name:
errors.append(
f"{artifact_path_text}: filename {filename!r} "
f"does not match path filename {artifact_relative.name!r}"
)
if not isinstance(sidecar_path_text, str):
errors.append(f"{artifact_path_text}: sidecar must be a string")
continue
if sidecar_path_text in seen_sidecars:
errors.append(f"{sidecar_path_text}: duplicate sidecar path")
seen_sidecars.add(sidecar_path_text)
if not is_safe_relative_path(sidecar_path_text):
errors.append(f"{sidecar_path_text}: unsafe sidecar path")
continue
artifact_path = input_dir / artifact_path_text
sidecar_path = input_dir / sidecar_path_text
if not is_plain_file(artifact_path):
errors.append(f"{artifact_path_text}: missing artifact file")
continue
if not is_plain_file(sidecar_path):
errors.append(f"{sidecar_path_text}: missing sidecar file")
continue
actual_size = artifact_path.stat().st_size
if actual_size <= 0:
errors.append(f"{artifact_path_text}: empty artifacts are not allowed")
if sidecar_path != Path(f"{artifact_path}.sha256"):
errors.append(
f"{artifact_path_text}: sidecar path {sidecar_path_text!r} "
f"does not match sibling {artifact_path.name}.sha256"
)
actual_digest = digest_file(artifact_path)
if expected_digest != actual_digest:
errors.append(
f"{artifact_path_text}: digest mismatch "
f"{expected_digest!r} != {actual_digest}"
)
if expected_size != actual_size:
errors.append(
f"{artifact_path_text}: size mismatch "
f"{expected_size!r} != {actual_size}"
)
try:
sidecar_digest, sidecar_filename = parse_sidecar(sidecar_path)
except ValueError as error:
errors.append(f"{sidecar_path_text}: {error}")
continue
if sidecar_digest != actual_digest:
errors.append(
f"{sidecar_path_text}: sidecar digest mismatch "
f"{sidecar_digest} != {actual_digest}"
)
if sidecar_filename != artifact_path.name:
errors.append(
f"{sidecar_path_text}: sidecar filename mismatch "
f"{sidecar_filename!r} != {artifact_path.name!r}"
)
manifest_resolved = manifest_path.resolve()
manifest_sidecar_resolved = Path(f"{manifest_path}.sha256").resolve()
actual_artifacts: set[str] = set()
actual_sidecars: set[str] = set()
for path in sorted(input_dir.rglob("*")):
if not path.is_file():
continue
if path.is_symlink():
relative = path.relative_to(input_dir).as_posix()
if path.suffix == ".sha256":
actual_sidecars.add(relative)
else:
actual_artifacts.add(relative)
continue
resolved = path.resolve()
if resolved == manifest_resolved or resolved == manifest_sidecar_resolved:
continue
relative = path.relative_to(input_dir).as_posix()
if path.suffix == ".sha256":
actual_sidecars.add(relative)
else:
actual_artifacts.add(relative)
missing_from_manifest = sorted(actual_artifacts - seen_paths)
if missing_from_manifest:
errors.append(
"artifacts missing from release manifest: "
+ ", ".join(missing_from_manifest)
)
orphan_sidecars = sorted(actual_sidecars - seen_sidecars)
if orphan_sidecars:
errors.append(
"sidecars missing from release manifest: " + ", ".join(orphan_sidecars)
)
if errors:
print(
"release manifest validation failed:\n"
+ "\n".join(f" {error}" for error in errors),
file=sys.stderr,
)
return 70
print(f"release manifest validated: {manifest_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/open-shorebird-mirror-validator.XXXXXX")"
trap 'rm -rf "$TMP_DIR"' EXIT
"$ROOT/scripts/verify_assemble_artifact_mirror.sh" >/dev/null
PYTHON_BIN=python3
if ! command -v "$PYTHON_BIN" >/dev/null 2>&1; then
PYTHON_BIN=python
fi
ENGINE_REVISION=engine123
MIRROR_ROOT="$TMP_DIR/mirror"
mkdir -p "$MIRROR_ROOT/shorebird/$ENGINE_REVISION"
cat > "$MIRROR_ROOT/shorebird/$ENGINE_REVISION/artifacts_manifest.yaml" <<EOF
flutter_engine_revision: 'base-engine'
storage_bucket: 'shorebird'
artifact_overrides:
- 'flutter_infra_release/flutter/\$engine/linux-x64-release/artifacts.zip'
EOF
"$PYTHON_BIN" - "$MIRROR_ROOT/shorebird/$ENGINE_REVISION" <<'PY'
from pathlib import Path
import sys
import zipfile
root = Path(sys.argv[1])
patch_zips = {
"patch-linux-x64.zip": "patch",
"patch-darwin-x64.zip": "patch",
"patch-darwin-arm64.zip": "patch",
"patch-windows-x64.zip": "patch.exe",
}
for zip_name, entry_name in patch_zips.items():
with zipfile.ZipFile(root / zip_name, "w", zipfile.ZIP_DEFLATED) as archive:
archive.writestr(entry_name, f"{zip_name}:{entry_name}\n")
PY
mkdir -p "$MIRROR_ROOT/shorebird/flutter_infra_release/flutter/$ENGINE_REVISION/linux-x64-release"
printf 'linux-engine-artifacts\n' \
> "$MIRROR_ROOT/shorebird/flutter_infra_release/flutter/$ENGINE_REVISION/linux-x64-release/artifacts.zip"
while IFS= read -r -d '' mirror_file; do
if [[ "$mirror_file" == *.sha256 ]]; then
continue
fi
"$ROOT/scripts/write_sha256.sh" "$mirror_file"
done < <(find "$MIRROR_ROOT/shorebird" -type f -print0)
"$PYTHON_BIN" "$ROOT/scripts/validate_artifact_mirror.py" "$MIRROR_ROOT" >/dev/null
UNSAFE_MIRROR_ROOT="$TMP_DIR/unsafe-mirror"
cp -R "$MIRROR_ROOT" "$UNSAFE_MIRROR_ROOT"
"$PYTHON_BIN" - "$UNSAFE_MIRROR_ROOT/shorebird/$ENGINE_REVISION/artifacts_manifest.yaml" <<'PY'
from pathlib import Path
import sys
manifest_path = Path(sys.argv[1])
manifest_path.write_text(
"\n".join(
[
"flutter_engine_revision: 'base-engine'",
"storage_bucket: 'shorebird'",
"artifact_overrides:",
" - '../outside/artifacts.zip'",
]
)
+ "\n",
encoding="utf-8",
)
PY
"$ROOT/scripts/write_sha256.sh" \
"$UNSAFE_MIRROR_ROOT/shorebird/$ENGINE_REVISION/artifacts_manifest.yaml" \
"$UNSAFE_MIRROR_ROOT/shorebird/$ENGINE_REVISION/artifacts_manifest.yaml.sha256"
if "$PYTHON_BIN" "$ROOT/scripts/validate_artifact_mirror.py" \
"$UNSAFE_MIRROR_ROOT" >"$TMP_DIR/unsafe-mirror.log" 2>&1; then
echo "validate_artifact_mirror.py unexpectedly accepted an unsafe manifest override" >&2
exit 70
fi
grep -q "unsafe artifact override path" "$TMP_DIR/unsafe-mirror.log"
SYMLINK_MIRROR_ROOT="$TMP_DIR/symlink-mirror"
cp -R "$MIRROR_ROOT" "$SYMLINK_MIRROR_ROOT"
rm "$SYMLINK_MIRROR_ROOT/shorebird/$ENGINE_REVISION/patch-linux-x64.zip"
ln -s "$MIRROR_ROOT/shorebird/$ENGINE_REVISION/patch-linux-x64.zip" \
"$SYMLINK_MIRROR_ROOT/shorebird/$ENGINE_REVISION/patch-linux-x64.zip"
if "$PYTHON_BIN" "$ROOT/scripts/validate_artifact_mirror.py" \
"$SYMLINK_MIRROR_ROOT" >"$TMP_DIR/symlink-mirror.log" 2>&1; then
echo "validate_artifact_mirror.py unexpectedly accepted a symlink artifact" >&2
exit 70
fi
grep -q "symlink entries are not allowed" "$TMP_DIR/symlink-mirror.log"
EMPTY_OVERRIDE_ROOT="$TMP_DIR/empty-override-mirror"
cp -R "$MIRROR_ROOT" "$EMPTY_OVERRIDE_ROOT"
"$PYTHON_BIN" - "$EMPTY_OVERRIDE_ROOT/shorebird/flutter_infra_release/flutter/$ENGINE_REVISION/linux-x64-release/artifacts.zip" <<'PY'
from pathlib import Path
import sys
Path(sys.argv[1]).write_bytes(b"")
PY
"$ROOT/scripts/write_sha256.sh" \
"$EMPTY_OVERRIDE_ROOT/shorebird/flutter_infra_release/flutter/$ENGINE_REVISION/linux-x64-release/artifacts.zip" \
"$EMPTY_OVERRIDE_ROOT/shorebird/flutter_infra_release/flutter/$ENGINE_REVISION/linux-x64-release/artifacts.zip.sha256"
if "$PYTHON_BIN" "$ROOT/scripts/validate_artifact_mirror.py" \
"$EMPTY_OVERRIDE_ROOT" >"$TMP_DIR/empty-override-mirror.log" 2>&1; then
echo "validate_artifact_mirror.py unexpectedly accepted an empty manifest override artifact" >&2
exit 70
fi
grep -q "artifact override is empty" "$TMP_DIR/empty-override-mirror.log"
printf 'tampered\n' >> "$MIRROR_ROOT/shorebird/$ENGINE_REVISION/patch-linux-x64.zip"
if "$PYTHON_BIN" "$ROOT/scripts/validate_artifact_mirror.py" "$MIRROR_ROOT" >/dev/null 2>&1; then
echo "validate_artifact_mirror.py unexpectedly accepted a stale sidecar" >&2
exit 70
fi
echo "validate_artifact_mirror.py smoke test passed"
+287
View File
@@ -0,0 +1,287 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/open-shorebird-artifact-job.XXXXXX")"
trap 'rm -rf "$TMP_DIR"' EXIT
PYTHON_BIN=python3
if ! command -v "$PYTHON_BIN" >/dev/null 2>&1; then
PYTHON_BIN=python
fi
ENGINE_REVISION=engine123
DOWNLOADED="$TMP_DIR/downloaded-artifacts"
mkdir -p "$DOWNLOADED"
write_artifact() {
local path="$1"
local content="$2"
mkdir -p "$(dirname "$path")"
printf '%s\n' "$content" > "$path"
"$ROOT/scripts/write_sha256.sh" "$path"
}
write_zip() {
local zip_path="$1"
local entry_name="$2"
local content="$3"
mkdir -p "$(dirname "$zip_path")"
"$PYTHON_BIN" - "$zip_path" "$entry_name" "$content" <<'PY'
from pathlib import Path
import sys
import zipfile
zip_path, entry_name, content = sys.argv[1:]
with zipfile.ZipFile(Path(zip_path), "w", zipfile.ZIP_DEFLATED) as archive:
archive.writestr(entry_name, content + "\n")
PY
"$ROOT/scripts/write_sha256.sh" "$zip_path"
}
write_tgz() {
local archive_path="$1"
local staging_dir="$2"
local root_entry="$3"
mkdir -p "$(dirname "$archive_path")"
tar -C "$staging_dir" -czf "$archive_path" "$root_entry"
"$ROOT/scripts/write_sha256.sh" "$archive_path"
}
for target in \
cli-linux-x64/open-shorebird-cli-linux-x64.tar.gz \
cli-macos-x64/open-shorebird-cli-macos-x64.tar.gz \
cli-macos-arm64/open-shorebird-cli-macos-arm64.tar.gz \
cli-windows-x64/open-shorebird-cli-windows-x64.tar.gz \
shorebird-server-linux-amd64/shorebird-server-linux-amd64.tar.gz \
shorebird-server-linux-arm64/shorebird-server-linux-arm64.tar.gz \
shorebird-server-darwin-amd64/shorebird-server-darwin-amd64.tar.gz \
shorebird-server-darwin-arm64/shorebird-server-darwin-arm64.tar.gz \
shorebird-server-windows-amd64/shorebird-server-windows-amd64.tar.gz \
custom-dart-sdk-linux-x64/custom-dart-sdk-linux-x64.tar.gz \
custom-dart-sdk-macos-arm64/custom-dart-sdk-macos-arm64.tar.gz; do
write_artifact "$DOWNLOADED/$target" "$target"
done
mkdir -p "$DOWNLOADED/mirror-metadata/artifacts/mirror/shorebird/$ENGINE_REVISION"
cat > "$DOWNLOADED/mirror-metadata/artifacts/mirror/shorebird/$ENGINE_REVISION/artifacts_manifest.yaml" <<EOF
flutter_engine_revision: 'base-engine'
storage_bucket: 'shorebird'
artifact_overrides:
- 'flutter_infra_release/flutter/\$engine/android-arm64-release/artifacts.zip'
- 'flutter_infra_release/flutter/\$engine/android-arm64-release/symbols.zip'
- 'flutter_infra_release/flutter/\$engine/linux-x64-release/artifacts.zip'
- 'flutter_infra_release/flutter/\$engine/linux-x64-release/linux-x64-flutter-gtk.zip'
- 'flutter_infra_release/flutter/\$engine/ios-release/artifacts.zip'
- 'flutter_infra_release/flutter/\$engine/flutter_patched_sdk_product.zip'
- 'flutter_infra_release/flutter/\$engine/flutter-web-sdk.zip'
- 'flutter_infra_release/flutter/\$engine/darwin-arm64-release/FlutterMacOS.framework.zip'
EOF
"$ROOT/scripts/write_sha256.sh" \
"$DOWNLOADED/mirror-metadata/artifacts/mirror/shorebird/$ENGINE_REVISION/artifacts_manifest.yaml"
write_patch_artifact() {
local artifact_name="$1"
local zip_name="$2"
local entry_name="$3"
write_zip "$DOWNLOADED/$artifact_name/artifacts/mirror/$zip_name" "$entry_name" "$zip_name"
write_zip "$DOWNLOADED/$artifact_name/artifacts/mirror/shorebird/$ENGINE_REVISION/$zip_name" "$entry_name" "$zip_name"
}
write_patch_artifact mirror-patch-linux-x64.zip patch-linux-x64.zip patch
write_patch_artifact mirror-patch-darwin-x64.zip patch-darwin-x64.zip patch
write_patch_artifact mirror-patch-darwin-arm64.zip patch-darwin-arm64.zip patch
write_patch_artifact mirror-patch-windows-x64.zip patch-windows-x64.zip patch.exe
engine_stage="$TMP_DIR/engine-stage"
mkdir -p "$engine_stage"
make_engine_archive() {
local artifact_dir="$1"
local archive_name="$2"
local root_name="$3"
local mirror_subdir="$4"
shift 4
local stage="$engine_stage/$root_name"
rm -rf "$stage"
mkdir -p "$stage/$root_name/mirror/shorebird/flutter_infra_release/flutter/$ENGINE_REVISION/$mirror_subdir"
while [[ "$#" -gt 0 ]]; do
local file_name="$1"
local content="$2"
shift 2
write_artifact \
"$stage/$root_name/mirror/shorebird/flutter_infra_release/flutter/$ENGINE_REVISION/$mirror_subdir/$file_name" \
"$content"
done
write_tgz "$DOWNLOADED/$artifact_dir/$archive_name" "$stage" "$root_name"
}
make_engine_archive \
linux-engine-x64 linux-engine-x64.tar.gz linux-engine linux-x64-release \
artifacts.zip linux-artifacts \
linux-x64-flutter-gtk.zip linux-gtk
mkdir -p "$engine_stage/linux-engine/linux-engine/mirror/shorebird/flutter_infra_release/flutter/$ENGINE_REVISION"
write_artifact \
"$engine_stage/linux-engine/linux-engine/mirror/shorebird/flutter_infra_release/flutter/$ENGINE_REVISION/flutter_patched_sdk_product.zip" \
linux-patched-sdk
write_tgz "$DOWNLOADED/linux-engine-x64/linux-engine-x64.tar.gz" \
"$engine_stage/linux-engine" \
linux-engine
make_engine_archive \
android-engine-arm64 android-engine-arm64.tar.gz android-engine android-arm64-release \
artifacts.zip android-artifacts \
symbols.zip android-symbols
make_engine_archive \
flutter-web-sdk flutter-web-sdk.tar.gz web-sdk . \
flutter-web-sdk.zip web-sdk
make_engine_archive \
ios-interpreter-engine ios-interpreter-engine.tar.gz ios-engine ios-release \
artifacts.zip ios-artifacts
make_engine_archive \
macos-engine-arm64 macos-engine-arm64.tar.gz macos-engine darwin-arm64-release \
FlutterMacOS.framework.zip macos-framework
mirror_input="$TMP_DIR/mirror-input"
mkdir -p "$mirror_input"
cp -R "$DOWNLOADED"/mirror-* "$mirror_input/"
cp -R "$DOWNLOADED/linux-engine-x64" "$mirror_input/"
cp -R "$DOWNLOADED/android-engine-arm64" "$mirror_input/"
cp -R "$DOWNLOADED/flutter-web-sdk" "$mirror_input/"
cp -R "$DOWNLOADED/ios-interpreter-engine" "$mirror_input/"
cp -R "$DOWNLOADED/macos-engine-arm64" "$mirror_input/"
assembled="$TMP_DIR/artifacts/open-shorebird-artifact-mirror"
mkdir -p "$TMP_DIR/artifacts"
"$ROOT/scripts/assemble_artifact_mirror.sh" "$mirror_input" "$assembled" >/dev/null
tar -C "$TMP_DIR/artifacts" -czf "$TMP_DIR/open-shorebird-artifact-mirror.tar.gz" open-shorebird-artifact-mirror
mirror_extract_dir="$TMP_DIR/mirror-extract"
mkdir -p "$mirror_extract_dir"
"$PYTHON_BIN" "$ROOT/scripts/safe_extract_tar.py" \
"$TMP_DIR/open-shorebird-artifact-mirror.tar.gz" \
"$mirror_extract_dir"
"$PYTHON_BIN" "$ROOT/scripts/validate_artifact_mirror.py" \
"$mirror_extract_dir/open-shorebird-artifact-mirror" >/dev/null
"$ROOT/scripts/write_sha256.sh" "$TMP_DIR/open-shorebird-artifact-mirror.tar.gz"
manifest_input="$TMP_DIR/manifest-input"
mkdir -p "$manifest_input"
cp -R "$DOWNLOADED"/. "$manifest_input/"
mkdir -p "$manifest_input/open-shorebird-artifact-mirror"
cp "$TMP_DIR/open-shorebird-artifact-mirror.tar.gz" "$manifest_input/open-shorebird-artifact-mirror/"
cp "$TMP_DIR/open-shorebird-artifact-mirror.tar.gz.sha256" "$manifest_input/open-shorebird-artifact-mirror/"
"$PYTHON_BIN" "$ROOT/scripts/write_release_manifest.py" \
"$manifest_input" \
--github-sha test-sha \
--require 'cli-linux-x64/*open-shorebird-cli-linux-x64.tar.gz' \
--require 'cli-macos-x64/*open-shorebird-cli-macos-x64.tar.gz' \
--require 'cli-macos-arm64/*open-shorebird-cli-macos-arm64.tar.gz' \
--require 'cli-windows-x64/*open-shorebird-cli-windows-x64.tar.gz' \
--require 'shorebird-server-linux-amd64/*shorebird-server-linux-amd64.tar.gz' \
--require 'shorebird-server-linux-arm64/*shorebird-server-linux-arm64.tar.gz' \
--require 'shorebird-server-darwin-amd64/*shorebird-server-darwin-amd64.tar.gz' \
--require 'shorebird-server-darwin-arm64/*shorebird-server-darwin-arm64.tar.gz' \
--require 'shorebird-server-windows-amd64/*shorebird-server-windows-amd64.tar.gz' \
--require 'custom-dart-sdk-linux-x64/*custom-dart-sdk-linux-x64.tar.gz' \
--require 'custom-dart-sdk-macos-arm64/*custom-dart-sdk-macos-arm64.tar.gz' \
--require 'linux-engine-x64/*linux-engine-x64.tar.gz' \
--require 'android-engine-arm64/*android-engine-arm64.tar.gz' \
--require 'flutter-web-sdk/*flutter-web-sdk.tar.gz' \
--require 'ios-interpreter-engine/*ios-interpreter-engine.tar.gz' \
--require 'macos-engine-arm64/*macos-engine-arm64.tar.gz' \
--require 'mirror-patch-linux-x64.zip/*patch-linux-x64.zip' \
--require 'mirror-patch-darwin-x64.zip/*patch-darwin-x64.zip' \
--require 'mirror-patch-darwin-arm64.zip/*patch-darwin-arm64.zip' \
--require 'mirror-patch-windows-x64.zip/*patch-windows-x64.zip' \
--require 'mirror-metadata/*artifacts_manifest.yaml' \
--require 'open-shorebird-artifact-mirror/*open-shorebird-artifact-mirror.tar.gz' \
--output "$TMP_DIR/open-shorebird-release-manifest.json"
"$PYTHON_BIN" "$ROOT/scripts/validate_release_manifest.py" \
"$manifest_input" \
"$TMP_DIR/open-shorebird-release-manifest.json" >/dev/null
mkdir -p "$manifest_input/open-shorebird-release-manifest"
cp "$TMP_DIR/open-shorebird-release-manifest.json" \
"$manifest_input/open-shorebird-release-manifest/"
"$ROOT/scripts/write_sha256.sh" \
"$manifest_input/open-shorebird-release-manifest/open-shorebird-release-manifest.json"
"$ROOT/scripts/verify_downloaded_release_artifacts.sh" \
--github-sha test-sha \
"$manifest_input" >/dev/null
if "$ROOT/scripts/verify_downloaded_release_artifacts.sh" \
--github-sha wrong-sha \
"$manifest_input" >"$TMP_DIR/wrong-download-sha.log" 2>&1; then
echo "unexpectedly accepted downloaded artifacts for the wrong github_sha" >&2
exit 1
fi
grep -q "github_sha is" "$TMP_DIR/wrong-download-sha.log"
release_manifest_path="$manifest_input/open-shorebird-release-manifest/open-shorebird-release-manifest.json"
mirror_archive_path="$manifest_input/open-shorebird-artifact-mirror/open-shorebird-artifact-mirror.tar.gz"
printf '%064d open-shorebird-release-manifest.json\n' 0 > "$release_manifest_path.sha256"
if "$ROOT/scripts/verify_downloaded_release_artifacts.sh" \
--github-sha test-sha \
"$manifest_input" >/dev/null 2>&1; then
echo "unexpectedly accepted a stale downloaded release manifest sidecar" >&2
exit 1
fi
"$ROOT/scripts/write_sha256.sh" "$release_manifest_path"
printf '%064d open-shorebird-artifact-mirror.tar.gz\n' 0 > "$mirror_archive_path.sha256"
if "$ROOT/scripts/verify_downloaded_release_artifacts.sh" \
--github-sha test-sha \
"$manifest_input" >/dev/null 2>&1; then
echo "unexpectedly accepted a stale downloaded mirror archive sidecar" >&2
exit 1
fi
"$ROOT/scripts/write_sha256.sh" "$mirror_archive_path"
"$ROOT/scripts/verify_downloaded_release_artifacts.sh" \
--github-sha test-sha \
"$manifest_input" >/dev/null
unsafe_download="$TMP_DIR/unsafe-download"
cp -R "$manifest_input" "$unsafe_download"
unsafe_mirror_archive="$unsafe_download/open-shorebird-artifact-mirror/open-shorebird-artifact-mirror.tar.gz"
"$PYTHON_BIN" - "$unsafe_mirror_archive" <<'PY'
import io
import sys
import tarfile
archive_path = sys.argv[1]
with tarfile.open(archive_path, "w:gz") as archive:
content = b"unsafe\n"
member = tarfile.TarInfo("../outside.txt")
member.size = len(content)
archive.addfile(member, io.BytesIO(content))
PY
"$ROOT/scripts/write_sha256.sh" "$unsafe_mirror_archive"
"$PYTHON_BIN" "$ROOT/scripts/write_release_manifest.py" \
"$unsafe_download" \
--github-sha test-sha \
--require 'open-shorebird-artifact-mirror/*open-shorebird-artifact-mirror.tar.gz' \
--output "$unsafe_download/open-shorebird-release-manifest/open-shorebird-release-manifest.json"
"$ROOT/scripts/write_sha256.sh" \
"$unsafe_download/open-shorebird-release-manifest/open-shorebird-release-manifest.json"
if "$ROOT/scripts/verify_downloaded_release_artifacts.sh" \
--github-sha test-sha \
"$unsafe_download" >"$TMP_DIR/unsafe-download.log" 2>&1; then
echo "unexpectedly accepted an unsafe downloaded mirror archive" >&2
exit 1
fi
grep -q "unsafe archive member path" "$TMP_DIR/unsafe-download.log"
"$PYTHON_BIN" - "$TMP_DIR/open-shorebird-release-manifest.json" <<'PY'
import json
import sys
manifest = json.load(open(sys.argv[1], encoding="utf-8"))
paths = {artifact["path"] for artifact in manifest["artifacts"]}
assert any(path.endswith("open-shorebird-artifact-mirror.tar.gz") for path in paths)
assert any(path.endswith("linux-engine-x64.tar.gz") for path in paths)
assert any(path.endswith("patch-windows-x64.zip") for path in paths)
assert any(path.endswith("artifacts_manifest.yaml") for path in paths)
PY
echo "artifact-mirror workflow assembly smoke test passed"
+178
View File
@@ -0,0 +1,178 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/open-shorebird-assemble.XXXXXX")"
trap 'rm -rf "$TMP_DIR"' EXIT
PYTHON_BIN=python3
if ! command -v "$PYTHON_BIN" >/dev/null 2>&1; then
PYTHON_BIN=python
fi
ENGINE_REVISION=engine123
INPUT_DIR="$TMP_DIR/downloaded-artifacts"
OUTPUT_DIR="$TMP_DIR/mirror"
mkdir -p "$INPUT_DIR"
mkdir -p "$INPUT_DIR/mirror-metadata/artifacts/mirror/shorebird/$ENGINE_REVISION"
cat > "$INPUT_DIR/mirror-metadata/artifacts/mirror/shorebird/$ENGINE_REVISION/artifacts_manifest.yaml" <<EOF
flutter_engine_revision: 'base-engine'
storage_bucket: 'shorebird'
artifact_overrides:
- 'flutter_infra_release/flutter/\$engine/linux-x64-release/artifacts.zip'
EOF
mkdir -p "$INPUT_DIR/mirror-patch/artifacts/mirror/shorebird/$ENGINE_REVISION"
"$PYTHON_BIN" - "$INPUT_DIR/mirror-patch/artifacts/mirror/shorebird/$ENGINE_REVISION" <<'PY'
from pathlib import Path
import sys
import zipfile
root = Path(sys.argv[1])
patch_zips = {
"patch-linux-x64.zip": "patch",
"patch-darwin-x64.zip": "patch",
"patch-darwin-arm64.zip": "patch",
"patch-windows-x64.zip": "patch.exe",
}
for zip_name, entry_name in patch_zips.items():
with zipfile.ZipFile(root / zip_name, "w", zipfile.ZIP_DEFLATED) as archive:
archive.writestr(entry_name, f"{zip_name}:{entry_name}\n")
PY
engine_staging="$TMP_DIR/linux-engine"
mkdir -p "$engine_staging/linux-engine/mirror/shorebird/flutter_infra_release/flutter/$ENGINE_REVISION/linux-x64-release"
printf 'linux-engine-artifacts\n' \
> "$engine_staging/linux-engine/mirror/shorebird/flutter_infra_release/flutter/$ENGINE_REVISION/linux-x64-release/artifacts.zip"
tar -C "$engine_staging" -czf "$INPUT_DIR/linux-engine-x64.tar.gz" linux-engine
"$ROOT/scripts/assemble_artifact_mirror.sh" "$INPUT_DIR" "$OUTPUT_DIR"
test -f "$OUTPUT_DIR/shorebird/$ENGINE_REVISION/artifacts_manifest.yaml"
test -f "$OUTPUT_DIR/shorebird/$ENGINE_REVISION/artifacts_manifest.yaml.sha256"
test -f "$OUTPUT_DIR/shorebird/$ENGINE_REVISION/patch-linux-x64.zip"
test -f "$OUTPUT_DIR/shorebird/$ENGINE_REVISION/patch-linux-x64.zip.sha256"
test -f "$OUTPUT_DIR/shorebird/$ENGINE_REVISION/patch-darwin-x64.zip"
test -f "$OUTPUT_DIR/shorebird/$ENGINE_REVISION/patch-darwin-x64.zip.sha256"
test -f "$OUTPUT_DIR/shorebird/$ENGINE_REVISION/patch-darwin-arm64.zip"
test -f "$OUTPUT_DIR/shorebird/$ENGINE_REVISION/patch-darwin-arm64.zip.sha256"
test -f "$OUTPUT_DIR/shorebird/$ENGINE_REVISION/patch-windows-x64.zip"
test -f "$OUTPUT_DIR/shorebird/$ENGINE_REVISION/patch-windows-x64.zip.sha256"
test -f "$OUTPUT_DIR/shorebird/flutter_infra_release/flutter/$ENGINE_REVISION/linux-x64-release/artifacts.zip"
test -f "$OUTPUT_DIR/shorebird/flutter_infra_release/flutter/$ENGINE_REVISION/linux-x64-release/artifacts.zip.sha256"
CONFLICT_INPUT="$TMP_DIR/conflicting-artifacts"
mkdir -p "$CONFLICT_INPUT/conflict/artifacts/mirror/shorebird/$ENGINE_REVISION"
"$PYTHON_BIN" - "$CONFLICT_INPUT/conflict/artifacts/mirror/shorebird/$ENGINE_REVISION/patch-linux-x64.zip" <<'PY'
from pathlib import Path
import sys
import zipfile
with zipfile.ZipFile(Path(sys.argv[1]), "w", zipfile.ZIP_DEFLATED) as archive:
archive.writestr("patch", "different-patch\n")
PY
if "$ROOT/scripts/assemble_artifact_mirror.sh" "$CONFLICT_INPUT" "$OUTPUT_DIR" >/dev/null 2>&1; then
echo "assemble_artifact_mirror.sh unexpectedly allowed a conflicting mirror file" >&2
exit 70
fi
UNSAFE_TAR_INPUT="$TMP_DIR/unsafe-tar"
UNSAFE_TAR_OUTPUT="$TMP_DIR/unsafe-tar-output"
mkdir -p "$UNSAFE_TAR_INPUT"
"$PYTHON_BIN" - "$UNSAFE_TAR_INPUT/unsafe-engine.tar.gz" <<'PY'
import io
import sys
import tarfile
archive_path = sys.argv[1]
with tarfile.open(archive_path, "w:gz") as archive:
content = b"unsafe\n"
member = tarfile.TarInfo("../outside.txt")
member.size = len(content)
archive.addfile(member, io.BytesIO(content))
PY
if "$ROOT/scripts/assemble_artifact_mirror.sh" \
"$UNSAFE_TAR_INPUT" \
"$UNSAFE_TAR_OUTPUT" >"$TMP_DIR/unsafe-tar.log" 2>&1; then
echo "assemble_artifact_mirror.sh unexpectedly allowed an unsafe tar member" >&2
exit 70
fi
grep -q "unsafe archive member path" "$TMP_DIR/unsafe-tar.log"
SYMLINK_TAR_INPUT="$TMP_DIR/symlink-tar"
SYMLINK_TAR_OUTPUT="$TMP_DIR/symlink-tar-output"
mkdir -p "$SYMLINK_TAR_INPUT"
"$PYTHON_BIN" - "$SYMLINK_TAR_INPUT/symlink-engine.tar.gz" <<'PY'
import sys
import tarfile
archive_path = sys.argv[1]
with tarfile.open(archive_path, "w:gz") as archive:
member = tarfile.TarInfo("engine/mirror/shorebird/link")
member.type = tarfile.SYMTYPE
member.linkname = "/tmp/outside"
archive.addfile(member)
PY
if "$ROOT/scripts/assemble_artifact_mirror.sh" \
"$SYMLINK_TAR_INPUT" \
"$SYMLINK_TAR_OUTPUT" >"$TMP_DIR/symlink-tar.log" 2>&1; then
echo "assemble_artifact_mirror.sh unexpectedly allowed a tar symlink member" >&2
exit 70
fi
grep -q "unsupported archive member type" "$TMP_DIR/symlink-tar.log"
DUPLICATE_TAR_INPUT="$TMP_DIR/duplicate-tar"
DUPLICATE_TAR_OUTPUT="$TMP_DIR/duplicate-tar-output"
mkdir -p "$DUPLICATE_TAR_INPUT"
"$PYTHON_BIN" - "$DUPLICATE_TAR_INPUT/duplicate-engine.tar.gz" <<'PY'
import io
import sys
import tarfile
archive_path = sys.argv[1]
with tarfile.open(archive_path, "w:gz") as archive:
for content in (b"first\n", b"second\n"):
member = tarfile.TarInfo("engine/mirror/shorebird/duplicate.txt")
member.size = len(content)
archive.addfile(member, io.BytesIO(content))
PY
if "$ROOT/scripts/assemble_artifact_mirror.sh" \
"$DUPLICATE_TAR_INPUT" \
"$DUPLICATE_TAR_OUTPUT" >"$TMP_DIR/duplicate-tar.log" 2>&1; then
echo "assemble_artifact_mirror.sh unexpectedly allowed a duplicate tar member" >&2
exit 70
fi
grep -q "duplicate archive member path" "$TMP_DIR/duplicate-tar.log"
BAD_ZIP_INPUT="$TMP_DIR/bad-zip"
BAD_ZIP_OUTPUT="$TMP_DIR/bad-zip-output"
mkdir -p "$BAD_ZIP_INPUT/metadata/artifacts/mirror/shorebird/$ENGINE_REVISION"
cat > "$BAD_ZIP_INPUT/metadata/artifacts/mirror/shorebird/$ENGINE_REVISION/artifacts_manifest.yaml" <<EOF
flutter_engine_revision: 'base-engine'
storage_bucket: 'shorebird'
artifact_overrides: []
EOF
printf 'not a zip\n' > "$BAD_ZIP_INPUT/metadata/artifacts/mirror/shorebird/$ENGINE_REVISION/patch-linux-x64.zip"
if "$ROOT/scripts/assemble_artifact_mirror.sh" "$BAD_ZIP_INPUT" "$BAD_ZIP_OUTPUT" >/dev/null 2>&1; then
echo "assemble_artifact_mirror.sh unexpectedly allowed an invalid patch zip" >&2
exit 70
fi
MISSING_INPUT="$TMP_DIR/missing-override"
MISSING_OUTPUT="$TMP_DIR/missing-output"
mkdir -p "$MISSING_INPUT/metadata/artifacts/mirror/shorebird/$ENGINE_REVISION"
cat > "$MISSING_INPUT/metadata/artifacts/mirror/shorebird/$ENGINE_REVISION/artifacts_manifest.yaml" <<EOF
flutter_engine_revision: 'base-engine'
storage_bucket: 'shorebird'
artifact_overrides:
- 'flutter_infra_release/flutter/\$engine/ios-release/artifacts.zip'
EOF
if "$ROOT/scripts/assemble_artifact_mirror.sh" "$MISSING_INPUT" "$MISSING_OUTPUT" >/dev/null 2>&1; then
echo "assemble_artifact_mirror.sh unexpectedly allowed a missing manifest override" >&2
exit 70
fi
echo "assemble_artifact_mirror.sh smoke test passed"
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CI_MIN_FREE_DISK_GB=2 \
CI_AVAILABLE_DISK_KB_OVERRIDE=$((3 * 1024 * 1024)) \
"$ROOT/scripts/check_ci_capacity.sh" >/dev/null
if CI_MIN_FREE_DISK_GB=4 \
CI_AVAILABLE_DISK_KB_OVERRIDE=$((3 * 1024 * 1024)) \
"$ROOT/scripts/check_ci_capacity.sh" >/dev/null 2>&1; then
echo "check_ci_capacity.sh unexpectedly accepted insufficient disk" >&2
exit 70
fi
if CI_MIN_FREE_DISK_GB=not-a-number \
CI_AVAILABLE_DISK_KB_OVERRIDE=$((3 * 1024 * 1024)) \
"$ROOT/scripts/check_ci_capacity.sh" >/dev/null 2>&1; then
echo "check_ci_capacity.sh unexpectedly accepted an invalid minimum" >&2
exit 70
fi
CI_MIN_FREE_DISK_GB=0 "$ROOT/scripts/check_ci_capacity.sh" >/dev/null
echo "check_ci_capacity.sh smoke test passed"
+2124
View File
File diff suppressed because it is too large Load Diff
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WORKFLOW="$ROOT/.github/workflows/open-shorebird-ci.yml"
RUBY_ARGS=()
usage() {
cat >&2 <<'EOF'
usage: verify_ci_workflow.sh [--require-tracked] [--require-clean] [--require-upload-ready] [workflow.yml]
Validates the Open Shorebird GitHub Actions workflow contract. The upload-ready
mode additionally requires every required CI support file to be tracked in its
own git checkout and every required owning checkout to be clean.
EOF
}
while [[ "$#" -gt 0 ]]; do
case "$1" in
--require-tracked|--require-clean|--require-upload-ready)
RUBY_ARGS+=("$1")
shift
;;
-h|--help)
usage
exit 0
;;
-*)
echo "unknown argument: $1" >&2
usage
exit 64
;;
*)
WORKFLOW="$1"
shift
if [[ "$#" -gt 0 ]]; then
echo "unexpected extra argument: $1" >&2
usage
exit 64
fi
;;
esac
done
if [[ ! -f "$WORKFLOW" ]]; then
echo "missing workflow: $WORKFLOW" >&2
exit 66
fi
command -v ruby >/dev/null 2>&1 || {
echo "ruby is required to validate GitHub workflow YAML" >&2
exit 127
}
if [[ "${#RUBY_ARGS[@]}" -eq 0 ]]; then
ruby "$ROOT/scripts/verify_ci_workflow.rb" "$WORKFLOW"
else
ruby "$ROOT/scripts/verify_ci_workflow.rb" "${RUBY_ARGS[@]}" "$WORKFLOW"
fi
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ "$#" -eq 0 ]]; then
echo "usage: $0 <args.gn>..." >&2
exit 64
fi
require_gn_value() {
local args_file="$1"
local key="$2"
local value="$3"
local actual
if ! actual="$(read_gn_value "$args_file" "$key")"; then
echo "expected $key = $value in $args_file, but $key is missing" >&2
exit 70
fi
if [[ "$actual" != "$value" ]]; then
echo "expected $key = $value in $args_file, found $actual" >&2
exit 70
fi
}
read_gn_value() {
local file="$1"
local key="$2"
awk -v key="$key" '
$1 == key && $2 == "=" {
value = $0
sub("^[[:space:]]*" key "[[:space:]]*=[[:space:]]*", "", value)
sub("[[:space:]]*$", "", value)
found = 1
}
END {
if (!found) {
exit 1
}
print value
}
' "$file"
}
for args_file in "$@"; do
if [[ ! -f "$args_file" ]]; then
echo "missing args.gn: $args_file" >&2
exit 66
fi
if [[ "$(read_gn_value "$args_file" dart_dynamic_modules)" == "true" ]]; then
echo "DART_DYNAMIC_MODULES must not be enabled: $args_file" >&2
exit 70
fi
require_gn_value "$args_file" dart_dynamic_modules false
require_gn_value "$args_file" dart_enable_aot_patching true
require_gn_value "$args_file" dart_enable_shorebird_interpreter true
echo "Verified $args_file: patched Dart SDK flags are enabled without DDM"
done
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
usage() {
cat >&2 <<'EOF'
usage: verify_downloaded_release_artifacts.sh [--github-sha sha] downloaded-artifacts
Verifies a downloaded full SDK build artifact set. The release manifest and
artifact mirror archive must have valid checksum sidecars, the manifest must
cover every downloaded artifact, and the mirror archive must safely extract to a
valid open Shorebird artifact mirror.
EOF
}
DOWNLOAD_DIR=""
EXPECTED_GITHUB_SHA=""
while [[ "$#" -gt 0 ]]; do
case "$1" in
--github-sha)
if [[ "$#" -lt 2 || -z "${2:-}" ]]; then
echo "--github-sha value is required" >&2
usage
exit 64
fi
EXPECTED_GITHUB_SHA="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
-*)
echo "unknown argument: $1" >&2
usage
exit 64
;;
*)
if [[ -n "$DOWNLOAD_DIR" ]]; then
echo "unexpected extra argument: $1" >&2
usage
exit 64
fi
DOWNLOAD_DIR="$1"
shift
;;
esac
done
if [[ -z "$DOWNLOAD_DIR" ]]; then
usage
exit 64
fi
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/open-shorebird-downloaded-release.XXXXXX")"
trap 'rm -rf "$TMP_DIR"' EXIT
PYTHON_BIN=python3
if ! command -v "$PYTHON_BIN" >/dev/null 2>&1; then
PYTHON_BIN=python
fi
fail() {
echo "error: $*" >&2
exit 70
}
verify_sha256_sidecar() {
local artifact_path="$1"
local sidecar_path="$2"
[[ -f "$sidecar_path" ]] || fail "missing checksum sidecar: $sidecar_path"
"$PYTHON_BIN" - "$artifact_path" "$sidecar_path" <<'PY'
import hashlib
from pathlib import Path
import sys
artifact_path = Path(sys.argv[1])
sidecar_path = Path(sys.argv[2])
try:
text = sidecar_path.read_text(encoding="utf-8").strip()
except UnicodeDecodeError as error:
print(f"error: {sidecar_path}: invalid UTF-8: {error}", file=sys.stderr)
sys.exit(70)
parts = text.split()
if len(parts) != 2:
print(
f"error: {sidecar_path}: expected '<sha256> <filename>', got {text!r}",
file=sys.stderr,
)
sys.exit(70)
expected_digest, expected_filename = parts
if len(expected_digest) != 64 or any(
char not in "0123456789abcdef" for char in expected_digest
):
print(f"error: {sidecar_path}: invalid sha256 digest {expected_digest!r}", file=sys.stderr)
sys.exit(70)
if expected_filename != artifact_path.name:
print(
f"error: {sidecar_path}: filename mismatch "
f"{expected_filename!r} != {artifact_path.name!r}",
file=sys.stderr,
)
sys.exit(70)
digest = hashlib.sha256()
with artifact_path.open("rb") as artifact:
for chunk in iter(lambda: artifact.read(1024 * 1024), b""):
digest.update(chunk)
actual_digest = digest.hexdigest()
if expected_digest != actual_digest:
print(
f"error: {sidecar_path}: digest mismatch "
f"{expected_digest} != {actual_digest}",
file=sys.stderr,
)
sys.exit(70)
PY
}
[[ -d "$DOWNLOAD_DIR" ]] || fail "missing downloaded artifacts directory: $DOWNLOAD_DIR"
manifest_paths=()
while IFS= read -r path; do
manifest_paths+=("$path")
done < <(find "$DOWNLOAD_DIR" -type f -name open-shorebird-release-manifest.json | sort)
if [[ "${#manifest_paths[@]}" -ne 1 ]]; then
fail "expected exactly one open-shorebird-release-manifest.json, found ${#manifest_paths[@]}"
fi
manifest_path="${manifest_paths[0]}"
manifest_sidecar="$manifest_path.sha256"
verify_sha256_sidecar "$manifest_path" "$manifest_sidecar"
validate_manifest_args=()
if [[ -n "$EXPECTED_GITHUB_SHA" ]]; then
validate_manifest_args+=(--github-sha "$EXPECTED_GITHUB_SHA")
fi
"$PYTHON_BIN" "$ROOT/scripts/validate_release_manifest.py" \
"${validate_manifest_args[@]}" \
"$DOWNLOAD_DIR" \
"$manifest_path"
mirror_archives=()
while IFS= read -r path; do
mirror_archives+=("$path")
done < <(find "$DOWNLOAD_DIR" -type f -name open-shorebird-artifact-mirror.tar.gz | sort)
if [[ "${#mirror_archives[@]}" -ne 1 ]]; then
fail "expected exactly one open-shorebird-artifact-mirror.tar.gz, found ${#mirror_archives[@]}"
fi
mirror_archive="${mirror_archives[0]}"
mirror_sidecar="$mirror_archive.sha256"
verify_sha256_sidecar "$mirror_archive" "$mirror_sidecar"
"$PYTHON_BIN" "$ROOT/scripts/safe_extract_tar.py" "$mirror_archive" "$TMP_DIR"
mirror_root="$TMP_DIR/open-shorebird-artifact-mirror"
[[ -d "$mirror_root" ]] || fail "mirror archive did not contain open-shorebird-artifact-mirror/"
"$PYTHON_BIN" "$ROOT/scripts/validate_artifact_mirror.py" "$mirror_root"
echo "downloaded release artifacts verified: $DOWNLOAD_DIR"
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ "$#" -eq 0 ]]; then
echo "usage: $0 <args.gn>... | <args.gn> key=value ..." >&2
exit 64
fi
verify_no_ddm() {
local args_file="$1"
if [[ ! -f "$args_file" ]]; then
echo "missing args.gn: $args_file" >&2
exit 66
fi
local actual
if ! actual="$(read_gn_value "$args_file" dart_dynamic_modules)"; then
return 0
fi
if [[ "$actual" == "true" ]]; then
echo "DART_DYNAMIC_MODULES must not be enabled: $args_file" >&2
exit 70
fi
}
read_gn_value() {
local file="$1"
local key="$2"
awk -v key="$key" '
$1 == key && $2 == "=" {
value = $0
sub("^[[:space:]]*" key "[[:space:]]*=[[:space:]]*", "", value)
sub("[[:space:]]*$", "", value)
found = 1
}
END {
if (!found) {
exit 1
}
print value
}
' "$file"
}
has_expectations=0
for arg in "$@"; do
if [[ "$arg" == *=* ]]; then
has_expectations=1
break
fi
done
if [[ "$has_expectations" == "0" ]]; then
for args_file in "$@"; do
verify_no_ddm "$args_file"
echo "Verified $args_file: dart_dynamic_modules is not true"
done
exit 0
fi
args_file="$1"
shift
verify_no_ddm "$args_file"
for expectation in "$@"; do
if [[ "$expectation" != *=* ]]; then
echo "invalid expectation: $expectation; expected key=value" >&2
exit 64
fi
key="${expectation%%=*}"
value="${expectation#*=}"
if ! actual="$(read_gn_value "$args_file" "$key")"; then
echo "expected $key = $value in $args_file, but $key is missing" >&2
exit 70
fi
if [[ "$actual" != "$value" ]]; then
echo "expected $key = $value in $args_file, found $actual" >&2
exit 70
fi
done
echo "Verified $args_file: dart_dynamic_modules is not true and expected flags are present"
+209
View File
@@ -0,0 +1,209 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat >&2 <<'EOF'
usage: verify_hosted_full_sdk_build.sh --repo owner/name [options]
Dispatches the Open Shorebird full SDK build on GitHub Actions, waits for the
workflow run to finish, downloads all artifacts, and verifies the release
manifest plus assembled artifact mirror.
Options:
--repo owner/name GitHub repository to run against.
--ref branch-or-sha Ref to dispatch. Defaults to current branch.
--workflow file Workflow file. Defaults to open-shorebird-ci.yml.
--download-dir path Artifact download directory. Defaults to hosted-full-sdk-artifacts.
--timeout-minutes minutes Maximum wait time. Defaults to 720.
--poll-seconds seconds Poll interval. Defaults to 30.
--linux-heavy-runner label Override linux_heavy_runner.
--macos-heavy-runner label Override macos_heavy_runner.
--sdk-min-free-disk-gb value Override sdk_min_free_disk_gb.
--engine-min-free-disk-gb value Override engine_min_free_disk_gb.
--base-flutter-engine-revision v Override base_flutter_engine_revision.
--skip-gclient-sync Dispatch with run_gclient_sync=false.
EOF
}
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
REPO=""
REF=""
WORKFLOW="open-shorebird-ci.yml"
DOWNLOAD_DIR="hosted-full-sdk-artifacts"
TIMEOUT_MINUTES=720
POLL_SECONDS=30
LINUX_HEAVY_RUNNER=""
MACOS_HEAVY_RUNNER=""
SDK_MIN_FREE_DISK_GB=""
ENGINE_MIN_FREE_DISK_GB=""
BASE_FLUTTER_ENGINE_REVISION=""
RUN_GCLIENT_SYNC=true
while [[ "$#" -gt 0 ]]; do
case "$1" in
--repo)
REPO="${2:-}"
shift 2
;;
--ref)
REF="${2:-}"
shift 2
;;
--workflow)
WORKFLOW="${2:-}"
shift 2
;;
--download-dir)
DOWNLOAD_DIR="${2:-}"
shift 2
;;
--timeout-minutes)
TIMEOUT_MINUTES="${2:-}"
shift 2
;;
--poll-seconds)
POLL_SECONDS="${2:-}"
shift 2
;;
--linux-heavy-runner)
LINUX_HEAVY_RUNNER="${2:-}"
shift 2
;;
--macos-heavy-runner)
MACOS_HEAVY_RUNNER="${2:-}"
shift 2
;;
--sdk-min-free-disk-gb)
SDK_MIN_FREE_DISK_GB="${2:-}"
shift 2
;;
--engine-min-free-disk-gb)
ENGINE_MIN_FREE_DISK_GB="${2:-}"
shift 2
;;
--base-flutter-engine-revision)
BASE_FLUTTER_ENGINE_REVISION="${2:-}"
shift 2
;;
--skip-gclient-sync)
RUN_GCLIENT_SYNC=false
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "unknown argument: $1" >&2
usage
exit 64
;;
esac
done
if [[ -z "$REPO" ]]; then
echo "--repo owner/name is required" >&2
usage
exit 64
fi
if ! command -v gh >/dev/null 2>&1; then
echo "GitHub CLI 'gh' is required" >&2
exit 69
fi
if [[ -z "$REF" ]]; then
REF="$(git -C "$ROOT" branch --show-current 2>/dev/null || true)"
fi
if [[ -z "$REF" ]]; then
echo "--ref is required when the current checkout is detached" >&2
exit 64
fi
run_fields=(
-f full_sdk_build=true
-f run_gclient_sync="$RUN_GCLIENT_SYNC"
-f run_runtime_smokes=false
)
[[ -z "$LINUX_HEAVY_RUNNER" ]] || run_fields+=(-f linux_heavy_runner="$LINUX_HEAVY_RUNNER")
[[ -z "$MACOS_HEAVY_RUNNER" ]] || run_fields+=(-f macos_heavy_runner="$MACOS_HEAVY_RUNNER")
[[ -z "$SDK_MIN_FREE_DISK_GB" ]] || run_fields+=(-f sdk_min_free_disk_gb="$SDK_MIN_FREE_DISK_GB")
[[ -z "$ENGINE_MIN_FREE_DISK_GB" ]] || run_fields+=(-f engine_min_free_disk_gb="$ENGINE_MIN_FREE_DISK_GB")
[[ -z "$BASE_FLUTTER_ENGINE_REVISION" ]] || run_fields+=(-f base_flutter_engine_revision="$BASE_FLUTTER_ENGINE_REVISION")
echo "Dispatching $WORKFLOW on $REPO@$REF with full_sdk_build=true"
start_epoch="$(date +%s)"
start_iso="$(date -u -r "$start_epoch" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -d "@$start_epoch" +"%Y-%m-%dT%H:%M:%SZ")"
gh workflow run "$WORKFLOW" \
--repo "$REPO" \
--ref "$REF" \
"${run_fields[@]}"
run_id=""
for _ in {1..40}; do
run_list_args=(
--repo "$REPO"
--workflow "$WORKFLOW"
--event workflow_dispatch
--json databaseId,createdAt
--limit 20
)
if [[ "$REF" =~ ^[0-9a-fA-F]{40}$ ]]; then
run_list_args+=(--commit "$REF")
else
run_list_args+=(--branch "$REF")
fi
run_id="$(
gh run list \
"${run_list_args[@]}" \
--jq ".[] | select(.createdAt >= \"$start_iso\") | .databaseId" \
|
head -n 1
)"
[[ -z "$run_id" ]] || break
sleep 3
done
if [[ -z "$run_id" ]]; then
echo "unable to find dispatched workflow run for $WORKFLOW on $REF" >&2
exit 70
fi
echo "Waiting for hosted full SDK run: $run_id"
deadline=$((start_epoch + TIMEOUT_MINUTES * 60))
run_head_sha=""
while true; do
IFS=$'\t' read -r status conclusion url run_head_sha < <(
gh run view "$run_id" \
--repo "$REPO" \
--json status,conclusion,url,headSha \
--jq '[.status, (.conclusion // ""), .url, (.headSha // "")] | @tsv'
)
echo "run $run_id status=$status conclusion=${conclusion:-null} url=$url"
if [[ "$status" == "completed" ]]; then
if [[ "$conclusion" != "success" ]]; then
echo "hosted full SDK build failed: conclusion=$conclusion" >&2
exit 70
fi
break
fi
if [[ "$(date +%s)" -ge "$deadline" ]]; then
echo "timed out waiting for hosted full SDK build after $TIMEOUT_MINUTES minutes" >&2
exit 70
fi
sleep "$POLL_SECONDS"
done
rm -rf "$DOWNLOAD_DIR"
mkdir -p "$DOWNLOAD_DIR"
gh run download "$run_id" --repo "$REPO" --dir "$DOWNLOAD_DIR"
if [[ -z "$run_head_sha" ]]; then
echo "unable to read headSha for workflow run $run_id" >&2
exit 70
fi
"$ROOT/scripts/verify_downloaded_release_artifacts.sh" \
--github-sha "$run_head_sha" \
"$DOWNLOAD_DIR"
echo "hosted full SDK build verified: $run_id"
+125 -26
View File
@@ -14,6 +14,7 @@ CHECKED_IPA=""
CHECKED_PATCH_ARTIFACT=""
ENTITLEMENTS_CHECKED=0
APP_STORE_STRICT_CHECKED=0
NO_BUNDLED_KEY_CHECKED=0
CLEANUP_DIR=""
cleanup() {
@@ -35,14 +36,14 @@ read_gn_value() {
$1 == key && $2 == "=" {
value = $0
sub("^[[:space:]]*" key "[[:space:]]*=[[:space:]]*", "", value)
print value
sub("[[:space:]]*$", "", value)
found = 1
exit
}
END {
if (!found) {
exit 1
}
print value
}
' "$file"
}
@@ -66,6 +67,7 @@ verify_ios_engine_args() {
require_gn_value "$args_file" target_os '"ios"'
require_gn_value "$args_file" dart_dynamic_modules false
require_gn_value "$args_file" dart_enable_aot_patching true
require_gn_value "$args_file" dart_enable_shorebird_interpreter true
require_gn_value "$args_file" shorebird_use_interpreter true
require_gn_value "$args_file" shorebird_enable_aot_patching false
@@ -77,6 +79,7 @@ verify_host_engine_args() {
require_gn_value "$args_file" target_os '"mac"'
require_gn_value "$args_file" dart_dynamic_modules false
require_gn_value "$args_file" dart_enable_aot_patching true
require_gn_value "$args_file" dart_enable_shorebird_interpreter true
require_gn_value "$args_file" shorebird_use_interpreter true
}
@@ -143,6 +146,24 @@ verify_entitlements() {
fi
}
verify_no_bundled_patch_key() {
local app_bundle="$1"
[[ "$APP_STORE_STRICT" == "1" ]] || return 0
local shorebird_yaml
shorebird_yaml="$(
find "$app_bundle" -name "shorebird.yaml" -type f -print -quit
)"
if [[ -z "$shorebird_yaml" ]]; then
return 0
fi
if grep -Eq '^[[:space:]]*aot_patch_key_hex[[:space:]]*:' "$shorebird_yaml"; then
fail "APP_STORE_STRICT=1 rejects bundled aot_patch_key_hex in $shorebird_yaml"
fi
NO_BUNDLED_KEY_CHECKED=1
}
verify_app_bundle() {
if [[ -n "$IOS_APP_BUNDLE" && -n "$IOS_IPA" ]]; then
fail "set only one of IOS_APP_BUNDLE or IOS_IPA"
@@ -165,6 +186,7 @@ verify_app_bundle() {
fi
verify_entitlements "$IOS_APP_BUNDLE"
verify_no_bundled_patch_key "$IOS_APP_BUNDLE"
}
file_magic_hex() {
@@ -186,31 +208,105 @@ verify_patch_artifact() {
;;
esac
local compact_json
compact_json="$(LC_ALL=C tr -d '[:space:]' < "$IOS_PATCH_ARTIFACT")"
local python_bin
python_bin=python3
if ! command -v "$python_bin" >/dev/null 2>&1; then
python_bin=python
fi
command -v "$python_bin" >/dev/null 2>&1 ||
fail "python3 or python is required to inspect IOS_PATCH_ARTIFACT"
if [[ "$compact_json" != \{* ]]; then
fail "iOS patch artifact must be the encrypted open JSON wrapper, not a raw native/code payload"
fi
if ! grep -Fq '"format":"open-aot-vmcode-encrypted-v1"' <<<"$compact_json"; then
fail "iOS patch artifact is not an open encrypted VM code artifact"
fi
if grep -Fq '"runtime_mode":"dart-dynamic-modules"' <<<"$compact_json" ||
grep -Fq '"runtime_mode":"dynamic-modules"' <<<"$compact_json"; then
fail "iOS patch artifact uses DART_DYNAMIC_MODULES runtime mode"
fi
if ! grep -Fq '"runtime_mode":"dart-bytecode-interpreter"' <<<"$compact_json"; then
fail "iOS patch artifact must declare runtime_mode dart-bytecode-interpreter"
fi
if ! grep -Fq '"target_os":"ios"' <<<"$compact_json"; then
fail "iOS patch artifact must target iOS"
fi
if ! grep -Fq '"target_arch":"arm64"' <<<"$compact_json"; then
fail "iOS patch artifact must target arm64"
fi
if ! grep -Fq '"payload_kind":"full-snapshot"' <<<"$compact_json"; then
fail "current iOS interpreter mapper requires payload_kind full-snapshot"
fi
"$python_bin" - "$IOS_PATCH_ARTIFACT" <<'PY'
import base64
import binascii
import json
import re
import sys
path = sys.argv[1]
try:
with open(path, encoding="utf-8") as file:
artifact = json.load(file)
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
raise SystemExit(
"iOS patch artifact must be the encrypted open JSON wrapper, "
f"not a raw native/code payload: {error}"
)
if not isinstance(artifact, dict):
raise SystemExit("iOS patch artifact JSON must be an object")
metadata = artifact.get("metadata")
if not isinstance(metadata, dict):
raise SystemExit("iOS patch artifact must contain metadata object")
def require(mapping, key, expected, scope):
actual = mapping.get(key)
if actual != expected:
raise SystemExit(
f"iOS patch artifact {scope}.{key} is {actual!r}; "
f"expected {expected!r}"
)
require(artifact, "format", "open-aot-vmcode-encrypted-v1", "artifact")
runtime_mode = metadata.get("runtime_mode")
if runtime_mode in {"dart-dynamic-modules", "dynamic-modules"}:
raise SystemExit("iOS patch artifact uses DART_DYNAMIC_MODULES runtime mode")
require(metadata, "runtime_mode", "dart-bytecode-interpreter", "metadata")
require(metadata, "target_os", "ios", "metadata")
require(metadata, "target_arch", "arm64", "metadata")
require(artifact, "payload_kind", "full-snapshot", "artifact")
encryption = artifact.get("encryption")
if not isinstance(encryption, dict):
raise SystemExit("iOS patch artifact must contain encryption object")
require(encryption, "algorithm", "AES-256-GCM", "encryption")
def require_base64(mapping, key, scope):
value = mapping.get(key)
if not isinstance(value, str) or not value:
raise SystemExit(f"iOS patch artifact {scope}.{key} must be non-empty")
try:
decoded = base64.b64decode(value, validate=True)
except (binascii.Error, ValueError) as error:
raise SystemExit(
f"iOS patch artifact {scope}.{key} is not valid base64: {error}"
)
if not decoded:
raise SystemExit(f"iOS patch artifact {scope}.{key} decodes to empty bytes")
return decoded
require_base64(artifact, "encrypted_payload_base64", "artifact")
require_base64(encryption, "nonce_base64", "encryption")
require_base64(encryption, "tag_base64", "encryption")
key_id = encryption.get("key_id")
if not isinstance(key_id, str) or not key_id:
raise SystemExit("iOS patch artifact encryption.key_id must be non-empty")
hex_pattern = re.compile(r"^[0-9a-f]{64}$")
for scope, mapping, key in (
("artifact", artifact, "payload_sha256"),
("encryption", encryption, "aad_sha256"),
):
value = mapping.get(key)
if not isinstance(value, str) or not hex_pattern.fullmatch(value):
raise SystemExit(
f"iOS patch artifact {scope}.{key} must be a lowercase SHA-256 hex digest"
)
reconstructed_size = artifact.get("reconstructed_size")
if reconstructed_size is not None:
if not isinstance(reconstructed_size, int) or reconstructed_size <= 0:
raise SystemExit(
"iOS patch artifact reconstructed_size must be a positive integer"
)
PY
CHECKED_PATCH_ARTIFACT="$IOS_PATCH_ARTIFACT"
}
@@ -240,6 +336,9 @@ fi
if [[ "$APP_STORE_STRICT_CHECKED" == "1" ]]; then
echo " App Store strict: get-task-allow is not true"
fi
if [[ "$NO_BUNDLED_KEY_CHECKED" == "1" ]]; then
echo " key material: no bundled aot_patch_key_hex"
fi
if [[ -n "$CHECKED_PATCH_ARTIFACT" ]]; then
echo " patch artifact: encrypted interpreter full-snapshot for ios/arm64"
fi
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/ios-route-validator.XXXXXX")"
trap 'rm -rf "$TMP_DIR"' EXIT
IOS_ENGINE_DIR="$TMP_DIR/ios_release"
HOST_ENGINE_DIR="$TMP_DIR/host_release_arm64"
mkdir -p "$IOS_ENGINE_DIR" "$HOST_ENGINE_DIR"
cat > "$IOS_ENGINE_DIR/args.gn" <<'EOF'
target_os = "android"
target_os = "ios"
dart_dynamic_modules = false
dart_enable_aot_patching = false
dart_enable_aot_patching = true
dart_enable_shorebird_interpreter = false
dart_enable_shorebird_interpreter = true
shorebird_use_interpreter = false
shorebird_use_interpreter = true
shorebird_enable_aot_patching = true
shorebird_enable_aot_patching = false
EOF
cat > "$HOST_ENGINE_DIR/args.gn" <<'EOF'
target_os = "linux"
target_os = "mac"
dart_dynamic_modules = false
dart_enable_aot_patching = false
dart_enable_aot_patching = true
dart_enable_shorebird_interpreter = false
dart_enable_shorebird_interpreter = true
shorebird_use_interpreter = false
shorebird_use_interpreter = true
EOF
write_artifact() {
local path="$1"
local runtime_mode="$2"
local target_os="${3:-ios}"
cat > "$path" <<EOF
{
"format": "open-aot-vmcode-encrypted-v1",
"metadata": {
"app_id": "app.test",
"app_build_id": "1",
"flavor_id": "pro",
"license_type": "pro",
"sdk_hash": "sdk",
"base_snapshot_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"patch_snapshot_hash": "1111111111111111111111111111111111111111111111111111111111111111",
"target_os": "$target_os",
"target_arch": "arm64",
"runtime_mode": "$runtime_mode"
},
"payload_kind": "full-snapshot",
"reconstructed_size": 4,
"payload_sha256": "2222222222222222222222222222222222222222222222222222222222222222",
"encrypted_payload_base64": "AQIDBA==",
"encryption": {
"algorithm": "AES-256-GCM",
"key_id": "test-key",
"nonce_base64": "AQIDBAUGBwgJCgsM",
"tag_base64": "AQIDBAUGBwgJCgsMDQ4PEA==",
"aad_sha256": "3333333333333333333333333333333333333333333333333333333333333333"
}
}
EOF
}
valid_artifact="$TMP_DIR/valid.vmcode"
write_artifact "$valid_artifact" "dart-bytecode-interpreter"
IOS_ENGINE_DIR="$IOS_ENGINE_DIR" \
HOST_ENGINE_DIR="$HOST_ENGINE_DIR" \
IOS_PATCH_ARTIFACT="$valid_artifact" \
"$ROOT/scripts/verify_ios_interpreter_route.sh" >/dev/null
bad_runtime="$TMP_DIR/bad-runtime.vmcode"
write_artifact "$bad_runtime" "dart-dynamic-modules"
if IOS_ENGINE_DIR="$IOS_ENGINE_DIR" \
HOST_ENGINE_DIR="$HOST_ENGINE_DIR" \
IOS_PATCH_ARTIFACT="$bad_runtime" \
"$ROOT/scripts/verify_ios_interpreter_route.sh" >/dev/null 2>&1; then
echo "iOS route validator unexpectedly accepted DART_DYNAMIC_MODULES metadata" >&2
exit 70
fi
bad_target="$TMP_DIR/bad-target.vmcode"
write_artifact "$bad_target" "dart-bytecode-interpreter" "android"
if IOS_ENGINE_DIR="$IOS_ENGINE_DIR" \
HOST_ENGINE_DIR="$HOST_ENGINE_DIR" \
IOS_PATCH_ARTIFACT="$bad_target" \
"$ROOT/scripts/verify_ios_interpreter_route.sh" >/dev/null 2>&1; then
echo "iOS route validator unexpectedly accepted a non-iOS patch artifact" >&2
exit 70
fi
bad_json="$TMP_DIR/bad-json.vmcode"
printf '{"format":"open-aot-vmcode-encrypted-v1"\n' > "$bad_json"
if IOS_ENGINE_DIR="$IOS_ENGINE_DIR" \
HOST_ENGINE_DIR="$HOST_ENGINE_DIR" \
IOS_PATCH_ARTIFACT="$bad_json" \
"$ROOT/scripts/verify_ios_interpreter_route.sh" >/dev/null 2>&1; then
echo "iOS route validator unexpectedly accepted malformed JSON" >&2
exit 70
fi
bad_native="$TMP_DIR/bad-native.vmcode"
printf '\xcf\xfa\xed\xfe' > "$bad_native"
if IOS_ENGINE_DIR="$IOS_ENGINE_DIR" \
HOST_ENGINE_DIR="$HOST_ENGINE_DIR" \
IOS_PATCH_ARTIFACT="$bad_native" \
"$ROOT/scripts/verify_ios_interpreter_route.sh" >/dev/null 2>&1; then
echo "iOS route validator unexpectedly accepted a Mach-O patch artifact" >&2
exit 70
fi
echo "iOS interpreter route validator smoke test passed"
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
fail() {
echo "error: $*" >&2
exit 70
}
require_contains() {
local path="$1"
local needle="$2"
grep -Fq "$needle" "$path" || fail "$path is missing required text: $needle"
}
reject_contains() {
local path="$1"
local needle="$2"
if grep -Fq "$needle" "$path"; then
fail "$path contains forbidden text: $needle"
fi
}
check_forbidden_in_file() {
local path="$1"
local pattern
[[ -f "$path" ]] || fail "missing open-infrastructure check input: $path"
for pattern in "${FORBIDDEN_PATTERNS[@]}"; do
reject_contains "$path" "$pattern"
done
}
check_forbidden_in_tree() {
local tree="$1"
local extension="$2"
local path
[[ -d "$tree" ]] || fail "missing open-infrastructure check tree: $tree"
while IFS= read -r -d '' path; do
check_forbidden_in_file "$path"
done < <(find "$tree" -type f -name "*.$extension" -print0)
}
FORBIDDEN_PATTERNS=(
"https://download.shorebird.dev"
"download.shorebird.dev"
"api.shorebird.dev"
"auth.shorebird.dev"
"console.shorebird.dev"
"cdn.shorebird.cloud"
"git@github.com:shorebirdtech/dart-sdk.git"
"github.com/shorebirdtech/updater.git"
"github.com/shorebirdtech/flutter.git"
"shorebird-dart-sdk-prebuilt"
"shorebirdtech/_build_engine"
)
BUILD_SENSITIVE_FILES=(
"$ROOT/.gitmodules"
"$ROOT/flutter/DEPS"
"$ROOT/flutter/bin/internal/update_dart_sdk.ps1"
"$ROOT/flutter/bin/internal/update_dart_sdk.sh"
"$ROOT/flutter/dev/bots/post_process_docs.dart"
"$ROOT/flutter/dev/bots/unpublish_package.dart"
"$ROOT/flutter/dev/integration_tests/pure_android_host_apps/android_host_app_v2_embedding/settings.gradle"
"$ROOT/flutter/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/settings.gradle.kts"
"$ROOT/flutter/dev/tools/create_api_docs.dart"
"$ROOT/flutter/engine/src/flutter/build/zip_bundle.gni"
"$ROOT/flutter/engine/src/flutter/lib/web_ui/dev/steps/copy_artifacts_step.dart"
"$ROOT/flutter/packages/flutter_tools/gradle/aar_init_script.gradle"
"$ROOT/flutter/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt"
"$ROOT/flutter/packages/flutter_tools/lib/src/cache.dart"
"$ROOT/flutter/packages/flutter_tools/lib/src/http_host_validator.dart"
"$ROOT/flutter/packages/flutter_tools/pubspec.yaml"
"$ROOT/flutter/packages/shorebird_tests/test/shorebird_tests.dart"
"$ROOT/scripts/write_gclient.sh"
"$ROOT/shorebird/bin/shorebird.ps1"
"$ROOT/shorebird/third_party/flutter/bin/internal/shared.sh"
"$ROOT/updater/library/src/config.rs"
)
for path in "${BUILD_SENSITIVE_FILES[@]}"; do
check_forbidden_in_file "$path"
done
check_forbidden_in_tree "$ROOT/shorebird/packages/artifact_proxy/lib" dart
check_forbidden_in_tree "$ROOT/shorebird/packages/shorebird_cli/lib" dart
check_forbidden_in_tree "$ROOT/shorebird/packages/shorebird_code_push_client/lib" dart
require_contains "$ROOT/.gitmodules" "https://git.tonycloud.org/dart-lang/sdk.git"
require_contains "$ROOT/.gitmodules" "https://git.tonycloud.org/flutter/flutter.git"
require_contains "$ROOT/.gitmodules" "https://git.tonycloud.org/flutter/shorebird.git"
require_contains "$ROOT/.gitmodules" "https://git.tonycloud.org/flutter/shorebird-server.git"
require_contains "$ROOT/.gitmodules" "https://git.tonycloud.org/flutter/shorebird-updater.git"
require_contains "$ROOT/flutter/DEPS" '"dart_sdk_git": "https://git.tonycloud.org/dart-lang/sdk.git"'
require_contains "$ROOT/flutter/DEPS" '"updater_git": "https://git.tonycloud.org/flutter/shorebird-updater.git"'
require_contains "$ROOT/flutter/packages/flutter_tools/lib/src/cache.dart" \
"kOpenFlutterStorageUrl = 'http://localhost:8080/download.flutter.io'"
require_contains "$ROOT/flutter/bin/internal/update_dart_sdk.sh" \
"http://localhost:8080/download.flutter.io"
require_contains "$ROOT/flutter/bin/internal/update_dart_sdk.ps1" \
"http://localhost:8080/download.flutter.io"
require_contains "$ROOT/flutter/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt" \
'DEFAULT_MAVEN_HOST = "http://localhost:8080/download.flutter.io"'
require_contains "$ROOT/flutter/dev/tools/create_api_docs.dart" \
"Platform.environment['FLUTTER_STORAGE_BASE_URL']"
require_contains "$ROOT/flutter/packages/shorebird_tests/test/shorebird_tests.dart" \
"'FLUTTER_STORAGE_BASE_URL': 'http://localhost:8080/download.flutter.io'"
require_contains "$ROOT/shorebird/packages/shorebird_cli/lib/src/shorebird_env.dart" \
"defaultHostedUrl = 'http://localhost:8080'"
require_contains "$ROOT/shorebird/packages/shorebird_cli/lib/src/cache.dart" \
"defaultArtifactBaseUrl = 'http://localhost:8080/artifacts'"
require_contains "$ROOT/updater/library/src/config.rs" \
'const DEFAULT_BASE_URL: &str = "http://localhost:8080";'
echo "open infrastructure defaults check passed"
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
fail() {
echo "error: $*" >&2
exit 70
}
require_contains() {
local path="$1"
local needle="$2"
grep -Fq "$needle" "$path" || fail "$path is missing required text: $needle"
}
reject_contains() {
local path="$1"
local needle="$2"
if grep -Fq "$needle" "$path"; then
fail "$path contains forbidden text: $needle"
fi
}
shorebird_launcher="$ROOT/shorebird/bin/shorebird.ps1"
flutter_dart_updater="$ROOT/flutter/bin/internal/update_dart_sdk.ps1"
[[ -f "$shorebird_launcher" ]] || fail "missing PowerShell launcher: $shorebird_launcher"
[[ -f "$flutter_dart_updater" ]] || fail "missing Flutter Dart SDK updater: $flutter_dart_updater"
require_contains "$shorebird_launcher" \
'$defaultFlutterGitUrl = "https://git.tonycloud.org/flutter/flutter.git"'
require_contains "$shorebird_launcher" \
'$defaultFlutterStorageBaseUrl = "http://localhost:8080/download.flutter.io"'
require_contains "$shorebird_launcher" 'SHOREBIRD_FLUTTER_GIT_URL'
require_contains "$shorebird_launcher" 'SHOREBIRD_FLUTTER_STORAGE_BASE_URL'
require_contains "$shorebird_launcher" 'FLUTTER_STORAGE_BASE_URL'
require_contains "$flutter_dart_updater" '$Env:FLUTTER_STORAGE_BASE_URL'
require_contains "$flutter_dart_updater" \
'$dartSdkBaseUrl = "http://localhost:8080/download.flutter.io"'
for path in "$shorebird_launcher" "$flutter_dart_updater"; do
reject_contains "$path" 'download.shorebird.dev'
reject_contains "$path" 'api.shorebird.dev'
reject_contains "$path" 'auth.shorebird.dev'
reject_contains "$path" 'console.shorebird.dev'
reject_contains "$path" 'docs.shorebird.dev'
reject_contains "$path" 'github.com/shorebirdtech/flutter.git'
reject_contains "$path" 'github.com/shorebirdtech/shorebird'
reject_contains "$path" 'git@github.com:shorebirdtech'
done
if command -v pwsh >/dev/null 2>&1; then
SHOREBIRD_POWERSHELL_LAUNCHER="$shorebird_launcher" \
FLUTTER_DART_SDK_POWERSHELL_UPDATER="$flutter_dart_updater" \
pwsh -NoProfile -NonInteractive -Command '
$paths = @(
$env:SHOREBIRD_POWERSHELL_LAUNCHER,
$env:FLUTTER_DART_SDK_POWERSHELL_UPDATER
)
$failed = $false
foreach ($path in $paths) {
$tokens = $null
$errors = $null
[System.Management.Automation.Language.Parser]::ParseFile(
$path,
[ref]$tokens,
[ref]$errors
) | Out-Null
if ($errors.Count -gt 0) {
Write-Error "$path has PowerShell parse errors:"
foreach ($errorRecord in $errors) {
Write-Error " $($errorRecord.Message)"
}
$failed = $true
}
}
if ($failed) {
exit 70
}
'
else
echo "warning: pwsh not found; skipped PowerShell parse check" >&2
fi
echo "PowerShell open-default checks passed"
+238
View File
@@ -0,0 +1,238 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/open-shorebird-release-manifest.XXXXXX")"
trap 'rm -rf "$TMP_DIR"' EXIT
PYTHON_BIN=python3
if ! command -v "$PYTHON_BIN" >/dev/null 2>&1; then
PYTHON_BIN=python
fi
INPUT_DIR="$TMP_DIR/artifacts"
mkdir -p "$INPUT_DIR/cli-linux" "$INPUT_DIR/server-linux" "$INPUT_DIR/mirror-patch/artifacts/mirror"
printf 'cli archive\n' > "$INPUT_DIR/cli-linux/open-shorebird-cli-linux-x64.tar.gz"
printf 'server archive\n' > "$INPUT_DIR/server-linux/shorebird-server-linux-amd64.tar.gz"
printf 'patch archive\n' > "$INPUT_DIR/mirror-patch/artifacts/mirror/patch-linux-x64.zip"
"$ROOT/scripts/write_sha256.sh" "$INPUT_DIR/cli-linux/open-shorebird-cli-linux-x64.tar.gz"
"$ROOT/scripts/write_sha256.sh" "$INPUT_DIR/server-linux/shorebird-server-linux-amd64.tar.gz"
"$ROOT/scripts/write_sha256.sh" "$INPUT_DIR/mirror-patch/artifacts/mirror/patch-linux-x64.zip"
"$PYTHON_BIN" "$ROOT/scripts/write_release_manifest.py" \
"$INPUT_DIR" \
--github-sha test-sha \
--require 'cli-linux/*open-shorebird-cli-linux-x64.tar.gz' \
--require 'server-linux/*shorebird-server-linux-amd64.tar.gz' \
--require 'mirror-patch/*patch-linux-x64.zip' \
--output "$TMP_DIR/release-manifest.json"
"$PYTHON_BIN" "$ROOT/scripts/validate_release_manifest.py" \
--github-sha test-sha \
"$INPUT_DIR" \
"$TMP_DIR/release-manifest.json"
"$PYTHON_BIN" - "$TMP_DIR/release-manifest.json" <<'PY'
import json
import sys
manifest = json.load(open(sys.argv[1], encoding="utf-8"))
assert manifest["format_version"] == 1
assert manifest["github_sha"] == "test-sha"
assert manifest["artifact_count"] == 3
paths = {artifact["path"] for artifact in manifest["artifacts"]}
assert "cli-linux/open-shorebird-cli-linux-x64.tar.gz" in paths
assert "server-linux/shorebird-server-linux-amd64.tar.gz" in paths
assert "mirror-patch/artifacts/mirror/patch-linux-x64.zip" in paths
for artifact in manifest["artifacts"]:
assert artifact["artifact_group"] in {
"cli-linux",
"server-linux",
"mirror-patch",
}
assert artifact["filename"] == artifact["path"].split("/")[-1]
assert len(artifact["sha256"]) == 64
assert artifact["size"] > 0
assert artifact["sidecar"].endswith(".sha256")
PY
TAMPERED_MANIFEST="$TMP_DIR/tampered-release-manifest.json"
"$PYTHON_BIN" - "$TMP_DIR/release-manifest.json" "$TAMPERED_MANIFEST" <<'PY'
import json
import sys
source, output = sys.argv[1:]
manifest = json.load(open(source, encoding="utf-8"))
manifest["artifacts"][0]["sha256"] = "0" * 64
json.dump(manifest, open(output, "w", encoding="utf-8"))
PY
if "$PYTHON_BIN" "$ROOT/scripts/validate_release_manifest.py" \
"$INPUT_DIR" \
"$TAMPERED_MANIFEST" >/dev/null 2>&1; then
echo "validate_release_manifest.py unexpectedly accepted a tampered digest" >&2
exit 70
fi
if "$PYTHON_BIN" "$ROOT/scripts/validate_release_manifest.py" \
--github-sha wrong-sha \
"$INPUT_DIR" \
"$TMP_DIR/release-manifest.json" >"$TMP_DIR/wrong-sha.log" 2>&1; then
echo "validate_release_manifest.py unexpectedly accepted the wrong github_sha" >&2
exit 70
fi
grep -q "github_sha is" "$TMP_DIR/wrong-sha.log"
UNLISTED_ARTIFACTS="$TMP_DIR/unlisted-artifacts"
cp -R "$INPUT_DIR" "$UNLISTED_ARTIFACTS"
printf 'extra artifact\n' > "$UNLISTED_ARTIFACTS/extra.tar.gz"
"$ROOT/scripts/write_sha256.sh" "$UNLISTED_ARTIFACTS/extra.tar.gz"
if "$PYTHON_BIN" "$ROOT/scripts/validate_release_manifest.py" \
"$UNLISTED_ARTIFACTS" \
"$TMP_DIR/release-manifest.json" >/dev/null 2>&1; then
echo "validate_release_manifest.py unexpectedly accepted an unlisted artifact" >&2
exit 70
fi
UNSAFE_MANIFEST="$TMP_DIR/unsafe-release-manifest.json"
"$PYTHON_BIN" - "$TMP_DIR/release-manifest.json" "$UNSAFE_MANIFEST" <<'PY'
import json
import sys
source, output = sys.argv[1:]
manifest = json.load(open(source, encoding="utf-8"))
manifest["artifacts"][0]["path"] = r"cli-linux\open-shorebird-cli-linux-x64.tar.gz"
manifest["artifacts"][0]["sidecar"] = (
r"cli-linux\open-shorebird-cli-linux-x64.tar.gz.sha256"
)
json.dump(manifest, open(output, "w", encoding="utf-8"))
PY
if "$PYTHON_BIN" "$ROOT/scripts/validate_release_manifest.py" \
"$INPUT_DIR" \
"$UNSAFE_MANIFEST" >/dev/null 2>&1; then
echo "validate_release_manifest.py unexpectedly accepted an unsafe artifact path" >&2
exit 70
fi
BAD_PROVENANCE_MANIFEST="$TMP_DIR/bad-provenance-release-manifest.json"
"$PYTHON_BIN" - "$TMP_DIR/release-manifest.json" "$BAD_PROVENANCE_MANIFEST" <<'PY'
import json
import sys
source, output = sys.argv[1:]
manifest = json.load(open(source, encoding="utf-8"))
manifest["artifacts"][0]["artifact_group"] = "wrong-group"
manifest["artifacts"][1]["filename"] = "wrong-name.tar.gz"
json.dump(manifest, open(output, "w", encoding="utf-8"))
PY
if "$PYTHON_BIN" "$ROOT/scripts/validate_release_manifest.py" \
"$INPUT_DIR" \
"$BAD_PROVENANCE_MANIFEST" >/dev/null 2>&1; then
echo "validate_release_manifest.py unexpectedly accepted bad provenance fields" >&2
exit 70
fi
SYMLINK_LISTED_ARTIFACT="$TMP_DIR/symlink-listed-artifact"
cp -R "$INPUT_DIR" "$SYMLINK_LISTED_ARTIFACT"
rm "$SYMLINK_LISTED_ARTIFACT/cli-linux/open-shorebird-cli-linux-x64.tar.gz"
ln -s "$INPUT_DIR/cli-linux/open-shorebird-cli-linux-x64.tar.gz" \
"$SYMLINK_LISTED_ARTIFACT/cli-linux/open-shorebird-cli-linux-x64.tar.gz"
if "$PYTHON_BIN" "$ROOT/scripts/validate_release_manifest.py" \
"$SYMLINK_LISTED_ARTIFACT" \
"$TMP_DIR/release-manifest.json" >/dev/null 2>&1; then
echo "validate_release_manifest.py unexpectedly accepted a symlink artifact" >&2
exit 70
fi
EMPTY_LISTED_ARTIFACT="$TMP_DIR/empty-listed-artifact"
cp -R "$INPUT_DIR" "$EMPTY_LISTED_ARTIFACT"
printf '' > "$EMPTY_LISTED_ARTIFACT/cli-linux/open-shorebird-cli-linux-x64.tar.gz"
"$ROOT/scripts/write_sha256.sh" \
"$EMPTY_LISTED_ARTIFACT/cli-linux/open-shorebird-cli-linux-x64.tar.gz"
EMPTY_LISTED_MANIFEST="$TMP_DIR/empty-listed-release-manifest.json"
"$PYTHON_BIN" - "$TMP_DIR/release-manifest.json" "$EMPTY_LISTED_MANIFEST" <<'PY'
import hashlib
import json
import sys
source, output = sys.argv[1:]
manifest = json.load(open(source, encoding="utf-8"))
empty_digest = hashlib.sha256(b"").hexdigest()
for artifact in manifest["artifacts"]:
if artifact["path"] == "cli-linux/open-shorebird-cli-linux-x64.tar.gz":
artifact["sha256"] = empty_digest
artifact["size"] = 0
json.dump(manifest, open(output, "w", encoding="utf-8"))
PY
if "$PYTHON_BIN" "$ROOT/scripts/validate_release_manifest.py" \
"$EMPTY_LISTED_ARTIFACT" \
"$EMPTY_LISTED_MANIFEST" >"$TMP_DIR/empty-listed.log" 2>&1; then
echo "validate_release_manifest.py unexpectedly accepted an empty artifact" >&2
exit 70
fi
grep -q "empty artifacts are not allowed" "$TMP_DIR/empty-listed.log"
SYMLINK_INPUT="$TMP_DIR/symlink-input"
mkdir -p "$SYMLINK_INPUT"
printf 'symlink target\n' > "$SYMLINK_INPUT/target.tar.gz"
"$ROOT/scripts/write_sha256.sh" "$SYMLINK_INPUT/target.tar.gz"
ln -s target.tar.gz "$SYMLINK_INPUT/link.tar.gz"
if "$PYTHON_BIN" "$ROOT/scripts/write_release_manifest.py" \
"$SYMLINK_INPUT" \
--output "$TMP_DIR/symlink-input.json" >/dev/null 2>&1; then
echo "write_release_manifest.py unexpectedly accepted a symlink artifact" >&2
exit 70
fi
MISSING_SIDECAR="$TMP_DIR/missing-sidecar"
mkdir -p "$MISSING_SIDECAR"
printf 'missing sidecar\n' > "$MISSING_SIDECAR/artifact.tar.gz"
if "$PYTHON_BIN" "$ROOT/scripts/write_release_manifest.py" \
"$MISSING_SIDECAR" \
--output "$TMP_DIR/missing.json" >/dev/null 2>&1; then
echo "write_release_manifest.py unexpectedly accepted a missing sidecar" >&2
exit 70
fi
BAD_SIDECAR="$TMP_DIR/bad-sidecar"
mkdir -p "$BAD_SIDECAR"
printf 'bad sidecar\n' > "$BAD_SIDECAR/artifact.tar.gz"
printf '%064d artifact.tar.gz\n' 0 > "$BAD_SIDECAR/artifact.tar.gz.sha256"
if "$PYTHON_BIN" "$ROOT/scripts/write_release_manifest.py" \
"$BAD_SIDECAR" \
--output "$TMP_DIR/bad.json" >/dev/null 2>&1; then
echo "write_release_manifest.py unexpectedly accepted a bad sidecar" >&2
exit 70
fi
EMPTY_ARTIFACT="$TMP_DIR/empty-artifact"
mkdir -p "$EMPTY_ARTIFACT"
printf '' > "$EMPTY_ARTIFACT/artifact.tar.gz"
"$ROOT/scripts/write_sha256.sh" "$EMPTY_ARTIFACT/artifact.tar.gz"
if "$PYTHON_BIN" "$ROOT/scripts/write_release_manifest.py" \
"$EMPTY_ARTIFACT" \
--output "$TMP_DIR/empty.json" >"$TMP_DIR/empty-artifact.log" 2>&1; then
echo "write_release_manifest.py unexpectedly accepted an empty artifact" >&2
exit 70
fi
grep -q "empty artifacts are not allowed" "$TMP_DIR/empty-artifact.log"
ORPHAN_SIDECAR="$TMP_DIR/orphan-sidecar"
mkdir -p "$ORPHAN_SIDECAR"
printf '%064d missing.tar.gz\n' 0 > "$ORPHAN_SIDECAR/missing.tar.gz.sha256"
if "$PYTHON_BIN" "$ROOT/scripts/write_release_manifest.py" \
"$ORPHAN_SIDECAR" \
--output "$TMP_DIR/orphan.json" >/dev/null 2>&1; then
echo "write_release_manifest.py unexpectedly accepted an orphan sidecar" >&2
exit 70
fi
if "$PYTHON_BIN" "$ROOT/scripts/write_release_manifest.py" \
"$INPUT_DIR" \
--require 'missing-artifact/*.tar.gz' \
--output "$TMP_DIR/missing-required.json" >/dev/null 2>&1; then
echo "write_release_manifest.py unexpectedly accepted a missing required artifact" >&2
exit 70
fi
echo "write_release_manifest.py smoke test passed"
+125
View File
@@ -0,0 +1,125 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/open-source-sync.XXXXXX")"
trap 'rm -rf "$TMP_DIR"' EXIT
init_git_checkout() {
local path="$1"
mkdir -p "$path"
git -C "$path" init -q
}
real_path() {
python3 -c 'import os,sys; print(os.path.realpath(sys.argv[1]))' "$1"
}
make_workspace() {
local workspace="$1"
mkdir -p "$workspace/scripts"
cp "$ROOT/scripts/sync_open_sources.sh" "$workspace/scripts/sync_open_sources.sh"
chmod +x "$workspace/scripts/sync_open_sources.sh"
init_git_checkout "$workspace/dart-sdk-new"
mkdir -p "$workspace/dart-sdk-new/runtime/vm"
: > "$workspace/dart-sdk-new/runtime/vm/dart_api_impl.h"
init_git_checkout "$workspace/updater"
mkdir -p "$workspace/updater/library/include"
: > "$workspace/updater/library/include/updater_engine.h"
mkdir -p "$workspace/flutter/engine/src/flutter/third_party"
}
run_sync() {
local workspace="$1"
DART_SRC="$workspace/dart-sdk-new" \
UPDATER_SRC="$workspace/updater" \
"$workspace/scripts/sync_open_sources.sh"
}
assert_links_to_workspace_sources() {
local workspace="$1"
local dart_target="$workspace/flutter/engine/src/flutter/third_party/dart"
local updater_target="$workspace/flutter/engine/src/flutter/third_party/updater"
test -L "$dart_target"
test -L "$updater_target"
[[ "$(real_path "$dart_target")" == "$(real_path "$workspace/dart-sdk-new")" ]]
[[ "$(real_path "$updater_target")" == "$(real_path "$workspace/updater")" ]]
test -f "$dart_target/runtime/vm/dart_api_impl.h"
test -f "$updater_target/library/include/updater_engine.h"
}
clean_checkout_workspace="$TMP_DIR/clean-checkouts"
make_workspace "$clean_checkout_workspace"
init_git_checkout "$clean_checkout_workspace/flutter/engine/src/flutter/third_party/dart"
init_git_checkout "$clean_checkout_workspace/flutter/engine/src/flutter/third_party/updater"
run_sync "$clean_checkout_workspace"
assert_links_to_workspace_sources "$clean_checkout_workspace"
run_sync "$clean_checkout_workspace"
assert_links_to_workspace_sources "$clean_checkout_workspace"
stale_link_workspace="$TMP_DIR/stale-link"
make_workspace "$stale_link_workspace"
mkdir -p "$stale_link_workspace/other-dart"
ln -s "../../../../../other-dart" \
"$stale_link_workspace/flutter/engine/src/flutter/third_party/dart"
if run_sync "$stale_link_workspace" >"$TMP_DIR/stale-link.log" 2>&1; then
echo "expected stale Dart symlink to fail" >&2
exit 1
fi
grep -q "Dart SDK target symlink points at" "$TMP_DIR/stale-link.log"
dirty_checkout_workspace="$TMP_DIR/dirty-checkout"
make_workspace "$dirty_checkout_workspace"
dirty_dart="$dirty_checkout_workspace/flutter/engine/src/flutter/third_party/dart"
init_git_checkout "$dirty_dart"
: > "$dirty_dart/untracked.txt"
if run_sync "$dirty_checkout_workspace" >"$TMP_DIR/dirty-checkout.log" 2>&1; then
echo "expected dirty Dart checkout to fail" >&2
exit 1
fi
grep -q "Dart SDK target is a dirty git checkout" "$TMP_DIR/dirty-checkout.log"
test -d "$dirty_dart/.git"
test -f "$dirty_dart/untracked.txt"
forbidden_dart_remote_workspace="$TMP_DIR/forbidden-dart-remote"
make_workspace "$forbidden_dart_remote_workspace"
git -C "$forbidden_dart_remote_workspace/dart-sdk-new" remote add origin \
https://github.com/dart-lang/sdk.git
if run_sync "$forbidden_dart_remote_workspace" >"$TMP_DIR/forbidden-dart-remote.log" 2>&1; then
echo "expected forbidden Dart SDK remote to fail" >&2
exit 1
fi
grep -q "Dart SDK source checkout uses forbidden remote fragment" \
"$TMP_DIR/forbidden-dart-remote.log"
forbidden_updater_remote_workspace="$TMP_DIR/forbidden-updater-remote"
make_workspace "$forbidden_updater_remote_workspace"
git -C "$forbidden_updater_remote_workspace/updater" remote add origin \
https://github.com/shorebirdtech/updater.git
if run_sync "$forbidden_updater_remote_workspace" >"$TMP_DIR/forbidden-updater-remote.log" 2>&1; then
echo "expected forbidden updater source remote to fail" >&2
exit 1
fi
grep -q "updater submodule source checkout uses forbidden remote fragment" \
"$TMP_DIR/forbidden-updater-remote.log"
forbidden_updater_url_workspace="$TMP_DIR/forbidden-updater-url"
make_workspace "$forbidden_updater_url_workspace"
rm -rf "$forbidden_updater_url_workspace/updater"
if DART_SRC="$forbidden_updater_url_workspace/dart-sdk-new" \
UPDATER_SRC="$forbidden_updater_url_workspace/missing-updater" \
UPDATER_URL=https://github.com/shorebirdtech/updater.git \
"$forbidden_updater_url_workspace/scripts/sync_open_sources.sh" \
>"$TMP_DIR/forbidden-updater-url.log" 2>&1; then
echo "expected forbidden explicit UPDATER_URL to fail" >&2
exit 1
fi
grep -q "UPDATER_URL points at a forbidden official Shorebird updater remote" \
"$TMP_DIR/forbidden-updater-url.log"
echo "sync_open_sources.sh smoke test passed"
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
"$ROOT/scripts/verify_ci_workflow.sh" \
--require-upload-ready \
"$ROOT/.github/workflows/open-shorebird-ci.yml"
echo "upload readiness check passed"
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/open-shorebird-sha256.XXXXXX")"
trap 'rm -rf "$TMP_DIR"' EXIT
PYTHON_BIN=python3
if ! command -v "$PYTHON_BIN" >/dev/null 2>&1; then
PYTHON_BIN=python
fi
ARTIFACT="$TMP_DIR/artifact.txt"
SIDECAR="$TMP_DIR/artifact.txt.sha256"
CUSTOM_SIDECAR="$TMP_DIR/custom.sha256"
printf 'open-shorebird\n' > "$ARTIFACT"
"$ROOT/scripts/write_sha256.sh" "$ARTIFACT"
EXPECTED_HASH="$("$PYTHON_BIN" - "$ARTIFACT" <<'PY'
import hashlib
import pathlib
import sys
print(hashlib.sha256(pathlib.Path(sys.argv[1]).read_bytes()).hexdigest())
PY
)"
EXPECTED_LINE="$EXPECTED_HASH artifact.txt"
ACTUAL_LINE="$(cat "$SIDECAR")"
if [[ "$ACTUAL_LINE" != "$EXPECTED_LINE" ]]; then
echo "unexpected sha256 sidecar: $ACTUAL_LINE" >&2
echo "expected: $EXPECTED_LINE" >&2
exit 70
fi
"$ROOT/scripts/write_sha256.sh" "$ARTIFACT" "$CUSTOM_SIDECAR"
CUSTOM_LINE="$(cat "$CUSTOM_SIDECAR")"
if [[ "$CUSTOM_LINE" != "$EXPECTED_LINE" ]]; then
echo "unexpected custom sha256 sidecar: $CUSTOM_LINE" >&2
echo "expected: $EXPECTED_LINE" >&2
exit 70
fi
if "$ROOT/scripts/write_sha256.sh" "$TMP_DIR/missing.txt" >/dev/null 2>&1; then
echo "write_sha256.sh unexpectedly succeeded for a missing artifact" >&2
exit 70
fi
echo "write_sha256.sh smoke test passed"
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""Write an open Shorebird artifact proxy manifest.
The artifact proxy expects this file at:
/shorebird/<shorebird-engine-revision>/artifacts_manifest.yaml
It maps a custom Shorebird engine revision back to the upstream Flutter engine
revision for unchanged artifacts, and lists the artifact paths that should be
served from the open Shorebird mirror.
"""
from __future__ import annotations
import argparse
from pathlib import Path
import sys
DEFAULT_ARTIFACT_OVERRIDES = (
"flutter_infra_release/flutter/$engine/android-arm64-release/artifacts.zip",
"flutter_infra_release/flutter/$engine/android-arm64-release/symbols.zip",
"flutter_infra_release/flutter/$engine/linux-x64-release/artifacts.zip",
"flutter_infra_release/flutter/$engine/linux-x64-release/linux-x64-flutter-gtk.zip",
"flutter_infra_release/flutter/$engine/ios-release/artifacts.zip",
"flutter_infra_release/flutter/$engine/flutter_patched_sdk_product.zip",
"flutter_infra_release/flutter/$engine/flutter-web-sdk.zip",
"flutter_infra_release/flutter/$engine/darwin-arm64-release/FlutterMacOS.framework.zip",
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument(
"--flutter-engine-revision",
required=True,
help="Upstream Flutter engine revision used for non-overridden artifacts.",
)
parser.add_argument(
"--storage-bucket",
default="shorebird",
help="Bucket/path prefix under the Shorebird artifact mirror.",
)
parser.add_argument(
"--output",
type=Path,
help="Output path. Writes to stdout when omitted.",
)
return parser.parse_args()
def yaml_quote(value: str) -> str:
return "'" + value.replace("'", "''") + "'"
def build_manifest(flutter_engine_revision: str, storage_bucket: str) -> str:
lines = [
f"flutter_engine_revision: {yaml_quote(flutter_engine_revision)}",
f"storage_bucket: {yaml_quote(storage_bucket)}",
"artifact_overrides:",
]
lines.extend(
f" - {yaml_quote(override)}" for override in DEFAULT_ARTIFACT_OVERRIDES
)
return "\n".join(lines) + "\n"
def main() -> int:
args = parse_args()
manifest = build_manifest(
flutter_engine_revision=args.flutter_engine_revision,
storage_bucket=args.storage_bucket,
)
if args.output is None:
sys.stdout.write(manifest)
return 0
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(manifest, encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+7 -2
View File
@@ -7,8 +7,13 @@ INCLUDE_ENGINE_DEPS="${INCLUDE_ENGINE_DEPS:-0}"
case "$PLATFORM" in
linux)
TARGET_OS='["linux"]'
DART_DOWNLOAD_ANDROID_DEPS="False"
if [[ "$INCLUDE_ENGINE_DEPS" == "1" ]]; then
TARGET_OS='["linux", "android"]'
DART_DOWNLOAD_ANDROID_DEPS="True"
else
TARGET_OS='["linux"]'
DART_DOWNLOAD_ANDROID_DEPS="False"
fi
;;
macos)
if [[ "$INCLUDE_ENGINE_DEPS" == "1" ]]; then
+177
View File
@@ -0,0 +1,177 @@
#!/usr/bin/env python3
"""Write a JSON manifest for CI release artifacts.
Every non-sidecar file under the input directory must have a sibling
`<file>.sha256` sidecar in the format written by scripts/write_sha256.py:
<hex sha256> <basename>
"""
from __future__ import annotations
import argparse
import fnmatch
import hashlib
import json
from pathlib import Path, PurePosixPath
import sys
def digest_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as file:
for chunk in iter(lambda: file.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def is_plain_file(path: Path) -> bool:
return path.is_file() and not path.is_symlink()
def parse_sidecar(path: Path) -> tuple[str, str]:
text = path.read_text(encoding="utf-8").strip()
parts = text.split()
if len(parts) != 2:
raise ValueError(f"expected '<sha256> <filename>', got {text!r}")
digest, filename = parts
if len(digest) != 64 or any(char not in "0123456789abcdef" for char in digest):
raise ValueError(f"invalid sha256 digest {digest!r}")
return digest, filename
def is_safe_relative_path(path: str) -> bool:
if not path or path == ".":
return False
if "\\" in path or "\x00" in path or path.endswith("/"):
return False
if any(ord(character) < 32 for character in path):
return False
candidate = PurePosixPath(path)
if candidate.is_absolute():
return False
if any(part in ("", ".", "..") for part in candidate.parts):
return False
if candidate.parts and ":" in candidate.parts[0]:
return False
return True
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("input_dir", type=Path)
parser.add_argument("--output", required=True, type=Path)
parser.add_argument("--github-sha", default="")
parser.add_argument(
"--require",
action="append",
default=[],
metavar="GLOB",
help="Require at least one artifact path matching this glob. May be repeated.",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
input_dir = args.input_dir
output_path = args.output
if not input_dir.is_dir():
print(f"missing input directory: {input_dir}", file=sys.stderr)
return 66
artifacts = []
errors = []
for artifact_path in sorted(path for path in input_dir.rglob("*") if path.is_file()):
if artifact_path.is_symlink():
errors.append(f"{artifact_path.relative_to(input_dir).as_posix()}: symlink artifacts are not allowed")
continue
if artifact_path.suffix == ".sha256":
continue
if artifact_path.resolve() == output_path.resolve():
continue
artifact_relative_path = artifact_path.relative_to(input_dir).as_posix()
if not is_safe_relative_path(artifact_relative_path):
errors.append(f"{artifact_relative_path}: unsafe artifact path")
continue
if artifact_path.stat().st_size <= 0:
errors.append(f"{artifact_relative_path}: empty artifacts are not allowed")
continue
artifact_relative = PurePosixPath(artifact_relative_path)
sidecar_path = Path(f"{artifact_path}.sha256")
if not is_plain_file(sidecar_path):
errors.append(f"{artifact_relative_path}: missing .sha256 sidecar")
continue
actual_digest = digest_file(artifact_path)
try:
sidecar_digest, sidecar_filename = parse_sidecar(sidecar_path)
except ValueError as error:
errors.append(f"{sidecar_path.relative_to(input_dir).as_posix()}: {error}")
continue
if sidecar_digest != actual_digest:
errors.append(
f"{sidecar_path.relative_to(input_dir).as_posix()}: digest mismatch "
f"{sidecar_digest} != {actual_digest}"
)
if sidecar_filename != artifact_path.name:
errors.append(
f"{sidecar_path.relative_to(input_dir).as_posix()}: filename mismatch "
f"{sidecar_filename!r} != {artifact_path.name!r}"
)
artifacts.append(
{
"path": artifact_relative_path,
"artifact_group": artifact_relative.parts[0],
"filename": artifact_path.name,
"sha256": actual_digest,
"size": artifact_path.stat().st_size,
"sidecar": sidecar_path.relative_to(input_dir).as_posix(),
}
)
orphan_sidecars = []
for sidecar_path in sorted(input_dir.rglob("*.sha256")):
artifact_path = Path(str(sidecar_path)[: -len(".sha256")])
if sidecar_path.is_symlink():
orphan_sidecars.append(sidecar_path.relative_to(input_dir).as_posix())
elif not is_plain_file(artifact_path):
orphan_sidecars.append(sidecar_path.relative_to(input_dir).as_posix())
if orphan_sidecars:
errors.append(f"orphan .sha256 sidecars: {', '.join(orphan_sidecars)}")
artifact_paths = [artifact["path"] for artifact in artifacts]
for required_glob in args.require:
if not any(fnmatch.fnmatchcase(path, required_glob) for path in artifact_paths):
errors.append(f"missing required artifact matching {required_glob!r}")
if errors:
print(
"release manifest validation failed:\n"
+ "\n".join(f" {error}" for error in errors),
file=sys.stderr,
)
return 70
manifest = {
"format_version": 1,
"github_sha": args.github_sha,
"artifact_count": len(artifacts),
"artifacts": artifacts,
}
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(
json.dumps(manifest, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""Write a sha256 sidecar in the common `digest filename` format."""
from __future__ import annotations
import hashlib
import pathlib
import sys
def digest_file(path: pathlib.Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as file:
for chunk in iter(lambda: file.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def main() -> int:
if len(sys.argv) not in (2, 3):
print("usage: write_sha256.py <artifact> [output]", file=sys.stderr)
return 64
artifact_path = pathlib.Path(sys.argv[1])
output_path = (
pathlib.Path(sys.argv[2])
if len(sys.argv) == 3
else pathlib.Path(f"{artifact_path}.sha256")
)
if not artifact_path.is_file():
print(f"missing artifact: {artifact_path}", file=sys.stderr)
return 66
output_path.write_text(
f"{digest_file(artifact_path)} {artifact_path.name}\n",
encoding="utf-8",
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ "$#" -ne 1 && "$#" -ne 2 ]]; then
echo "usage: $0 <artifact> [output]" >&2
exit 64
fi
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PYTHON_BIN=python3
if ! command -v "$PYTHON_BIN" >/dev/null 2>&1; then
PYTHON_BIN=python
fi
"$PYTHON_BIN" "$ROOT/scripts/write_sha256.py" "$@"