Create open Shorebird workspace meta-repo

This commit is contained in:
Tony
2026-06-24 03:16:46 +08:00
commit 56f121c517
27 changed files with 1135 additions and 0 deletions
+11
View File
@@ -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
+26
View File
@@ -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/
+28
View File
@@ -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
+55
View File
@@ -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.
Submodule
+1
Submodule dart-sdk-new added at 08139af589
Submodule
+1
Submodule depot_tools added at 90f5d4d14d
+81
View File
@@ -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.
+92
View File
@@ -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` |
Submodule
+1
Submodule flutter added at 9754ce03d7
+5
View File
@@ -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
+5
View File
@@ -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
+69
View File
@@ -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
+49
View File
@@ -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."
+40
View File
@@ -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 <linux|macos>" >&2
exit 64
;;
esac
cat > "$ROOT/.gclient" <<EOF
solutions = [
{
"name": "dart-sdk-new",
"url": "https://git.tonycloud.org/dart-lang/sdk.git",
"deps_file": "DEPS",
"managed": False,
"custom_deps": {},
"custom_vars": {
"dart_root": "dart-sdk-new",
"download_android_deps": False,
"checkout_javascript_engines": False,
"checkout_benchmarks_internal": False,
"checkout_flute": False,
},
},
]
target_os = $TARGET_OS
EOF
echo "Wrote $ROOT/.gclient for $PLATFORM with target_os = $TARGET_OS"
Submodule
+1
Submodule shorebird added at c824005c2d
+1
Submodule shorebird-engine added at e4575c7403
+1
Submodule shorebird-server added at 774954fce7
@@ -0,0 +1,7 @@
import 'package:license_flavor_patch_test/license.dart';
void main() {
final license = currentLicenseType();
print('license:${license.name}');
print(proFeatureEnabled ? 'pro-feature:enabled' : 'pro-feature:off');
}
@@ -0,0 +1,14 @@
enum LicenseType { free, pro, enterprise }
LicenseType currentLicenseType() {
const license = String.fromEnvironment('LICENSE_TYPE', defaultValue: 'free');
return switch (license) {
'pro' => LicenseType.pro,
'enterprise' => LicenseType.enterprise,
_ => LicenseType.free,
};
}
bool get proFeatureEnabled =>
currentLicenseType() == LicenseType.pro ||
currentLicenseType() == LicenseType.enterprise;
@@ -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'),
),
],
),
),
),
);
}
}
@@ -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"
@@ -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
@@ -0,0 +1,5 @@
app_id: license-flavor-patch-test
flavors:
free: license-flavor-free
pro: license-flavor-pro
enterprise: license-flavor-enterprise
@@ -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);
}
@@ -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);
});
}
@@ -0,0 +1,363 @@
import 'dart:convert';
import 'dart:io';
Future<void> 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<String, Object?>();
if (artifactJson['payload_kind'] != 'full-snapshot') {
throw StateError('Expected a directly loadable full-snapshot payload.');
}
final metadata = (artifactJson['metadata'] as Map).cast<String, Object?>();
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<void> _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<String> _runAot(Directory patchBuildDir, File vmcode) async {
final result = await _run('${patchBuildDir.path}/dartaotruntime.exe', [
vmcode.path,
]);
return result.stdout as String;
}
Future<ProcessResult> _runTool(
Directory toolWorkspace,
List<String> 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<ProcessResult> _runToolExpectFailure(
Directory toolWorkspace,
List<String> 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<ProcessResult> _run(
String executable,
List<String> 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();
}
Submodule
+1
Submodule updater added at 3ac748ff28