From 56f121c51753f7fa4c50d2f9c77362bad89b787e Mon Sep 17 00:00:00 2001 From: Tony Date: Wed, 24 Jun 2026 03:16:46 +0800 Subject: [PATCH] Create open Shorebird workspace meta-repo --- .gitattributes | 11 + .gitignore | 26 ++ .gitmodules | 28 ++ README.md | 55 +++ dart-sdk-new | 1 + depot_tools | 1 + docs/PLATFORM_TESTING.md | 81 ++++ docs/REPOSITORIES.md | 92 +++++ flutter | 1 + scripts/bootstrap_linux.sh | 5 + scripts/bootstrap_macos.sh | 5 + scripts/platform_test_common.sh | 69 ++++ scripts/sync_open_sources.sh | 49 +++ scripts/write_gclient.sh | 40 ++ shorebird | 1 + shorebird-engine | 1 + shorebird-server | 1 + .../bin/license_status.dart | 7 + .../lib/license.dart | 14 + .../license_flavor_patch_test/lib/main.dart | 34 ++ .../license_flavor_patch_test/pubspec.lock | 189 +++++++++ .../license_flavor_patch_test/pubspec.yaml | 19 + .../license_flavor_patch_test/shorebird.yaml | 5 + .../test/license_pro_test.dart | 19 + .../test/license_test.dart | 17 + .../tool/verify_aot_patch.dart | 363 ++++++++++++++++++ updater | 1 + 27 files changed, 1135 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 README.md create mode 160000 dart-sdk-new create mode 160000 depot_tools create mode 100644 docs/PLATFORM_TESTING.md create mode 100644 docs/REPOSITORIES.md create mode 160000 flutter create mode 100755 scripts/bootstrap_linux.sh create mode 100755 scripts/bootstrap_macos.sh create mode 100755 scripts/platform_test_common.sh create mode 100755 scripts/sync_open_sources.sh create mode 100755 scripts/write_gclient.sh create mode 160000 shorebird create mode 160000 shorebird-engine create mode 160000 shorebird-server create mode 100644 testapps/license_flavor_patch_test/bin/license_status.dart create mode 100644 testapps/license_flavor_patch_test/lib/license.dart create mode 100644 testapps/license_flavor_patch_test/lib/main.dart create mode 100644 testapps/license_flavor_patch_test/pubspec.lock create mode 100644 testapps/license_flavor_patch_test/pubspec.yaml create mode 100644 testapps/license_flavor_patch_test/shorebird.yaml create mode 100644 testapps/license_flavor_patch_test/test/license_pro_test.dart create mode 100644 testapps/license_flavor_patch_test/test/license_test.dart create mode 100644 testapps/license_flavor_patch_test/tool/verify_aot_patch.dart create mode 160000 updater diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5ef7161 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +* text=auto + +*.sh text eol=lf +*.md text eol=lf +*.dart text eol=lf +*.yaml text eol=lf +*.lock text eol=lf + +.gitattributes text eol=lf +.gitignore text eol=lf +.gitmodules text eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5dee144 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Platform/generated checkout metadata. +.gclient +.gclient_entries +.gclient_previous_* +.cipd/ +buildtools/ +sdk/ + +# Root scratch output. +build_log*.txt +build_patched_sdk.* +compare_tool.dart +test_bytecode.dart +*.log + +# Local agent state. +.agents/ + +# Dart/Flutter build output in root-owned test fixtures. +**/.dart_tool/ +**/build/ + +# Local editor/OS files. +.DS_Store +.idea/ +.vscode/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..9342dd4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,28 @@ +[submodule "dart-sdk-new"] + path = dart-sdk-new + url = https://git.tonycloud.org/dart-lang/sdk.git + branch = tonycloud/dev +[submodule "depot_tools"] + path = depot_tools + url = https://chromium.googlesource.com/chromium/tools/depot_tools.git + branch = main +[submodule "flutter"] + path = flutter + url = https://git.tonycloud.org/flutter/flutter.git + branch = shorebird/dev +[submodule "shorebird"] + path = shorebird + url = https://git.tonycloud.org/flutter/shorebird.git + branch = main +[submodule "shorebird-engine"] + path = shorebird-engine + url = https://github.com/shorebirdtech/engine.git + branch = shorebird/dev +[submodule "shorebird-server"] + path = shorebird-server + url = https://git.tonycloud.org/flutter/shorebird-server.git + branch = main +[submodule "updater"] + path = updater + url = https://git.tonycloud.org/flutter/shorebird-updater.git + branch = main diff --git a/README.md b/README.md new file mode 100644 index 0000000..fae2197 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# Open Shorebird Workspace + +This repository is a meta-workspace for the open Shorebird replacement work. It +pins the source repositories used by the SDK, Flutter/engine, CLI, updater, and +self-hosted server work as git submodules, while keeping local test fixtures and +platform scripts in this root repository. + +## Submodules + +| Path | Role | Remote | Branch | +| --- | --- | --- | --- | +| `dart-sdk-new` | Dart SDK fork with AOT patch runtime work | `https://git.tonycloud.org/dart-lang/sdk.git` | `tonycloud/dev` | +| `flutter` | Flutter fork used by app and engine integration tests | `https://git.tonycloud.org/flutter/flutter.git` | `shorebird/dev` | +| `shorebird` | CLI, protocol/client packages, open patch tools | `https://git.tonycloud.org/flutter/shorebird.git` | `main` | +| `shorebird-engine` | Shorebird engine reference checkout | `https://github.com/shorebirdtech/engine.git` | `shorebird/dev` | +| `shorebird-server` | Self-hosted CodePush/auth/management server | `https://git.tonycloud.org/flutter/shorebird-server.git` | `main` | +| `updater` | Runtime updater and patch package tooling | `https://git.tonycloud.org/flutter/shorebird-updater.git` | `main` | +| `depot_tools` | Chromium/Dart checkout tooling | `https://chromium.googlesource.com/chromium/tools/depot_tools.git` | `main` | + +The Dart SDK checkout has additional gclient-managed dependencies under +`dart-sdk-new/third_party`. They are intentionally not top-level submodules. + +## First Checkout + +```bash +git submodule update --init --recursive +``` + +For Linux: + +```bash +./scripts/bootstrap_linux.sh +``` + +For macOS and iOS-preparation checks: + +```bash +./scripts/bootstrap_macos.sh +``` + +By default the bootstrap scripts run `gclient sync`, which can download a large +toolchain/dependency set. Set `SKIP_GCLIENT_SYNC=1` when the checkout is already +synced or when you only want source-level tests. + +## Local Test Fixture + +`testapps/license_flavor_patch_test` is kept in the root repository because it +tests the open AOT patch and license/flavor behavior across the nested repos. +Generated build output under that app is ignored by this meta-repository. + +## Documents + +- `docs/REPOSITORIES.md` records the remote URL inventory and cleanup policy. +- `docs/PLATFORM_TESTING.md` documents the Linux, macOS, and later iOS test + flow. diff --git a/dart-sdk-new b/dart-sdk-new new file mode 160000 index 0000000..08139af --- /dev/null +++ b/dart-sdk-new @@ -0,0 +1 @@ +Subproject commit 08139af589d0d32fbdec64b127c075ea3427cde8 diff --git a/depot_tools b/depot_tools new file mode 160000 index 0000000..90f5d4d --- /dev/null +++ b/depot_tools @@ -0,0 +1 @@ +Subproject commit 90f5d4d14de66a5aae4a55b554ad4565111169af diff --git a/docs/PLATFORM_TESTING.md b/docs/PLATFORM_TESTING.md new file mode 100644 index 0000000..b8944f6 --- /dev/null +++ b/docs/PLATFORM_TESTING.md @@ -0,0 +1,81 @@ +# Linux And macOS Platform Testing + +The first complete runtime proof was built on Windows. The next platform pass +should verify the same source layout on Linux and macOS, then use macOS to +prepare iOS testing without introducing JIT or writable-executable-memory +requirements. + +## Prerequisites + +Linux: + +- Git +- Bash +- Dart or Flutter on `PATH` +- Go 1.23+ +- Python and standard build dependencies required by Dart/Flutter + +macOS: + +- Git +- Bash +- Dart or Flutter on `PATH` +- Go 1.23+ +- Xcode command line tools +- Full Xcode for iOS simulator/device follow-up + +Both platforms need network access for a fresh `git submodule update` and +`gclient sync`. + +## Bootstrap + +Linux: + +```bash +./scripts/bootstrap_linux.sh +``` + +macOS: + +```bash +./scripts/bootstrap_macos.sh +``` + +Useful environment variables: + +| Variable | Effect | +| --- | --- | +| `SKIP_GCLIENT_SYNC=1` | Do not run `gclient sync`. Useful when dependencies are already present. | +| `SKIP_TESTS=1` | Prepare checkout only; skip Dart/Go tests. | +| `DART_BIN=/path/to/dart` | Override the Dart executable. | +| `FLUTTER_BIN=/path/to/flutter` | Override the Flutter executable. | +| `GO_BIN=/path/to/go` | Override the Go executable. | +| `RUN_IOS_SMOKE=1` | On macOS, run a no-codesign iOS build smoke check when Flutter/Xcode are available. | + +## Expected Checks + +The bootstrap scripts run: + +1. `git submodule update --init --recursive` +2. platform-specific `.gclient` generation +3. optional `gclient sync` +4. public updater sync into the Flutter engine third-party location +5. Shorebird CLI focused tests for self-hosted `base_url` behavior +6. open AOT patch tool tests +7. self-hosted server Go tests +8. license/flavor AOT verification when `dart-sdk-new/out/ReleaseX64AotPatch/args.gn` + already exists + +## iOS Preparation Notes + +The macOS script writes: + +```python +target_os = ["mac", "ios"] +``` + +That prepares gclient for iOS dependencies. The actual iOS runtime test should +still be run later on macOS after the Flutter engine/Dart SDK patch build is +available. The AOT patching design remains iOS-compatible only if patch loading +uses mapped snapshot data/instructions and does not rely on JIT, the KBC +interpreter, or writable executable memory. diff --git a/docs/REPOSITORIES.md b/docs/REPOSITORIES.md new file mode 100644 index 0000000..1e9fb6e --- /dev/null +++ b/docs/REPOSITORIES.md @@ -0,0 +1,92 @@ +# Repository Inventory + +Last verified: 2026-06-24. + +## Active Top-Level Repositories + +| Path | Remote URL | Branch | Pinned commit | +| --- | --- | --- | --- | +| `dart-sdk-new` | `https://git.tonycloud.org/dart-lang/sdk.git` | `tonycloud/dev` | `08139af589d0d32fbdec64b127c075ea3427cde8` | +| `depot_tools` | `https://chromium.googlesource.com/chromium/tools/depot_tools.git` | `main` | `90f5d4d14de66a5aae4a55b554ad4565111169af` | +| `flutter` | `https://git.tonycloud.org/flutter/flutter.git` | `shorebird/dev` | `9754ce03d76fb06c23a72ddbd8b7137bfa85446c` | +| `shorebird` | `https://git.tonycloud.org/flutter/shorebird.git` | `main` | `c824005c2dc65f56b0fa078984fadb649876fce7` | +| `shorebird-engine` | `https://github.com/shorebirdtech/engine.git` | `shorebird/dev` | `e4575c7403bd3453d04bfa03e01218e5a0bca0c6` | +| `shorebird-server` | `https://git.tonycloud.org/flutter/shorebird-server.git` | `main` | `774954fce70181846995ebcad842e9c54e57f1a2` | +| `updater` | `https://git.tonycloud.org/flutter/shorebird-updater.git` | `main` | `3ac748ff28eb4ebb53630c66230b9b1be5df73c2` | + +## Generated Or Obsolete Root Folders + +The following root folders are not part of the meta-workspace and can be +regenerated or replaced by tracked source: + +| Path | Reason | +| --- | --- | +| `.cipd` | Local CIPD cache created by gclient/depot_tools. | +| `buildtools` | Ad hoc root tool download/output; Dart build tools live under `dart-sdk-new/buildtools`. | +| `dart-sdk` | Empty stale SDK checkout. | +| `hello_shorebird_test` | Old throwaway Flutter app. The active fixture is `testapps/license_flavor_patch_test`. | +| `patches` | Earlier one-off patch files superseded by source changes and `packages/open_aot_patch_tools`. | +| `sdk` | Generated Windows toolchain cache. | +| `testapp2` | Old throwaway Flutter app. | +| `testapp3` | Old throwaway Flutter app. | + +## Submodule Policy + +Only top-level project repositories are submodules. Third-party repositories and +CIPD packages fetched by `gclient sync` remain owned by their upstream DEPS files +and should not be duplicated in `.gitmodules`. + +Root-owned content should be limited to: + +- workspace documentation +- bootstrap and test scripts +- small test fixtures that intentionally combine multiple submodules +- meta-repository config such as `.gitignore` and `.gitmodules` + +## Nested Git Checkouts Observed + +These repositories were found under top-level checkouts. They are managed by the +owning repository's DEPS/gclient workflow, not by the root `.gitmodules` file. + +| Path | Remote URL | +| --- | --- | +| `dart-sdk-new/buildtools/clang_format/script` | `https://chromium.googlesource.com/chromium/llvm-project/cfe/tools/clang-format.git` | +| `dart-sdk-new/tests/co19/src` | `https://dart.googlesource.com/co19` | +| `dart-sdk-new/third_party/binaryen/src` | `https://chromium.googlesource.com/external/github.com/WebAssembly/binaryen.git` | +| `dart-sdk-new/third_party/boringssl/src` | `https://boringssl.googlesource.com/boringssl.git` | +| `dart-sdk-new/third_party/cpu_features/src` | `https://chromium.googlesource.com/external/github.com/google/cpu_features.git` | +| `dart-sdk-new/third_party/crashpad/crashpad` | `https://chromium.googlesource.com/crashpad/crashpad.git` | +| `dart-sdk-new/third_party/cygwin` | `https://chromium.googlesource.com/chromium/deps/cygwin.git` | +| `dart-sdk-new/third_party/emsdk` | `https://dart.googlesource.com/external/github.com/emscripten-core/emsdk.git` | +| `dart-sdk-new/third_party/googletest` | `https://fuchsia.googlesource.com/third_party/googletest` | +| `dart-sdk-new/third_party/icu` | `https://chromium.googlesource.com/chromium/deps/icu.git` | +| `dart-sdk-new/third_party/jinja2` | `https://chromium.googlesource.com/chromium/src/third_party/jinja2.git` | +| `dart-sdk-new/third_party/libc` | `https://llvm.googlesource.com/llvm-project/libc` | +| `dart-sdk-new/third_party/libcxx` | `https://llvm.googlesource.com/llvm-project/libcxx` | +| `dart-sdk-new/third_party/libcxxabi` | `https://llvm.googlesource.com/llvm-project/libcxxabi` | +| `dart-sdk-new/third_party/markupsafe` | `https://chromium.googlesource.com/chromium/src/third_party/markupsafe.git` | +| `dart-sdk-new/third_party/mini_chromium/mini_chromium` | `https://chromium.googlesource.com/chromium/mini_chromium` | +| `dart-sdk-new/third_party/perfetto/src` | `https://chromium.googlesource.com/external/github.com/google/perfetto` | +| `dart-sdk-new/third_party/pkg/core` | `https://dart.googlesource.com/core.git` | +| `dart-sdk-new/third_party/pkg/dart_style` | `https://dart.googlesource.com/dart_style.git` | +| `dart-sdk-new/third_party/pkg/dartdoc` | `https://dart.googlesource.com/dartdoc.git` | +| `dart-sdk-new/third_party/pkg/ecosystem` | `https://dart.googlesource.com/ecosystem.git` | +| `dart-sdk-new/third_party/pkg/http` | `https://dart.googlesource.com/http.git` | +| `dart-sdk-new/third_party/pkg/i18n` | `https://dart.googlesource.com/i18n.git` | +| `dart-sdk-new/third_party/pkg/leak_tracker` | `https://dart.googlesource.com/leak_tracker.git` | +| `dart-sdk-new/third_party/pkg/native` | `https://dart.googlesource.com/native.git` | +| `dart-sdk-new/third_party/pkg/protobuf` | `https://dart.googlesource.com/protobuf.git` | +| `dart-sdk-new/third_party/pkg/pub` | `https://dart.googlesource.com/pub.git` | +| `dart-sdk-new/third_party/pkg/shelf` | `https://dart.googlesource.com/shelf.git` | +| `dart-sdk-new/third_party/pkg/sync_http` | `https://dart.googlesource.com/sync_http.git` | +| `dart-sdk-new/third_party/pkg/tar` | `https://dart.googlesource.com/external/github.com/simolus3/tar.git` | +| `dart-sdk-new/third_party/pkg/test` | `https://dart.googlesource.com/test.git` | +| `dart-sdk-new/third_party/pkg/tools` | `https://dart.googlesource.com/tools.git` | +| `dart-sdk-new/third_party/pkg/vector_math` | `https://dart.googlesource.com/external/github.com/google/vector_math.dart.git` | +| `dart-sdk-new/third_party/pkg/web` | `https://dart.googlesource.com/web.git` | +| `dart-sdk-new/third_party/pkg/webdev` | `https://dart.googlesource.com/webdev.git` | +| `dart-sdk-new/third_party/pkg/webdriver` | `https://dart.googlesource.com/external/github.com/google/webdriver.dart.git` | +| `dart-sdk-new/third_party/pkg/webkit_inspection_protocol` | `https://dart.googlesource.com/external/github.com/google/webkit_inspection_protocol.dart.git` | +| `dart-sdk-new/third_party/ply` | `https://chromium.googlesource.com/chromium/src/third_party/ply.git` | +| `dart-sdk-new/third_party/WebCore` | `https://dart.googlesource.com/webcore.git` | +| `dart-sdk-new/third_party/zlib` | `https://chromium.googlesource.com/chromium/src/third_party/zlib.git` | diff --git a/flutter b/flutter new file mode 160000 index 0000000..9754ce0 --- /dev/null +++ b/flutter @@ -0,0 +1 @@ +Subproject commit 9754ce03d76fb06c23a72ddbd8b7137bfa85446c diff --git a/scripts/bootstrap_linux.sh b/scripts/bootstrap_linux.sh new file mode 100755 index 0000000..e64a612 --- /dev/null +++ b/scripts/bootstrap_linux.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +exec "$ROOT/scripts/platform_test_common.sh" linux diff --git a/scripts/bootstrap_macos.sh b/scripts/bootstrap_macos.sh new file mode 100755 index 0000000..32dec08 --- /dev/null +++ b/scripts/bootstrap_macos.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +exec "$ROOT/scripts/platform_test_common.sh" macos diff --git a/scripts/platform_test_common.sh b/scripts/platform_test_common.sh new file mode 100755 index 0000000..7f0d4d6 --- /dev/null +++ b/scripts/platform_test_common.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +set -euo pipefail + +PLATFORM="${1:?platform is required}" +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +DART_BIN="${DART_BIN:-dart}" +FLUTTER_BIN="${FLUTTER_BIN:-flutter}" +GO_BIN="${GO_BIN:-go}" + +run() { + echo + echo "==> $*" + "$@" +} + +require_command() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "missing required command: $1" >&2 + exit 127 + fi +} + +require_command git +require_command "$DART_BIN" +require_command "$GO_BIN" + +if [[ "$PLATFORM" == "macos" ]]; then + if command -v xcodebuild >/dev/null 2>&1; then + xcodebuild -version + else + echo "warning: xcodebuild not found; iOS follow-up checks will be unavailable." >&2 + fi +fi + +run git -C "$ROOT" submodule update --init --recursive +run "$ROOT/scripts/write_gclient.sh" "$PLATFORM" +run "$ROOT/scripts/sync_open_sources.sh" + +export PATH="$ROOT/depot_tools:$PATH" +if [[ "${SKIP_GCLIENT_SYNC:-0}" != "1" ]]; then + require_command gclient + run gclient sync --no-history +else + echo + echo "==> skipping gclient sync because SKIP_GCLIENT_SYNC=1" +fi + +if [[ "${SKIP_TESTS:-0}" == "1" ]]; then + echo + echo "==> skipping tests because SKIP_TESTS=1" + 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/packages/open_aot_patch_tools' && '$DART_BIN' pub get && '$DART_BIN' test" +run bash -lc "cd '$ROOT/shorebird-server' && '$GO_BIN' test ./..." + +AOT_ARGS="$ROOT/dart-sdk-new/out/ReleaseX64AotPatch/args.gn" +if [[ -f "$AOT_ARGS" ]]; then + run bash -lc "cd '$ROOT/testapps/license_flavor_patch_test' && '$FLUTTER_BIN' pub get && '$DART_BIN' run tool/verify_aot_patch.dart" +else + echo + echo "==> skipping license/flavor AOT verification; missing $AOT_ARGS" +fi + +if [[ "$PLATFORM" == "macos" && "${RUN_IOS_SMOKE:-0}" == "1" ]]; then + require_command "$FLUTTER_BIN" + run bash -lc "cd '$ROOT/testapps/license_flavor_patch_test' && '$FLUTTER_BIN' build ios --debug --no-codesign" +fi diff --git a/scripts/sync_open_sources.sh b/scripts/sync_open_sources.sh new file mode 100755 index 0000000..5e067df --- /dev/null +++ b/scripts/sync_open_sources.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +UPDATER_SRC="${UPDATER_SRC:-$ROOT/updater}" +UPDATER_URL="${UPDATER_URL:-https://github.com/shorebirdtech/updater.git}" +TARGET="$ROOT/flutter/engine/src/flutter/third_party/updater" + +echo "[open-source-sync] updater source: $UPDATER_SRC" +echo "[open-source-sync] target: $TARGET" + +mkdir -p "$(dirname "$TARGET")" + +if [[ -L "$TARGET" ]]; then + echo "[open-source-sync] updater target is already a symlink." + exit 0 +fi + +if [[ -d "$TARGET/.git" ]]; 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 + fi +elif [[ -e "$TARGET" ]]; then + echo "target exists but is not a symlink or git checkout: $TARGET" >&2 + exit 1 +elif [[ -d "$UPDATER_SRC/.git" ]]; 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}" +fi + +if [[ ! -f "$TARGET/library/include/updater_engine.h" ]]; then + echo "updater checkout is missing library/include/updater_engine.h" >&2 + exit 1 +fi + +echo "[open-source-sync] public Shorebird updater is available." diff --git a/scripts/write_gclient.sh b/scripts/write_gclient.sh new file mode 100755 index 0000000..ad95696 --- /dev/null +++ b/scripts/write_gclient.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PLATFORM="${1:-}" + +case "$PLATFORM" in + linux) + TARGET_OS='["linux"]' + ;; + macos) + TARGET_OS='["mac", "ios"]' + ;; + *) + echo "usage: $0 " >&2 + exit 64 + ;; +esac + +cat > "$ROOT/.gclient" < LicenseType.pro, + 'enterprise' => LicenseType.enterprise, + _ => LicenseType.free, + }; +} + +bool get proFeatureEnabled => + currentLicenseType() == LicenseType.pro || + currentLicenseType() == LicenseType.enterprise; diff --git a/testapps/license_flavor_patch_test/lib/main.dart b/testapps/license_flavor_patch_test/lib/main.dart new file mode 100644 index 0000000..3a9580e --- /dev/null +++ b/testapps/license_flavor_patch_test/lib/main.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +import 'license.dart'; + +void main() => runApp(const LicenseFlavorPatchApp()); + +class LicenseFlavorPatchApp extends StatelessWidget { + const LicenseFlavorPatchApp({super.key}); + + @override + Widget build(BuildContext context) { + final license = currentLicenseType(); + return MaterialApp( + home: Scaffold( + appBar: AppBar(title: const Text('License Patch Test')), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'license:${license.name}', + key: const ValueKey('license-status'), + ), + Text( + proFeatureEnabled ? 'pro-feature:enabled' : 'pro-feature:off', + key: const ValueKey('feature-status'), + ), + ], + ), + ), + ), + ); + } +} diff --git a/testapps/license_flavor_patch_test/pubspec.lock b/testapps/license_flavor_patch_test/pubspec.lock new file mode 100644 index 0000000..3dc43b3 --- /dev/null +++ b/testapps/license_flavor_patch_test/pubspec.lock @@ -0,0 +1,189 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" + source: hosted + version: "0.12.19" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + url: "https://pub.dev" + source: hosted + version: "0.7.10" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" +sdks: + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/testapps/license_flavor_patch_test/pubspec.yaml b/testapps/license_flavor_patch_test/pubspec.yaml new file mode 100644 index 0000000..b3b901c --- /dev/null +++ b/testapps/license_flavor_patch_test/pubspec.yaml @@ -0,0 +1,19 @@ +name: license_flavor_patch_test +description: Test app for encrypted flavor/license AOT patch verification. +publish_to: none + +environment: + sdk: ^3.9.0 + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true + assets: + - shorebird.yaml diff --git a/testapps/license_flavor_patch_test/shorebird.yaml b/testapps/license_flavor_patch_test/shorebird.yaml new file mode 100644 index 0000000..243d9b1 --- /dev/null +++ b/testapps/license_flavor_patch_test/shorebird.yaml @@ -0,0 +1,5 @@ +app_id: license-flavor-patch-test +flavors: + free: license-flavor-free + pro: license-flavor-pro + enterprise: license-flavor-enterprise diff --git a/testapps/license_flavor_patch_test/test/license_pro_test.dart b/testapps/license_flavor_patch_test/test/license_pro_test.dart new file mode 100644 index 0000000..0b2f0c1 --- /dev/null +++ b/testapps/license_flavor_patch_test/test/license_pro_test.dart @@ -0,0 +1,19 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:license_flavor_patch_test/license.dart'; +import 'package:license_flavor_patch_test/main.dart'; + +void main() { + final isProFlavor = currentLicenseType() == LicenseType.pro; + + test('pro flavor enables pro feature gate', () { + expect(currentLicenseType(), LicenseType.pro); + expect(proFeatureEnabled, isTrue); + }, skip: !isProFlavor); + + testWidgets('renders pro license status labels', (tester) async { + await tester.pumpWidget(const LicenseFlavorPatchApp()); + + expect(find.text('license:pro'), findsOneWidget); + expect(find.text('pro-feature:enabled'), findsOneWidget); + }, skip: !isProFlavor); +} diff --git a/testapps/license_flavor_patch_test/test/license_test.dart b/testapps/license_flavor_patch_test/test/license_test.dart new file mode 100644 index 0000000..b875d7e --- /dev/null +++ b/testapps/license_flavor_patch_test/test/license_test.dart @@ -0,0 +1,17 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:license_flavor_patch_test/license.dart'; +import 'package:license_flavor_patch_test/main.dart'; + +void main() { + test('defaults to free license flavor', () { + expect(currentLicenseType(), LicenseType.free); + expect(proFeatureEnabled, isFalse); + }); + + testWidgets('renders free license status labels', (tester) async { + await tester.pumpWidget(const LicenseFlavorPatchApp()); + + expect(find.text('license:free'), findsOneWidget); + expect(find.text('pro-feature:off'), findsOneWidget); + }); +} diff --git a/testapps/license_flavor_patch_test/tool/verify_aot_patch.dart b/testapps/license_flavor_patch_test/tool/verify_aot_patch.dart new file mode 100644 index 0000000..084779e --- /dev/null +++ b/testapps/license_flavor_patch_test/tool/verify_aot_patch.dart @@ -0,0 +1,363 @@ +import 'dart:convert'; +import 'dart:io'; + +Future main() async { + final appDir = File.fromUri(Platform.script).parent.parent; + final workspaceRoot = appDir.parent.parent; + final dartRoot = Directory('${workspaceRoot.path}/dart-sdk-new'); + final patchBuildDir = Directory('${dartRoot.path}/out/ReleaseX64AotPatch'); + final toolWorkspace = Directory('${workspaceRoot.path}/shorebird'); + final workDir = Directory('${appDir.path}/build/open_aot_patch_verify'); + + final argsFile = File('${patchBuildDir.path}/args.gn'); + if (!argsFile.existsSync()) { + throw StateError( + 'Missing ${argsFile.path}. Build the patch runtime first with ' + 'dart_enable_aot_patching=true and dart_dynamic_modules=false.', + ); + } + final args = argsFile.readAsStringSync(); + if (!args.contains('dart_enable_aot_patching = true') || + !args.contains('dart_dynamic_modules = false')) { + throw StateError( + '${argsFile.path} must enable AOT patching and disable dynamic modules.', + ); + } + + if (workDir.existsSync()) { + workDir.deleteSync(recursive: true); + } + workDir.createSync(recursive: true); + + final packageConfig = File('${workDir.path}/package_config.json'); + packageConfig.writeAsStringSync( + jsonEncode({ + 'configVersion': 2, + 'packages': [ + { + 'name': 'license_flavor_patch_test', + 'rootUri': appDir.uri.toString(), + 'packageUri': 'lib/', + 'languageVersion': '3.9', + }, + ], + }), + ); + + final baseMap = File('${workDir.path}/base.obfuscation.json'); + final patchMap = File('${workDir.path}/patch.obfuscation.json'); + final baseVmcode = File('${workDir.path}/base_free.vmcode'); + final patchVmcode = File('${workDir.path}/patch_pro.vmcode'); + final reconstructedVmcode = File('${workDir.path}/reconstructed_pro.vmcode'); + + await _compileAot( + workspaceRoot: workspaceRoot, + dartRoot: dartRoot, + patchBuildDir: patchBuildDir, + packageConfig: packageConfig, + licenseType: 'free', + outputVmcode: baseVmcode, + saveObfuscationMap: baseMap, + ); + await _compileAot( + workspaceRoot: workspaceRoot, + dartRoot: dartRoot, + patchBuildDir: patchBuildDir, + packageConfig: packageConfig, + licenseType: 'pro', + outputVmcode: patchVmcode, + loadObfuscationMap: baseMap, + saveObfuscationMap: patchMap, + ); + + _expectStatus( + await _runAot(patchBuildDir, baseVmcode), + license: 'free', + proFeature: false, + ); + _expectStatus( + await _runAot(patchBuildDir, patchVmcode), + license: 'pro', + proFeature: true, + ); + + final artifact = File('${workDir.path}/patch.json'); + final encrypted = File('${workDir.path}/patch.encrypted.json'); + const keyHex = + '000102030405060708090a0b0c0d0e0f' + '101112131415161718191a1b1c1d1e1f'; + const wrongKeyHex = + '1f1e1d1c1b1a19181716151413121110' + '0f0e0d0c0b0a09080706050403020100'; + const nonceHex = '000102030405060708090a0b'; + const offlineExpiresAt = '2030-01-01T00:00:00Z'; + const beforeExpiry = '2029-01-01T00:00:00Z'; + const afterExpiry = '2031-01-01T00:00:00Z'; + + await _runTool(toolWorkspace, [ + 'link', + '--base=${baseVmcode.path}', + '--patch=${patchVmcode.path}', + '--output=${artifact.path}', + '--app-id=license-flavor-patch-test', + '--app-build-id=host-aot-smoke', + '--base-flavor-id=free', + '--base-license-type=free', + '--flavor-id=pro', + '--license-type=pro', + '--sdk-hash=${_sha256File(File('${patchBuildDir.path}/gen_snapshot.exe'))}', + '--target-os=windows', + '--target-arch=x64', + '--obfuscation-map-hash=${_sha256File(baseMap)}', + '--offline-expires-at=$offlineExpiresAt', + '--full-snapshot=true', + ]); + + final artifactJson = (jsonDecode(artifact.readAsStringSync()) as Map) + .cast(); + if (artifactJson['payload_kind'] != 'full-snapshot') { + throw StateError('Expected a directly loadable full-snapshot payload.'); + } + final metadata = (artifactJson['metadata'] as Map).cast(); + if (metadata['offline_expires_at'] != '2030-01-01T00:00:00.000Z') { + throw StateError('Expected normalized offline_expires_at metadata.'); + } + + await _runTool(toolWorkspace, [ + 'encrypt', + '--input=${artifact.path}', + '--output=${encrypted.path}', + '--key-id=test-key', + '--key-hex=$keyHex', + '--nonce-hex=$nonceHex', + ]); + + await _runTool(toolWorkspace, [ + 'verify', + '--input=${encrypted.path}', + '--key-hex=$keyHex', + '--artifact-sha256=${_sha256File(encrypted)}', + '--base-flavor-id=free', + '--base-license-type=free', + '--flavor-id=pro', + '--license-type=pro', + '--base=${baseVmcode.path}', + '--now=$beforeExpiry', + ]); + await _runToolExpectFailure(toolWorkspace, [ + 'verify', + '--input=${encrypted.path}', + '--key-hex=$keyHex', + '--base-flavor-id=free', + '--base-license-type=free', + '--flavor-id=pro', + '--license-type=pro', + '--base=${baseVmcode.path}', + '--now=$afterExpiry', + ]); + await _runToolExpectFailure(toolWorkspace, [ + 'verify', + '--input=${encrypted.path}', + '--key-hex=$wrongKeyHex', + '--base-flavor-id=free', + '--base-license-type=free', + '--flavor-id=pro', + '--license-type=pro', + '--base=${baseVmcode.path}', + ]); + await _runToolExpectFailure(toolWorkspace, [ + 'verify', + '--input=${encrypted.path}', + '--key-hex=$keyHex', + '--base-flavor-id=free', + '--base-license-type=free', + '--flavor-id=enterprise', + '--license-type=pro', + '--base=${baseVmcode.path}', + ]); + + await _runTool(toolWorkspace, [ + 'dump-blobs', + '--input=${encrypted.path}', + '--key-hex=$keyHex', + '--base=${baseVmcode.path}', + '--output=${reconstructedVmcode.path}', + '--now=$beforeExpiry', + ]); + if (_sha256File(reconstructedVmcode) != _sha256File(patchVmcode)) { + throw StateError('Reconstructed patch vmcode does not match pro snapshot.'); + } + + _expectStatus( + await _runAot(patchBuildDir, reconstructedVmcode), + license: 'pro', + proFeature: true, + ); + + await _runToolExpectFailure(toolWorkspace, [ + 'dump-blobs', + '--input=${encrypted.path}', + '--key-hex=$keyHex', + '--base=${baseVmcode.path}', + '--output=${reconstructedVmcode.path}', + '--now=$afterExpiry', + ]); + + stdout.writeln( + 'AOT patch applied to license_flavor_patch_test successfully.', + ); + stdout.writeln('base: license:free / pro-feature:off'); + stdout.writeln('patch: license:pro / pro-feature:enabled'); + stdout.writeln('artifact: ${encrypted.path}'); + stdout.writeln('vmcode: ${reconstructedVmcode.path}'); +} + +Future _compileAot({ + required Directory workspaceRoot, + required Directory dartRoot, + required Directory patchBuildDir, + required File packageConfig, + required String licenseType, + required File outputVmcode, + required File saveObfuscationMap, + File? loadObfuscationMap, +}) async { + final dart = '${dartRoot.path}/tools/sdks/dart-sdk/bin/dart.exe'; + final genKernel = '${dartRoot.path}/pkg/vm/bin/gen_kernel.dart'; + final source = + '${workspaceRoot.path}/testapps/license_flavor_patch_test/bin/license_status.dart'; + final dill = File('${outputVmcode.parent.path}/${licenseType}_app.dill'); + await _run(dart, [ + genKernel, + '--packages', + packageConfig.path, + '--platform', + '${patchBuildDir.path}/vm_platform_product.dill', + '--aot', + '--target-os', + 'windows', + '-Ddart.vm.product=true', + '-DLICENSE_TYPE=$licenseType', + '-o', + dill.path, + '--invocation-modes=compile', + '--verbosity=all', + source, + ]); + + final genSnapshot = '${patchBuildDir.path}/gen_snapshot.exe'; + await _run(genSnapshot, [ + '--snapshot-kind=app-aot-elf', + '--elf=${outputVmcode.path}', + '--strip', + '--obfuscate', + if (loadObfuscationMap != null) + '--load-obfuscation-map=${loadObfuscationMap.path}', + '--save-obfuscation-map=${saveObfuscationMap.path}', + dill.path, + ]); +} + +Future _runAot(Directory patchBuildDir, File vmcode) async { + final result = await _run('${patchBuildDir.path}/dartaotruntime.exe', [ + vmcode.path, + ]); + return result.stdout as String; +} + +Future _runTool( + Directory toolWorkspace, + List args, +) async { + return _run( + '${toolWorkspace.parent.path}/dart-sdk-new/tools/sdks/dart-sdk/bin/dart.exe', + ['packages/open_aot_patch_tools/bin/open_aot_patch_tools.dart', ...args], + workingDirectory: toolWorkspace, + ); +} + +Future _runToolExpectFailure( + Directory toolWorkspace, + List args, +) async { + final result = await Process.run( + '${toolWorkspace.parent.path}/dart-sdk-new/tools/sdks/dart-sdk/bin/dart.exe', + ['packages/open_aot_patch_tools/bin/open_aot_patch_tools.dart', ...args], + workingDirectory: toolWorkspace.path, + ); + if (result.exitCode == 0) { + throw StateError( + 'Expected open_aot_patch_tools ${args.join(' ')} to fail.', + ); + } + return result; +} + +Future _run( + String executable, + List args, { + Directory? workingDirectory, +}) async { + final result = await Process.run( + executable, + args, + workingDirectory: workingDirectory?.path, + ); + if (result.exitCode != 0) { + throw ProcessException( + executable, + args, + [ + 'exit code ${result.exitCode}', + 'stdout:', + result.stdout, + 'stderr:', + result.stderr, + ].join('\n'), + result.exitCode, + ); + } + return result; +} + +void _expectStatus( + String output, { + required String license, + required bool proFeature, +}) { + final lines = const LineSplitter() + .convert(output.trim()) + .where((line) => line.isNotEmpty) + .toList(); + final expectedFeature = proFeature ? 'enabled' : 'off'; + if (!lines.contains('license:$license') || + !lines.contains('pro-feature:$expectedFeature')) { + throw StateError( + 'Unexpected app status. Expected license:$license and ' + 'pro-feature:$expectedFeature, got:\n$output', + ); + } +} + +String _sha256File(File file) { + final result = Process.runSync('certutil', [ + '-hashfile', + file.path, + 'SHA256', + ]); + if (result.exitCode != 0) { + throw ProcessException( + 'certutil', + ['-hashfile', file.path, 'SHA256'], + result.stderr.toString(), + result.exitCode, + ); + } + final match = RegExp( + r'\b[0-9a-fA-F]{64}\b', + ).firstMatch('${result.stdout}\n${result.stderr}'); + if (match == null) { + throw StateError('Could not parse SHA256 hash for ${file.path}.'); + } + return match.group(0)!.toLowerCase(); +} diff --git a/updater b/updater new file mode 160000 index 0000000..3ac748f --- /dev/null +++ b/updater @@ -0,0 +1 @@ +Subproject commit 3ac748ff28eb4ebb53630c66230b9b1be5df73c2