Compare commits

..

2 Commits

Author SHA1 Message Date
Tony 07a654e771 Default CLI to open infrastructure
ci / 📄 License Check (push) Has been cancelled
ci / ✅ Semantic Pull Request (push) Has been cancelled
ci / 🔤 Check Spelling (push) Has been cancelled
ci / 👀 Detect Changes (push) Has been cancelled
ci / 🎯 Build ${{ matrix.package }} (${{ matrix.os }}) (push) Has been cancelled
ci / 🎯 Build ${{ matrix.package }} (push) Has been cancelled
ci / 🔎 Verify ${{ matrix.package }} (push) Has been cancelled
ci / ci (push) Has been cancelled
Shorebird CI / changes (push) Has been cancelled
Shorebird CI / CSpell (push) Has been cancelled
Shorebird CI / artifact_proxy (push) Has been cancelled
Shorebird CI / dex (push) Has been cancelled
Shorebird CI / discord_gcp_alerts (push) Has been cancelled
Shorebird CI / flutter_version_resolver (push) Has been cancelled
Shorebird CI / jwt (push) Has been cancelled
Shorebird CI / scoped_deps (push) Has been cancelled
Shorebird CI / shorebird_build_trace (push) Has been cancelled
Shorebird CI / shorebird_ci (push) Has been cancelled
Shorebird CI / shorebird_cli (push) Has been cancelled
Shorebird CI / shorebird_code_push_client (push) Has been cancelled
Shorebird CI / shorebird_code_push_protocol (push) Has been cancelled
Shorebird CI / shorebird_redis_client (push) Has been cancelled
Shorebird CI / stripe_api (push) Has been cancelled
Shorebird CI / required (push) Has been cancelled
2026-06-25 20:06:50 +08:00
Tony d9088aeec6 Refactor Shorebird CLI to support configurable URLs and improve test coverage
ci / 📄 License Check (push) Has been cancelled
ci / ✅ Semantic Pull Request (push) Has been cancelled
ci / 🔤 Check Spelling (push) Has been cancelled
ci / 👀 Detect Changes (push) Has been cancelled
ci / 🎯 Build ${{ matrix.package }} (${{ matrix.os }}) (push) Has been cancelled
ci / 🎯 Build ${{ matrix.package }} (push) Has been cancelled
ci / 🔎 Verify ${{ matrix.package }} (push) Has been cancelled
ci / ci (push) Has been cancelled
Deploy Artifact Proxy Dev / ☁️ Artifact Proxy (push) Has been cancelled
Shorebird CI / changes (push) Has been cancelled
Shorebird CI / CSpell (push) Has been cancelled
Shorebird CI / artifact_proxy (push) Has been cancelled
Shorebird CI / dex (push) Has been cancelled
Shorebird CI / discord_gcp_alerts (push) Has been cancelled
Shorebird CI / flutter_version_resolver (push) Has been cancelled
Shorebird CI / jwt (push) Has been cancelled
Shorebird CI / scoped_deps (push) Has been cancelled
Shorebird CI / shorebird_build_trace (push) Has been cancelled
Shorebird CI / shorebird_ci (push) Has been cancelled
Shorebird CI / shorebird_cli (push) Has been cancelled
Shorebird CI / shorebird_code_push_client (push) Has been cancelled
Shorebird CI / shorebird_code_push_protocol (push) Has been cancelled
Shorebird CI / shorebird_redis_client (push) Has been cancelled
Shorebird CI / stripe_api (push) Has been cancelled
Shorebird CI / required (push) Has been cancelled
- Updated AAR releaser test to use ShorebirdProcess.defaultFlutterStorageBaseUrl.
- Changed macOS releaser test to reference a new troubleshooting URL.
- Enhanced shorebird_yaml_test with deserialization for AOT patch metadata.
- Modified network_checker_test to check self-hosted URLs from ShorebirdEnv.
- Added tests for artifact paths in shorebird_artifacts_test.
- Updated shorebird_cli_command_runner_test to reflect new repository URL.
- Improved shorebird_env_test with additional tests for shorebirdRoot and engine revision fallback.
- Adjusted shorebird_flutter_test to use environment variables for Flutter Git URL.
- Updated shorebird_process_test to utilize default Flutter storage URL.
- Enhanced shorebird_validator_test to link to the default hosted URL.
- Updated shorebird_web_console_test to use the configured hosted URL.
- Refactored code_push_client to use default hosted URL for API requests.
- Updated README and generation scripts for the code_push_protocol package to reflect new OpenAPI spec source.
- Modified shared.sh to allow configurable Flutter repository and storage URLs.

Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-06-25 17:21:56 +08:00
93 changed files with 2765 additions and 416 deletions
+82 -69
View File
@@ -1,98 +1,111 @@
# Open Source Replacement Audit
# Open Replacement Audit
Last verified: 2026-06-22.
Last verified: 2026-06-25.
This workspace is intended to build and operate without depending on
closed-source Shorebird components. The audit below distinguishes public
upstream Shorebird code from hosted Shorebird services that still need local
open replacements.
This workspace is intended to build and operate without closed Shorebird
services or closed SDK artifacts. Local forks/submodules are the source of truth:
do not clone official Shorebird infrastructure repositories as a bootstrap
default.
## Verified Public Shorebird Components
## Local Sources
The public GitHub organization at `https://github.com/shorebirdtech` lists the
following relevant repositories:
The combined workspace provides these open replacements:
- `shorebirdtech/shorebird`: public CLI, CodePush client, protocol models,
artifact proxy, and related tooling.
- `shorebirdtech/updater`: public Rust updater library and patch tooling. The
repository describes `library` as the runtime library linked into Flutter
Engine and `patch` as the developer patch packaging tool.
- `shorebirdtech/flutter`: public Flutter fork.
- `shorebirdtech/engine` and `shorebirdtech/buildroot`: public archived engine
build repositories.
| Surface | Open replacement in this workspace |
| --- | --- |
| Shorebird CLI | `packages/shorebird_cli`, configured for the self-hosted server and open artifact mirror |
| CodePush client/protocol | `packages/shorebird_code_push_client` and `packages/shorebird_code_push_protocol` |
| CodePush API/auth/console | `../shorebird-server` |
| Artifact mirror/proxy | `packages/artifact_proxy` |
| Runtime updater library | `../updater/library`, linked into the Flutter engine by `../scripts/sync_open_sources.sh` |
| Patch artifact generator | `../updater/patch` and `packages/open_aot_patch_tools` |
| Flutter engine and Flutter tool defaults | `../flutter`, linked to `../dart-sdk-new` and `../updater` |
| Dart VM patch runtime | `../dart-sdk-new` |
Conclusion: the runtime updater is not a closed-source component. The correct
open-source path is to use the public `shorebirdtech/updater` repository at the
engine location expected by the GN build:
Run source linking from the workspace root:
```sh
./scripts/sync_open_sources.sh
```
The script links:
```text
../flutter/engine/src/flutter/third_party/updater
flutter/engine/src/flutter/third_party/dart -> dart-sdk-new
flutter/engine/src/flutter/third_party/updater -> updater
```
Run from this directory:
Set `UPDATER_SRC` explicitly only when testing another updater fork. The default
path must remain the local `updater` submodule.
```powershell
.\sync_open_sources.ps1
```
## Hosted Surfaces
or manually from the workspace root:
The open CLI, CodePush client, updater, artifact proxy, and Flutter tool defaults
are configured to use local/self-hosted origins when no override is supplied:
```powershell
git clone https://github.com/shorebirdtech/updater.git `
flutter\engine\src\flutter\third_party\updater
```
| Hosted surface | Open default |
| --- | --- |
| API/auth server | `http://localhost:8080` |
| OpenAPI contract | `http://localhost:8080/openapi.yaml` and `../shorebird-server/internal/api/handlers/openapi.yaml` |
| CLI patch-tool artifact mirror | `http://localhost:8080/artifacts` |
| Flutter engine/Dart SDK/Maven artifact mirror | `http://localhost:8080/download.flutter.io` |
| Web console | self-hosted `shorebird-server/web` dashboard |
## Components Replaced In This Checkout
Override these values with `SHOREBIRD_HOSTED_URL`,
`SHOREBIRD_ARTIFACT_BASE_URL`, `SHOREBIRD_FLUTTER_STORAGE_BASE_URL`, and
`SHOREBIRD_FLUTTER_GIT_URL` when pointing the CLI at a public mirror.
Full SDK CI runs upload `open-shorebird-artifact-mirror`, which is assembled
from the produced patch-tool, metadata, engine, and web artifacts. They also
upload `open-shorebird-release-manifest`, which validates checksum sidecars and
records provenance for the CLI, server, SDK, engine, and mirror archives. After
downloading CI artifacts manually, run the root
`scripts/assemble_artifact_mirror.sh` helper to reproduce that merge, validate
`artifacts_manifest.yaml`, verify platform `patch-*.zip` tools, and write
missing checksum sidecars before publishing the directory behind
`SHOREBIRD_ARTIFACT_BASE_URL`.
| Shorebird surface | Public upstream status | Open replacement in this checkout |
| --- | --- | --- |
| Runtime updater linked into Flutter engine | Public: `shorebirdtech/updater` | Use public updater via `sync_open_sources.ps1`; engine wrapper remains at `../flutter/engine/src/flutter/shell/common/shorebird` |
| Patch artifact generator | Public upstream has `shorebirdtech/updater/patch`; this checkout also needs compact encrypted AOT tooling | `packages/open_aot_patch_tools` |
| Hosted CodePush API (`api.shorebird.dev`) | Hosted service, no public server implementation found in the public org audit | `../shorebird-server` |
| Hosted auth service (`auth.shorebird.dev`) | Hosted service | `../shorebird-server` `/auth/*` endpoints |
| Hosted web console (`console.shorebird.dev`) | Hosted service | `../shorebird-server/web` dashboard |
| Artifact proxy for Flutter artifacts | Public in `shorebirdtech/shorebird` | `packages/artifact_proxy` |
| CLI and CodePush protocol/client | Public in `shorebirdtech/shorebird` | `packages/shorebird_cli`, `shorebird_code_push_client`, and `shorebird_code_push_protocol` |
## Platform Patch Routes
## Self-Hosted Operation
| Platform | Patch route |
| --- | --- |
| iOS | Encrypted Dart bytecode interpreter artifact with `DART_DYNAMIC_MODULES=false`; does not load downloaded native executable text |
| Android | Native AOT patch runtime using the public updater `patch` binary and a patch-capable Android engine |
| macOS/Linux | Native AOT patch runtime using the public updater `patch` binary and patch-capable desktop engines |
| Windows | Public updater `patch` binary; runtime verification remains platform-runner dependent |
| Web | SDK artifact build only; not a Shorebird CodePush release platform in this CLI/protocol |
Start the open server:
The iOS App Store candidate route is interpreter-based. Do not reintroduce
`DART_DYNAMIC_MODULES`, `aot-tools.dill` publishing, or a native-AOT iOS patch
route as the default.
```powershell
cd ..\shorebird-server
go run ./cmd/server
```
## CI Contract
Point the CLI at it:
The root workflow `.github/workflows/open-shorebird-ci.yml` builds:
```powershell
$env:SHOREBIRD_HOSTED_URL = "http://localhost:8080"
$env:AUTH_SERVICE_URL = "http://localhost:8080/auth"
$env:SHOREBIRD_TOKEN = "<jwt-token-from-local-server>"
```
- CLI archives containing `shorebird`, `open_aot_patch_tools`, and
`artifact_proxy`
- public updater `patch-*.zip` mirror artifacts
- self-hosted `shorebird-server` binaries
- custom Dart SDK artifacts
- Linux, Android, web, iOS, and macOS engine/SDK artifacts on manual
`full_sdk_build=true` runs
Point devices at it through `shorebird.yaml`:
```yaml
app_id: <app-uuid>
base_url: http://localhost:8080
auto_update: true
```
The local server implements the Shorebird-compatible management and device
patch-check surfaces, including limited offline-license expiry metadata.
`../scripts/verify_ci_workflow.sh` is the executable contract for this audit. It
rejects closed hosted defaults, `DART_DYNAMIC_MODULES=true`, legacy
`aot-tools.dill` publishing, missing checksum sidecars, missing source tests, and
missing required artifact jobs.
## Security Boundary
The public updater, local server, and open patch tools must still enforce:
The open updater, local server, CLI, and patch tools must enforce:
- patch metadata compatibility: app id, release/build id, platform, arch,
SDK hash, base snapshot hash, flavor id, and license type
- cryptographic integrity: hash pinning or signing
- AES-GCM delivery confidentiality and tamper detection
- cryptographic integrity through hash pinning/signing where supported
- AES-GCM delivery confidentiality and tamper detection for encrypted interpreter
payloads
- limited offline-license expiry through `offline_expires_at`
Do not replace public upstream source with a new implementation unless the
public repository becomes unavailable or its license changes. Replacing a
public runtime updater with an incompatible clone increases risk and makes iOS
and Android behavior diverge from the engine integration expected by Shorebird.
Open replacement work should preserve Shorebird-compatible protocol shapes where
the CLI/updater expect them, while keeping service origins, SDK builds, and patch
tools controlled by this workspace.
+10 -15
View File
@@ -1,18 +1,14 @@
## Shorebird 🐦
## Shorebird
[![Discord](https://img.shields.io/discord/1030243211995791380?style=for-the-badge&logo=discord&color=blue)](https://discord.gg/shorebird)
<a href="https://www.producthunt.com/posts/shorebird-code-push?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-shorebird&#0045;code&#0045;push" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=449946&theme=neutral" alt="Shorebird&#0032;Code&#0032;Push - Flutter&#0032;over&#0032;the&#0032;air&#0032;updates | Product Hunt" style="width: 128px; height: 27px;" width="128" height="27" /></a>
[![shorebird ci](https://api.shorebird.dev/api/v1/github/shorebirdtech/shorebird/badge.svg)](https://console.shorebird.dev/ci)
[![ci](https://github.com/shorebirdtech/shorebird/actions/workflows/main.yaml/badge.svg)](https://github.com/shorebirdtech/shorebird/actions/workflows/main.yaml)
[![e2e](https://github.com/shorebirdtech/shorebird/actions/workflows/e2e.yaml/badge.svg)](https://github.com/shorebirdtech/shorebird/actions/workflows/e2e.yaml)
[![codecov](https://codecov.io/gh/shorebirdtech/shorebird/branch/main/graph/badge.svg)](https://codecov.io/gh/shorebirdtech/shorebird)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE-MIT)
[![License: Apache](https://img.shields.io/badge/license-Apache-orange.svg)](./LICENSE-APACHE)
## Getting Started
Visit https://docs.shorebird.dev to get started.
This fork is wired for the open workspace CI and self-hosted services described
in [OPEN_SOURCE_REPLACEMENTS.md](OPEN_SOURCE_REPLACEMENTS.md). The root
workspace workflow, `.github/workflows/open-shorebird-ci.yml`, builds the open
CLI, server, updater patch tools, custom Dart SDK, and engine artifacts.
## Packages
@@ -35,8 +31,8 @@ For more information, please refer to the documentation for each package.
## Contributing
If you're interested in contributing, please join us on
[Discord](https://discord.gg/shorebird).
If you're interested in contributing, use this fork's issue tracker or the root
workspace documentation.
### Environment setup
@@ -79,7 +75,8 @@ The following command will generate a coverage report for the Dart packages:
dart test --coverage=coverage && dart pub global run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --packages=.dart_tool/package_config.json --check-ignore
```
Coverage reports are uploaded to [Codecov](https://app.codecov.io/gh/shorebirdtech/shorebird).
Coverage reports are generated locally under `coverage/`. Configure upload for
your own mirror or CI account when publishing this fork.
## License
@@ -87,6 +84,4 @@ Shorebird projects are licensed for use under either Apache License, Version 2.0
(LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) MIT license
(LICENSE-MIT or http://opensource.org/licenses/MIT) at your option.
See our license philosophy for more information on why we license files this
way:
https://handbook.shorebird.dev/engineering/#licensing-philosophy
See the Apache and MIT license files in this repository for the license terms.
+25 -5
View File
@@ -15,6 +15,8 @@ $flutterPath = [IO.Path]::Combine($shorebirdCacheDir, "flutter", $flutterVersion
$flutter = [IO.Path]::Combine($shorebirdCacheDir, "flutter", $flutterVersion, "bin", "flutter.bat")
$shorebirdScript = [IO.Path]::Combine($shorebirdCliDir, "bin", "shorebird.dart")
$dart = [IO.Path]::Combine($flutterPath, "bin", "cache", "dart-sdk", "bin", "dart.exe")
$defaultFlutterGitUrl = "https://git.tonycloud.org/flutter/flutter.git"
$defaultFlutterStorageBaseUrl = "http://localhost:8080/download.flutter.io"
# Executes $command and redirects as much output to $null as possible.
#
@@ -105,10 +107,21 @@ function Test-ShorebirdNeedsUpdate {
function Update-Flutter {
Write-Output "Updating Flutter..."
$flutterGitUrl = $env:SHOREBIRD_FLUTTER_GIT_URL
if ([string]::IsNullOrWhiteSpace($flutterGitUrl)) {
$flutterGitUrl = $defaultFlutterGitUrl
}
$flutterStorageBaseUrl = $env:SHOREBIRD_FLUTTER_STORAGE_BASE_URL
if ([string]::IsNullOrWhiteSpace($flutterStorageBaseUrl)) {
$flutterStorageBaseUrl = $env:FLUTTER_STORAGE_BASE_URL
}
if ([string]::IsNullOrWhiteSpace($flutterStorageBaseUrl)) {
$flutterStorageBaseUrl = $defaultFlutterStorageBaseUrl
}
if (!(Test-Path $flutterPath)) {
Invoke-SilentlyIfNeeded {
git clone --filter=tree:0 https://github.com/shorebirdtech/flutter.git --no-checkout "$flutterPath"
git clone --filter=tree:0 "$flutterGitUrl" --no-checkout "$flutterPath"
}
}
else {
@@ -122,11 +135,18 @@ function Update-Flutter {
git -C "$flutterPath" -c advice.detachedHead=false checkout "$flutterVersion"
}
# Set FLUTTER_STORAGE_BASE_URL=https://download.shorebird.dev and execute
# a `flutter` command to trigger a download of Dart, etc.
$env:FLUTTER_STORAGE_BASE_URL = 'https://download.shorebird.dev';
# Set FLUTTER_STORAGE_BASE_URL and execute a `flutter` command to trigger
# a download of Dart, etc. through the configured open mirror.
$hadFlutterStorageBaseUrl = Test-Path Env:\FLUTTER_STORAGE_BASE_URL
$previousFlutterStorageBaseUrl = $env:FLUTTER_STORAGE_BASE_URL
$env:FLUTTER_STORAGE_BASE_URL = $flutterStorageBaseUrl;
& $flutter --version
Remove-Item Env:\FLUTTER_STORAGE_BASE_URL
if ($hadFlutterStorageBaseUrl) {
$env:FLUTTER_STORAGE_BASE_URL = $previousFlutterStorageBaseUrl
}
else {
Remove-Item Env:\FLUTTER_STORAGE_BASE_URL -ErrorAction SilentlyContinue
}
}
function Update-Shorebird {
+14
View File
@@ -0,0 +1,14 @@
# API Keys
This open Shorebird workspace is configured for a self-hosted server by
default. Create and manage API keys in that server's console, then pass the
token to the CLI with the `SHOREBIRD_TOKEN` environment variable.
Default local services:
- API: `http://localhost:8080`
- Auth: `http://localhost:8080/auth`
- Console: `http://localhost:8080`
The CLI also accepts `SHOREBIRD_HOSTED_URL`, `SHOREBIRD_AUTH_URL`, and
`SHOREBIRD_TOKEN` overrides for non-local deployments.
+32
View File
@@ -0,0 +1,32 @@
# Code Push Troubleshooting
The open replacement uses a self-hosted server and a local artifact mirror by
default. Check these values first when release or patch commands fail:
- `SHOREBIRD_HOSTED_URL`, default `http://localhost:8080`
- `SHOREBIRD_AUTH_URL`, default `http://localhost:8080/auth`
- `SHOREBIRD_ARTIFACT_BASE_URL`, default `http://localhost:8080/artifacts`
- `SHOREBIRD_FLUTTER_STORAGE_BASE_URL`, default `http://localhost:8080/download.flutter.io`
## Unsupported Class File Major Version 65
Use the Java version expected by the Flutter and Android Gradle Plugin version
in your project. If the Gradle build fails outside Shorebird, resolve that
plain `flutter build` failure before retrying the Shorebird command.
## Can't Run My App In VS Code
Run the app with the generated Flutter SDK and engine artifacts from this
workspace. Mixing a release built by this workspace with a stock Flutter tool
can make debug and launch behavior confusing.
## Your App Contains Asset Changes Warning When Creating A Patch
Asset changes are not part of a Dart code patch. Create a new release when
assets, native resources, icons, or bundled files change.
## Your App Contains Native Changes Warning When Creating A Patch Even Though You Haven't Changed Swiftobjective Ckotlinjava Code
Native build outputs can change when project files, plugins, build settings,
or generated native artifacts change. Compare the release and patch build
inputs, then create a new release if native outputs are expected to differ.
@@ -0,0 +1,26 @@
# Flutter Versions
This workspace builds against the open Flutter fork configured in
`SHOREBIRD_FLUTTER_GIT_URL`, which defaults to:
```text
https://git.tonycloud.org/flutter/flutter.git
```
Use `shorebird flutter versions list` to inspect versions known to the local
artifact mirror. Prefer passing `--flutter-version` explicitly to release
commands so releases and patches resolve the same engine artifacts.
## Supported Flutter Versions
Supported versions are the versions for which this workspace has built and
published matching engine artifacts into the configured artifact mirror. The
default mirror is:
```text
http://localhost:8080/artifacts
```
If a version is missing, build the matching custom engine artifacts in CI and
publish the generated mirror metadata before creating releases for that
version.
+50 -70
View File
@@ -4,22 +4,30 @@ This is a tool for proxying Flutter artifacts from a derived Flutter engine
revision back to the base Flutter engine revision. This is useful for
when you need to modify _some_ of the Flutter artifacts but not all of them.
This is a development tool which map requests to Google
Storage (either Shorebird's bucket or the official Flutter buckets).
This is a development tool which maps requests to configurable artifact origins.
By default, Flutter artifacts are redirected to the public Flutter storage
origin and Shorebird-specific artifacts are redirected to the local open mirror
root at `http://localhost:8080/artifacts`.
## Usage
Uses `config.dart` to configure the engine revisions and artifact overrides.
Uses `config.dart` to configure recognized artifact URL patterns. Runtime
origins are configured with environment variables:
- `SHOREBIRD_ARTIFACT_BASE_URL`: root for open Shorebird manifests and
artifacts, defaulting to `http://localhost:8080/artifacts`
- `ARTIFACT_PROXY_FLUTTER_BASE_URL`: root for upstream Flutter artifacts,
defaulting to `https://storage.googleapis.com`
```bash
# Run locally with hot-reload enabled.
DEV=true dart --enable-vm-service run bin/server.dart
PORT=8081 DEV=true dart --enable-vm-service run bin/server.dart
```
And then in a separate terminal:
```
FLUTTER_STORAGE_BASE_URL=http://localhost:8080 flutter precache -a
FLUTTER_STORAGE_BASE_URL=http://localhost:8081 flutter precache -a
```
You should use a separate checkout of Flutter when running this, so you don't
@@ -27,84 +35,56 @@ poison the cache of your main Flutter checkout.
## Updating config.dart
If run into 404s when fetching artifacts, ensure that the expected manifest
exists at
https://storage.googleapis.com/download.shorebird.dev/shorebird/$engineRevision/artifacts_manifest.yaml.
If you run into 404s when fetching artifacts, ensure that the expected manifest
exists at:
```text
$SHOREBIRD_ARTIFACT_BASE_URL/shorebird/$engineRevision/artifacts_manifest.yaml
```
If it does, you may need to update the artifact list in `config.dart` and, if
the artifact is one we're providing, add it in `tool/generate_manifest.sh`
the artifact is one we're providing, add it in
`../../../scripts/write_artifact_manifest.py`.
To do so, you will need to determine the artifact URLs. Follow these steps:
To do so, point Flutter at this proxy with `FLUTTER_STORAGE_BASE_URL`, run a
Shorebird/Flutter command that downloads the missing artifact, then add the
observed URL pattern to `packages/artifact_proxy/lib/config.dart`.
- Adjust shorebird_cli to point to http://localhost:8080 instead of https://download.shorebird.dev:
- packages\shorebird_cli\lib\src\shorebird_process.dart
```diff
Map<String, String> _environmentOverrides({
required String executable,
}) {
if (executable == 'flutter') {
// If this ever changes we also need to update the `shorebird` shell
// wrapper which downloads runs Flutter to fetch artifacts the first time.
- return {'FLUTTER_STORAGE_BASE_URL': 'https://download.shorebird.dev'};
+ return {'FLUTTER_STORAGE_BASE_URL': 'http://localhost:8080'};
}
return {};
}
```
- Adjust third_party Flutter to point to http://localhost:8080 instead of https://download.shorebird.dev:
- third_party\flutter\bin\internal\shared.sh
```diff
# Either clones or pulls the Shorebird Flutter repository, depending on whether FLUTTER_PATH exists.
function update_flutter {
if [[ -d "$FLUTTER_PATH" ]]; then
git -C "$FLUTTER_PATH" fetch
else
git clone --filter=tree:0 https://github.com/shorebirdtech/flutter.git --no-checkout "$FLUTTER_PATH"
fi
# -c to avoid printing a warning about being in a detached head state.
git -C "$FLUTTER_PATH" -c advice.detachedHead=false checkout "$FLUTTER_VERSION"
SHOREBIRD_ENGINE_VERSION=`cat "$FLUTTER_PATH/bin/internal/engine.version"`
echo "Shorebird Engine • revision $SHOREBIRD_ENGINE_VERSION"
# Install Shorebird Flutter Artifacts
- FLUTTER_STORAGE_BASE_URL=https://download.shorebird.dev $FLUTTER_PATH/bin/flutter --version
+ FLUTTER_STORAGE_BASE_URL=http://localhost:8080 $FLUTTER_PATH/bin/flutter --version
}
```
- Modify flutter_tool used by Shorebird to allow downloads from insecure URLs:
- shorebird\bin\cache\flutter\packages\flutter_tools\gradle\flutter.gradle
```diff
rootProject.allprojects {
repositories {
maven {
url repository
+ allowInsecureProtocol true
}
}
}
```
- Remove the flutter_tools snapshot
If you changed Flutter tool code while debugging, remove the flutter_tools
snapshot:
```bash
cd bin/cache/flutter/bin/cache
rm flutter_tools.s*
```
- Run a shorebird command (`shorebird run` works well)
- For each artifact that 404s, add a line to `packages\artifact_proxy\lib\config.dart`, following the conventions for capturing engine revisions and escaping relevant characters.
- Run a Shorebird command (`shorebird run` works well).
- For each artifact that 404s, add a line to
`packages/artifact_proxy/lib/config.dart`, following the conventions for
capturing engine revisions and escaping relevant characters.
## Generating an `artifact_manifest.yaml`
## Generating an `artifacts_manifest.yaml`
To generate a new `artifact_manifest.yaml` for a specific flutter_revision use the following command:
To generate a new `artifacts_manifest.yaml` for a specific Flutter engine
revision, use the workspace helper:
```
./tools/generate_manifest.sh <flutter_engine_revision> > artifact_manifest.yaml
../../../scripts/write_artifact_manifest.py \
--flutter-engine-revision <flutter_engine_revision> \
--output artifacts_manifest.yaml
```
Then upload the `artifact_manifest.yaml` to `download.shorebird.dev/shorebird/<shorebird_engine_revision>/artifacts_manifest.yaml`
`--flutter-engine-revision` is the upstream Flutter engine revision used for
unchanged artifacts. Upload the generated file under the custom Shorebird engine
revision path shown below.
Then upload the `artifacts_manifest.yaml` to:
```text
$SHOREBIRD_ARTIFACT_BASE_URL/shorebird/<shorebird_engine_revision>/artifacts_manifest.yaml
```
The GitHub CI engine artifacts include a `mirror/` subtree for the override
files listed by this manifest. Copy the contents of that subtree to
`$SHOREBIRD_ARTIFACT_BASE_URL` alongside the manifest and `patch-*.zip`
artifacts.
+35 -4
View File
@@ -5,13 +5,38 @@ import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_hotreload/shelf_hotreload.dart';
Future<void> main() async {
Future<void> main(List<String> args) async {
final isDev = Platform.environment['DEV'] == 'true';
final client = ArtifactManifestClient();
final handler = artifactProxyHandler(client: client);
final ip = InternetAddress.anyIPv6;
final shorebirdArtifactBaseUri = _uriFromEnvironment(
'SHOREBIRD_ARTIFACT_BASE_URL',
ArtifactManifestClient.defaultManifestBaseUri,
);
final flutterArtifactBaseUri = _uriFromEnvironment(
'ARTIFACT_PROXY_FLUTTER_BASE_URL',
defaultFlutterArtifactBaseUri,
);
final port = int.parse(Platform.environment['PORT'] ?? '8080');
if (args.contains('--health-check')) {
stdout.writeln(
'artifact_proxy ok '
'shorebird_artifacts=$shorebirdArtifactBaseUri '
'flutter_artifacts=$flutterArtifactBaseUri '
'port=$port',
);
return;
}
final client = ArtifactManifestClient(
manifestBaseUri: shorebirdArtifactBaseUri,
);
final handler = artifactProxyHandler(
client: client,
flutterArtifactBaseUri: flutterArtifactBaseUri,
shorebirdArtifactBaseUri: shorebirdArtifactBaseUri,
);
final ip = InternetAddress.anyIPv6;
// Hot-reload is enabled when the DEBUG environment variable is set to true.
if (isDev) return withHotreload(() => serve(handler, ip, port));
@@ -25,3 +50,9 @@ Future<HttpServer> serve(Handler proxy, InternetAddress ip, int port) async {
server.autoCompress = true;
return server;
}
Uri _uriFromEnvironment(String name, Uri defaultValue) {
final value = Platform.environment[name];
if (value == null || value.trim().isEmpty) return defaultValue;
return Uri.parse(value.trim());
}
+5 -3
View File
@@ -17,6 +17,7 @@ final engineArtifactPatterns = {
r'flutter_infra_release\/flutter\/(.*)\/linux-x64\/font-subset\.zip',
r'flutter_infra_release\/flutter\/(.*)\/linux-x64\/artifacts\.zip',
r'flutter_infra_release\/flutter\/(.*)\/linux-x64-release\/linux-x64-flutter-gtk\.zip',
r'flutter_infra_release\/flutter\/(.*)\/linux-x64-release\/artifacts\.zip',
r'flutter_infra_release\/flutter\/(.*)\/linux-x64-profile\/linux-x64-flutter-gtk\.zip',
r'flutter_infra_release\/flutter\/(.*)\/linux-x64-debug\/linux-x64-flutter-gtk\.zip',
r'flutter_infra_release\/flutter\/(.*)\/linux-arm64\/linux-arm64-flutter-gtk\.zip',
@@ -38,11 +39,11 @@ final engineArtifactPatterns = {
r'flutter_infra_release\/flutter\/(.*)\/darwin-x64\/framework\.zip',
r'flutter_infra_release\/flutter\/(.*)\/darwin-x64\/gen_snapshot\.zip',
r'flutter_infra_release\/flutter\/(.*)\/darwin-x64\/font-subset\.zip',
r'flutter_infra_release\/flutter\/(.*)\/darwin-x64\/FlutterMacOS.framework\.zip',
r'flutter_infra_release\/flutter\/(.*)\/darwin-x64\/FlutterMacOS\.framework\.zip',
r'flutter_infra_release\/flutter\/(.*)\/darwin-x64\/FlutterMacOS\.framework\.dSYM\.zip',
r'flutter_infra_release\/flutter\/(.*)\/darwin-x64\/artifacts\.zip',
r'flutter_infra_release\/flutter\/(.*)\/darwin-x64-release\/FlutterMacOS.framework\.zip',
r'flutter_infra_release\/flutter\/(.*)\/darwin-x64-profile\/FlutterMacOS.framework\.zip',
r'flutter_infra_release\/flutter\/(.*)\/darwin-x64-release\/FlutterMacOS\.framework\.zip',
r'flutter_infra_release\/flutter\/(.*)\/darwin-x64-profile\/FlutterMacOS\.framework\.zip',
r'flutter_infra_release\/flutter\/(.*)\/darwin-x64-profile\/artifacts\.zip',
r'flutter_infra_release\/flutter\/(.*)\/darwin-x64-profile\/framework\.zip',
r'flutter_infra_release\/flutter\/(.*)\/darwin-x64-profile\/gen_snapshot\.zip',
@@ -51,6 +52,7 @@ final engineArtifactPatterns = {
r'flutter_infra_release\/flutter\/(.*)\/darwin-x64-release\/gen_snapshot\.zip',
r'flutter_infra_release\/flutter\/(.*)\/darwin-arm64\/font-subset\.zip',
r'flutter_infra_release\/flutter\/(.*)\/darwin-arm64\/artifacts\.zip',
r'flutter_infra_release\/flutter\/(.*)\/darwin-arm64-release\/FlutterMacOS\.framework\.zip',
r'flutter_infra_release\/flutter\/(.*)\/dart-sdk-windows-x64\.zip',
r'flutter_infra_release\/flutter\/(.*)\/dart-sdk-linux-x64\.zip',
r'flutter_infra_release\/flutter\/(.*)\/dart-sdk-linux-arm64\.zip',
@@ -6,20 +6,26 @@ import 'package:http/http.dart' as http;
import 'package:quiver/collection.dart';
/// {@template artifact_manifest_client}
/// A client that fetches [ArtifactsManifest]s from the shorebird storage bucket
/// and caches them in memory.
/// A client that fetches [ArtifactsManifest]s from an open artifact mirror and
/// caches them in memory.
/// {@endtemplate}
class ArtifactManifestClient {
/// {@macro artifact_manifest_client}
ArtifactManifestClient({http.Client? httpClient})
: _httpClient = httpClient ?? http.Client();
ArtifactManifestClient({http.Client? httpClient, Uri? manifestBaseUri})
: _httpClient = httpClient ?? http.Client(),
_manifestBaseUri = manifestBaseUri ?? defaultManifestBaseUri;
/// Default root for open self-hosted artifact manifests.
static final defaultManifestBaseUri = Uri.parse(
'http://localhost:8080/artifacts',
);
final http.Client _httpClient;
final Uri _manifestBaseUri;
final _cache = LruMap<String, ArtifactsManifest>(maximumSize: 1000);
/// Fetches the [ArtifactsManifest] for the provided [revision] from the
/// shorebird storage bucket.
/// Fetches the [ArtifactsManifest] for the provided [revision].
Future<ArtifactsManifest> getManifest(String revision) async {
if (_cache.containsKey(revision)) return _cache[revision]!;
final manifest = await _fetchManifest(revision);
@@ -28,8 +34,9 @@ class ArtifactManifestClient {
}
Future<ArtifactsManifest> _fetchManifest(String revision) async {
final url = Uri.parse(
'https://storage.googleapis.com/download.shorebird.dev/shorebird/$revision/artifacts_manifest.yaml',
final url = _joinUri(
_manifestBaseUri,
'shorebird/$revision/artifacts_manifest.yaml',
);
final response = await _httpClient.get(url);
if (response.statusCode != HttpStatus.ok) {
@@ -44,3 +51,11 @@ ${response.statusCode} ${response.reasonPhrase}''');
);
}
}
Uri _joinUri(Uri base, String path) {
final basePath = base.path.endsWith('/')
? base.path.substring(0, base.path.length - 1)
: base.path;
final relativePath = path.startsWith('/') ? path.substring(1) : path;
return base.replace(path: '$basePath/$relativePath');
}
@@ -13,31 +13,38 @@ const String _explainerHtml = """
<body>
<p>
This server proxies requests for Flutter artifacts to the correct location,
depending on the engine revision. Most artifacts are served from the standard
`download.flutter.io` location, but a few artifacts are served from Shorebird's
storage bucket to add support for code push.
depending on the engine revision. Most artifacts are served from the configured
Flutter artifact origin, but a few artifacts are served from the configured
open Shorebird artifact mirror to add support for code push.
</p>
<p>
See <a href='https://docs.shorebird.dev/architecture'>
https://docs.shorebird.dev/architecture</a> for more information.
See the workspace README and docs/CI.md for open artifact mirror details.
</p>
<p>
Source code can be found here:
<a
href='https://github.com/shorebirdtech/shorebird/tree/main/packages/artifact_proxy'>
https://github.com/shorebirdtech/shorebird/tree/main/packages/artifact_proxy</a>
href='https://git.tonycloud.org/flutter/shorebird'>
https://git.tonycloud.org/flutter/shorebird</a>
</p>
<p>
If you're seeing problems with your Shorebird install, or are interested in
replicating this proxy, please reach out to us over Discord.
<a href='https://shorebird.dev/contact'>https://shorebird.dev/contact</a>
If you're seeing problems with this open artifact proxy, file an issue against
the repository that hosts this workspace.
</p>
</body>
</html>
""";
/// A [Handler] that proxies artifact requests to the correct location.
Handler artifactProxyHandler({required ArtifactManifestClient client}) {
Handler artifactProxyHandler({
required ArtifactManifestClient client,
Uri? flutterArtifactBaseUri,
Uri? shorebirdArtifactBaseUri,
}) {
final flutterBaseUri =
flutterArtifactBaseUri ?? defaultFlutterArtifactBaseUri;
final shorebirdBaseUri =
shorebirdArtifactBaseUri ?? defaultShorebirdArtifactBaseUri;
return (Request request) async {
final path = request.url.path;
if (path.isEmpty) {
@@ -81,6 +88,7 @@ Handler artifactProxyHandler({required ArtifactManifestClient client}) {
artifactPath: normalizedPath,
engine: shorebirdEngineRevision,
bucket: manifest.storageBucket,
baseUri: shorebirdBaseUri,
);
print('Shorebird engine artifact detected, forwarding to: $location');
return Response.found(location);
@@ -89,6 +97,7 @@ Handler artifactProxyHandler({required ArtifactManifestClient client}) {
final location = getFlutterArtifactLocation(
artifactPath: normalizedPath,
engine: manifest.flutterEngineRevision,
baseUri: flutterBaseUri,
);
print('Flutter artifact detected, forwarding to: $location');
return Response.found(location);
@@ -102,23 +111,40 @@ Handler artifactProxyHandler({required ArtifactManifestClient client}) {
return Response.notFound('Unrecognized artifact path: $path');
}
final location = getFlutterArtifactLocation(artifactPath: path);
final location = getFlutterArtifactLocation(
artifactPath: path,
baseUri: flutterBaseUri,
);
print('Flutter artifact detected, forwarding to: $location');
return Response.found(location);
};
}
/// Default public Flutter artifact origin.
final defaultFlutterArtifactBaseUri = Uri.parse(
'https://storage.googleapis.com',
);
/// Default root for open self-hosted Shorebird artifacts.
final defaultShorebirdArtifactBaseUri = Uri.parse(
'http://localhost:8080/artifacts',
);
/// Returns the location of the artifact at [artifactPath] using the
/// specified [engine] revision for original Flutter artifacts.
String getFlutterArtifactLocation({
required String artifactPath,
String? engine,
Uri? baseUri,
}) {
final adjustedPath = engine != null
? artifactPath.replaceAll(r'$engine', engine)
: artifactPath;
return 'https://storage.googleapis.com/$adjustedPath';
return _joinUri(
baseUri ?? defaultFlutterArtifactBaseUri,
adjustedPath,
).toString();
}
/// Returns the location of the artifact at [artifactPath] using the
@@ -127,7 +153,23 @@ String getShorebirdArtifactLocation({
required String artifactPath,
required String engine,
required String bucket,
Uri? baseUri,
}) {
final adjustedPath = artifactPath.replaceAll(r'$engine', engine);
return 'https://storage.googleapis.com/$bucket/$adjustedPath';
final bucketUri = Uri.tryParse(bucket);
final resolvedBaseUri = bucketUri != null && bucketUri.hasScheme
? bucketUri
: baseUri ?? defaultShorebirdArtifactBaseUri;
final path = bucketUri != null && bucketUri.hasScheme
? adjustedPath
: [if (bucket.isNotEmpty) bucket, adjustedPath].join('/');
return _joinUri(resolvedBaseUri, path).toString();
}
Uri _joinUri(Uri base, String path) {
final basePath = base.path.endsWith('/')
? base.path.substring(0, base.path.length - 1)
: base.path;
final relativePath = path.startsWith('/') ? path.substring(1) : path;
return base.replace(path: '$basePath/$relativePath');
}
@@ -10,7 +10,7 @@ part 'artifacts_manifest.g.dart';
///
/// ```yaml
/// flutter_engine_revision: ec975089acb540fc60752606a3d3ba809dd1528b
/// storage_bucket: download.shorebird.dev
/// storage_bucket: shorebird
/// artifact_overrides:
/// # artifacts.zip
/// - flutter_infra_release/flutter/$engine/android-arm-64-release/artifacts.zip
+1 -1
View File
@@ -1,7 +1,7 @@
name: artifact_proxy
description: Intercept and proxy Flutter artifact requests.
version: 1.0.0
repository: https://github.com/shorebirdtech/shorebird/
repository: https://git.tonycloud.org/flutter/shorebird-workspace
publish_to: "none"
resolution: workspace
@@ -12,10 +12,16 @@ void main() {
const shorebirdEngineRevision = '8b89f8bd9fc6982aa9c4557fd0e5e89db1ff9986';
const manifest = ArtifactsManifest(
flutterEngineRevision: 'ec975089acb540fc60752606a3d3ba809dd1528b',
storageBucket: 'download.shorebird.dev',
storageBucket: 'shorebird',
artifactOverrides: {
r'flutter_infra_release/flutter/$engine/android-arm64-release/artifacts.zip',
r'flutter_infra_release/flutter/$engine/android-arm64-release/symbols.zip',
r'flutter_infra_release/flutter/$engine/linux-x64-release/artifacts.zip',
r'flutter_infra_release/flutter/$engine/linux-x64-release/linux-x64-flutter-gtk.zip',
r'flutter_infra_release/flutter/$engine/ios-release/artifacts.zip',
r'flutter_infra_release/flutter/$engine/flutter_patched_sdk_product.zip',
r'flutter_infra_release/flutter/$engine/flutter-web-sdk.zip',
r'flutter_infra_release/flutter/$engine/darwin-arm64-release/FlutterMacOS.framework.zip',
r'flutter_infra_release/flutter/$engine/android-arm-release/artifacts.zip',
r'flutter_infra_release/flutter/$engine/android-arm-release/symbols.zip',
r'flutter_infra_release/flutter/$engine/android-x64-release/artifacts.zip',
@@ -81,12 +87,73 @@ void main() {
expect(
response,
isRedirectTo(
'https://storage.googleapis.com/${manifest.storageBucket}/$path',
'http://localhost:8080/artifacts/${manifest.storageBucket}/$path',
),
);
verify(() => client.getManifest(shorebirdEngineRevision)).called(1);
});
test('should proxy CI-produced desktop artifacts with overrides', () async {
const paths = [
'flutter_infra_release/flutter/$shorebirdEngineRevision/linux-x64-release/artifacts.zip',
'flutter_infra_release/flutter/$shorebirdEngineRevision/linux-x64-release/linux-x64-flutter-gtk.zip',
'flutter_infra_release/flutter/$shorebirdEngineRevision/flutter_patched_sdk_product.zip',
'flutter_infra_release/flutter/$shorebirdEngineRevision/darwin-arm64-release/FlutterMacOS.framework.zip',
];
for (final path in paths) {
expect(
await handler(buildRequest(path)),
isRedirectTo(
'http://localhost:8080/artifacts/${manifest.storageBucket}/$path',
),
);
}
verify(() => client.getManifest(shorebirdEngineRevision)).called(4);
});
test('should proxy CI-produced iOS and web artifacts with overrides', () async {
const paths = [
'flutter_infra_release/flutter/$shorebirdEngineRevision/ios-release/artifacts.zip',
'flutter_infra_release/flutter/$shorebirdEngineRevision/flutter-web-sdk.zip',
];
for (final path in paths) {
expect(
await handler(buildRequest(path)),
isRedirectTo(
'http://localhost:8080/artifacts/${manifest.storageBucket}/$path',
),
);
}
verify(() => client.getManifest(shorebirdEngineRevision)).called(2);
});
test('can use custom Flutter and Shorebird artifact roots', () async {
handler = artifactProxyHandler(
client: client,
flutterArtifactBaseUri: Uri.parse('https://flutter.example.com'),
shorebirdArtifactBaseUri: Uri.parse('https://shorebird.example.com'),
);
const shorebirdPath =
'flutter_infra_release/flutter/$shorebirdEngineRevision/android-x64-release/artifacts.zip';
const flutterPath =
'flutter_infra_release/flutter/$shorebirdEngineRevision/windows-x64/font-subset.zip';
expect(
await handler(buildRequest(shorebirdPath)),
isRedirectTo(
'https://shorebird.example.com/${manifest.storageBucket}/$shorebirdPath',
),
);
expect(
await handler(buildRequest(flutterPath)),
isRedirectTo(
'https://flutter.example.com/flutter_infra_release/flutter/${manifest.flutterEngineRevision}/windows-x64/font-subset.zip',
),
);
});
test('should proxy to Flutter artifacts '
'when an engine revision is detected with no override', () async {
const path =
@@ -0,0 +1,49 @@
import 'dart:io';
import 'package:test/test.dart';
void main() {
test('health-check exits successfully without binding a server', () async {
final workingDirectory = _shorebirdRoot();
final result = await Process.run(
Platform.resolvedExecutable,
[
'run',
'packages/artifact_proxy/bin/server.dart',
'--health-check',
],
workingDirectory: workingDirectory.path,
environment: {
'SHOREBIRD_ARTIFACT_BASE_URL': 'https://artifacts.example.com/open',
'ARTIFACT_PROXY_FLUTTER_BASE_URL': 'https://flutter.example.com',
'PORT': '18080',
},
);
expect(result.exitCode, 0, reason: result.stderr as String?);
expect(
result.stdout,
contains(
'artifact_proxy ok '
'shorebird_artifacts=https://artifacts.example.com/open '
'flutter_artifacts=https://flutter.example.com '
'port=18080',
),
);
});
}
Directory _shorebirdRoot() {
var directory = Directory.current;
while (true) {
if (File('${directory.path}/pubspec.yaml').existsSync() &&
Directory('${directory.path}/packages/artifact_proxy').existsSync()) {
return directory;
}
final parent = directory.parent;
if (parent.path == directory.path) {
throw StateError('Could not find Shorebird package root.');
}
directory = parent;
}
}
@@ -30,13 +30,30 @@ void main() {
expect(ArtifactManifestClient.new, returnsNormally);
});
test('makes correct http request to storage bucket', () async {
test('makes correct http request to open artifact mirror', () async {
client.getManifest(revision).ignore();
verify(
() => httpClient.get(
Uri.parse(
'https://storage.googleapis.com/download.shorebird.dev/shorebird/$revision/artifacts_manifest.yaml',
'http://localhost:8080/artifacts/shorebird/$revision/artifacts_manifest.yaml',
),
),
).called(1);
});
test('can use a custom artifact manifest root', () async {
client = ArtifactManifestClient(
httpClient: httpClient,
manifestBaseUri: Uri.parse('https://mirror.example.com/root'),
);
client.getManifest(revision).ignore();
verify(
() => httpClient.get(
Uri.parse(
'https://mirror.example.com/root/shorebird/$revision/artifacts_manifest.yaml',
),
),
).called(1);
@@ -68,7 +85,7 @@ void main() {
verify(
() => httpClient.get(
Uri.parse(
'https://storage.googleapis.com/download.shorebird.dev/shorebird/$revision/artifacts_manifest.yaml',
'http://localhost:8080/artifacts/shorebird/$revision/artifacts_manifest.yaml',
),
),
).called(1);
@@ -78,7 +95,7 @@ void main() {
const _testArtifactManifest = r'''
flutter_engine_revision: ec975089acb540fc60752606a3d3ba809dd1528b
storage_bucket: https://download.shorebird.dev
storage_bucket: shorebird
artifact_overrides:
# artifacts.zip
- flutter_infra_release/flutter/$engine/android-arm-64-release/artifacts.zip
@@ -189,6 +189,19 @@ metadata before decrypting. `Dart_InstallAotPatch` rejects iOS native-AOT
artifacts, rejects any dynamic-modules runtime mode, and rejects compact
interpreter payloads before payload installation.
The open CLI now follows that route for `shorebird patch ios`. By default, iOS
patch builds compile the patch target to Dart bytecode with `dart2bytecode`,
write `build/ios_interpreter_patch.bytecode`, wrap that payload as
`open-aot-vmcode-encrypted-v1`, and upload the encrypted interpreter artifact.
The bytecode compile forwards user Dart defines, `FLUTTER_APP_FLAVOR`, and the
standard Flutter version/revision/Dart SDK defines from
`bin/cache/flutter.version.json`. It also mirrors Flutter's
`FLUTTER_ENABLED_FEATURE_FLAGS` define for enabled runtime-id features. User
attempts to override those reserved Flutter defines are rejected.
`aot_patch_bytecode_path` and `SHOREBIRD_IOS_INTERPRETER_PATCH_PATH` are local
override hooks for experiments. The native iOS AOT patch branch is kept only for
development-device validation and requires `SHOREBIRD_IOS_NATIVE_AOT_PATCH=1`.
The open Flutter engine bridge can now pass the encrypted artifact selected by
the updater into `Dart_InstallAotPatch`, then hand the decrypted bytecode to
`Dart_ReloadBytecodePatch`. This bridge is intentionally no-DDM and never adds
@@ -200,12 +213,13 @@ functions to the VM's signed `InterpretCall` stub with attached bytecode. This
does not enable `DART_DYNAMIC_MODULES` and does not map downloaded executable
memory.
For local encrypted interpreter tests, `shorebird.yaml` may provide a
development AES key and expected metadata:
For local encrypted interpreter tests, `shorebird.yaml` may provide expected
metadata:
```yaml
aot_patch_runtime_mode: dart-bytecode-interpreter
aot_patch_key_id: test-key
aot_patch_key_hex: 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
aot_patch_app_build_id: 1.2.3+4
aot_patch_base_flavor_id: free
aot_patch_base_license_type: free
aot_patch_flavor_id: pro
@@ -214,10 +228,15 @@ aot_patch_sdk_hash: <flutter-engine-or-dart-sdk-hash>
aot_patch_base_snapshot_hash: <sha256-of-base-bytecode-snapshot>
```
Embedding a symmetric key in `shorebird.yaml` is a development bridge, not the
final production key strategy. The production replacement should supply the AES
key through an app-owned callback or platform key source while keeping the same
VM validation and bytecode reload path.
Add `aot_patch_bytecode_path: build/patch.interp` only when overriding the
generated bytecode payload.
Set `SHOREBIRD_AOT_PATCH_KEY_HEX` when running the CLI so the key is not bundled
with the reviewed app. Embedding `aot_patch_key_hex` in `shorebird.yaml` remains
a development bridge only; strict iOS route checks reject it. The production
replacement should supply the runtime AES key through an app-owned callback or
platform key source while keeping the same VM validation and bytecode reload
path.
The Flutter engine patch cache is prepared for this handoff:
+2 -2
View File
@@ -1,8 +1,8 @@
name: shorebird_redis_client
description: A lightweight Dart client library for communicating with a Redis server. Built by Shorebird.
version: 0.0.13
homepage: https://shorebird.dev
repository: https://github.com/shorebirdtech/shorebird/tree/main/packages/redis_client
homepage: https://git.tonycloud.org/flutter/shorebird-workspace
repository: https://git.tonycloud.org/flutter/shorebird-workspace
topics: [redis, cache, shorebird]
resolution: workspace
+2 -2
View File
@@ -1,8 +1,8 @@
name: scoped_deps
description: A simple Dart library for managing scoped dependencies built on top of Zones from dart:async.
version: 0.1.0+2
homepage: https://shorebird.dev
repository: https://github.com/shorebirdtech/shorebird/tree/main/packages/scoped_deps
homepage: https://git.tonycloud.org/flutter/shorebird-workspace
repository: https://git.tonycloud.org/flutter/shorebird-workspace
resolution: workspace
environment:
+2 -2
View File
@@ -5,8 +5,8 @@ description: >
Not intended for consumption outside of those projects.
version: 0.1.0
publish_to: none
homepage: https://shorebird.dev
repository: https://github.com/shorebirdtech/shorebird/tree/main/packages/shorebird_build_trace
homepage: https://git.tonycloud.org/flutter/shorebird-workspace
repository: https://git.tonycloud.org/flutter/shorebird-workspace
resolution: workspace
environment:
+2 -2
View File
@@ -4,8 +4,8 @@ description: >-
workflows, resolves affected packages via dependency graphs, and
verifies path filters stay in sync.
version: 0.2.4
homepage: https://shorebird.dev
repository: https://github.com/shorebirdtech/shorebird/tree/main/packages/shorebird_ci
homepage: https://git.tonycloud.org/flutter/shorebird-workspace
repository: https://git.tonycloud.org/flutter/shorebird-workspace
topics: [ci, github-actions, monorepo, shorebird]
resolution: workspace
@@ -37,7 +37,7 @@ class WindowsArchiveDiffer extends ArchiveDiffer {
// Otherwise, this function would return true if the file has a .dll or .exe
// extension.
//
// See https://github.com/shorebirdtech/shorebird/issues/2794
// See https://git.tonycloud.org/flutter/shorebird/issues/2794
return false;
}
}
@@ -112,7 +112,7 @@ ${lightCyan.wrap('shorebird release <platform> --flutter-version=3.29.0')}
• If `flutter build` completes successfully and `shorebird release`
fails when using the same flutter version, please file an issue:
${link(uri: Uri.parse('https://github.com/shorebirdtech/shorebird/issues/new'))}
${link(uri: Uri.parse(openShorebirdIssueUrl))}
''';
/// Cache of `flutter build <command>` help output checks for
@@ -246,7 +246,7 @@ Reason: Exited with code $exitCode.''',
if (target != null) '--target=$target',
if (targetPlatformArgs != null) '--target-platform=$targetPlatformArgs',
// TODO(bryanoltman): reintroduce coverage when we can support this.
// See https://github.com/shorebirdtech/shorebird/issues/1141.
// See https://git.tonycloud.org/flutter/shorebird/issues/1141.
// coverage:ignore-start
if (splitPerAbi) '--split-per-abi',
// coverage:ignore-end
@@ -459,7 +459,7 @@ Reason: Exited with code $exitCode.''',
throw ArtifactBuildException(
'Unable to find app.dill file.',
fixRecommendation:
'''Please file a bug at https://github.com/shorebirdtech/shorebird/issues/new with the logs for this command.''',
'''Please file a bug at $openShorebirdIssueUrl with the logs for this command.''',
);
}
@@ -531,7 +531,7 @@ Reason: Exited with code $exitCode.''',
throw ArtifactBuildException(
'Unable to find app.dill file.',
fixRecommendation:
'''Please file a bug at https://github.com/shorebirdtech/shorebird/issues/new with the logs for this command.''',
'''Please file a bug at $openShorebirdIssueUrl with the logs for this command.''',
);
}
@@ -596,7 +596,7 @@ Reason: Exited with code $exitCode.''',
throw ArtifactBuildException(
'Unable to find app.dill file.',
fixRecommendation:
'''Please file a bug at https://github.com/shorebirdtech/shorebird/issues/new with the logs for this command.''',
'''Please file a bug at $openShorebirdIssueUrl with the logs for this command.''',
);
}
@@ -760,7 +760,7 @@ Reason: Exited with code $exitCode.''',
/// Flutter SDK on the user's PATH. This is necessary because Flutter commands
/// run by shorebird update the package_config.json file to point to
/// shorebird's version of Flutter, which confuses VS Code. See
/// https://github.com/shorebirdtech/shorebird/issues/1101 for more info.
/// https://git.tonycloud.org/flutter/shorebird/issues/1101 for more info.
Future<void> _systemFlutterPubGet() async {
const executable = 'flutter';
if (osInterface.which(executable) == null) {
@@ -825,6 +825,51 @@ Either run `flutter pub get` manually, or follow the steps in ${cannotRunInVSCod
return File(outFilePath);
}
/// Compiles [inputFilePath] to a Dart bytecode snapshot at [outFilePath].
Future<File> buildDartBytecodeSnapshot({
required String inputFilePath,
required String outFilePath,
String? packageConfigPath,
List<String> dartDefines = const [],
List<String> experiments = const [],
}) async {
final arguments = [
shorebirdArtifacts.getArtifactPath(
artifact: ShorebirdArtifact.dart2BytecodeSnapshot,
),
'--platform=${shorebirdArtifacts.getArtifactPath(
artifact: ShorebirdArtifact.flutterProductPlatformDill,
)}',
'--target=flutter',
if (packageConfigPath != null) '--packages=$packageConfigPath',
for (final define in dartDefines) '-D$define',
'-Ddart.vm.profile=false',
'-Ddart.vm.product=true',
for (final experiment in experiments) '--enable-experiment=$experiment',
'-o',
outFilePath,
inputFilePath,
];
final exitCode = await shorebirdTracer.span<int>(
name: 'dart2bytecode',
category: 'subprocess',
body: () => process.stream(
shorebirdArtifacts.getArtifactPath(
artifact: ShorebirdArtifact.dartAotRuntime,
),
arguments,
runInShell: false,
),
);
if (exitCode != ExitCode.success.code) {
throw ArtifactBuildException('Failed to create Dart bytecode snapshot');
}
return File(outFilePath);
}
/// Builds a windows app and returns the x64 Release directory
Future<Directory> buildWindowsApp({
String? target,
@@ -225,7 +225,7 @@ class ArtifactManager {
// "strip{flavor}ReleaseDebugSymbols". We check first for the new
// directory and then fallback to the old one.
//
// See https://github.com/shorebirdtech/shorebird/issues/1798
// See https://git.tonycloud.org/flutter/shorebird/issues/1798
final strippedSymbolsDir = Directory(
p.join(releasePath, stripReleaseDebugSymbolsDirName),
);
@@ -252,7 +252,7 @@ class ArtifactManager {
/// (flutter/flutter#181275) handed `libapp.so` stripping to AGP, and the
/// strip task can emit nothing or leave stale output from a previous
/// build while the current library is still bundled into the AAB. See
/// https://github.com/shorebirdtech/shorebird/issues/3388. Reading the AAB
/// https://git.tonycloud.org/flutter/shorebird/issues/3388. Reading the AAB
/// first sidesteps the strip task entirely.
///
/// Reading the unstripped `merged_native_libs` is deliberately NOT used as a
@@ -20,6 +20,7 @@ import 'package:shorebird_cli/src/platform.dart';
import 'package:shorebird_cli/src/shorebird_cli_command_runner.dart';
import 'package:shorebird_cli/src/shorebird_command.dart';
import 'package:shorebird_cli/src/shorebird_env.dart';
import 'package:shorebird_cli/src/shorebird_web_console.dart';
import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
@@ -368,7 +369,7 @@ class Auth {
'`shorebird login:ci`. '
'This format is deprecated and will stop working in a future '
'release. '
'Create an API key at https://console.shorebird.dev instead.',
'Create an API key at ${ShorebirdWebConsole.uri('')} instead.',
);
} on FormatException catch (e) {
logger
+73 -8
View File
@@ -52,7 +52,25 @@ class Cache {
Cache() {
registerArtifact(PatchArtifact(cache: this, platform: platform));
registerArtifact(BundleToolArtifact(cache: this, platform: platform));
registerArtifact(AotToolsArtifact(cache: this, platform: platform));
if (legacyAotToolsEnabled) {
registerArtifact(AotToolsArtifact(cache: this, platform: platform));
}
}
/// Environment variable that enables the legacy closed aot-tools download.
static const legacyAotToolsEnvironmentVariable =
'SHOREBIRD_ENABLE_LEGACY_AOT_TOOLS';
/// Whether to download legacy `aot-tools.dill`.
///
/// The open iOS App Store-safe path uses Dart bytecode interpreter artifacts
/// and must not fetch Shorebird's closed native-AOT linker artifact by
/// default. Developers can opt in when validating the old native-AOT route.
bool get legacyAotToolsEnabled {
final value = platform.environment[legacyAotToolsEnvironmentVariable]
?.trim()
.toLowerCase();
return value == '1' || value == 'true' || value == 'yes';
}
/// Register a new [CachedArtifact] with the cache.
@@ -117,11 +135,34 @@ class Cache {
final List<CachedArtifact> _artifacts = [];
/// Default local root for open Shorebird CLI-managed binary artifacts.
static const defaultArtifactBaseUrl = 'http://localhost:8080/artifacts';
/// The storage base url.
String get storageBaseUrl => 'https://storage.googleapis.com';
String get storageBaseUrl =>
platform.environment['SHOREBIRD_STORAGE_BASE_URL'] ??
defaultArtifactBaseUrl;
/// The storage bucket host.
String get storageBucket => 'download.shorebird.dev';
String get storageBucket =>
platform.environment['SHOREBIRD_STORAGE_BUCKET'] ?? '';
/// The root URL for Shorebird CLI-managed binary artifacts.
///
/// Defaults to a local open artifact mirror. Deployments can set
/// `SHOREBIRD_ARTIFACT_BASE_URL` to host the same `/shorebird/<engine
/// revision>/...` layout at a public URL.
String get artifactBaseUrl {
final override = platform.environment['SHOREBIRD_ARTIFACT_BASE_URL'];
if (override != null && override.trim().isNotEmpty) {
return _trimTrailingSlash(override.trim());
}
final bucket = storageBucket.trim();
final base = _trimTrailingSlash(storageBaseUrl.trim());
if (bucket.isEmpty) return base;
return '$base/${_trimSlashes(bucket)}';
}
/// Clear the cache.
Future<void> clear() async {
@@ -130,6 +171,28 @@ class Cache {
await cacheDir.delete(recursive: true);
}
}
/// Returns the full URL for a Shorebird artifact stored under the current
/// engine revision.
String shorebirdArtifactUrl(String fileName) {
return '$artifactBaseUrl/shorebird/${shorebirdEnv.shorebirdEngineRevision}/$fileName';
}
String _trimTrailingSlash(String value) {
var trimmed = value;
while (trimmed.endsWith('/')) {
trimmed = trimmed.substring(0, trimmed.length - 1);
}
return trimmed;
}
String _trimSlashes(String value) {
var trimmed = value;
while (trimmed.startsWith('/')) {
trimmed = trimmed.substring(1);
}
return _trimTrailingSlash(trimmed);
}
}
/// {@template cached_artifact}
@@ -284,8 +347,11 @@ allowed to access $url.''');
}
/// {@template aot_tools_artifact}
/// The aot_tools.dill artifact.
/// Used for linking and generating optimized AOT snapshots.
/// The legacy closed aot_tools.dill artifact.
///
/// This is not part of the default open-source cache. It is available only
/// when [Cache.legacyAotToolsEnvironmentVariable] is set for development-only
/// native-AOT validation.
/// {@endtemplate}
class AotToolsArtifact extends CachedArtifact {
/// {@macro aot_tools_artifact}
@@ -311,8 +377,7 @@ class AotToolsArtifact extends CachedArtifact {
);
@override
Future<String> get storageUrl async =>
'${cache.storageBaseUrl}/${cache.storageBucket}/shorebird/${shorebirdEnv.shorebirdEngineRevision}/$fileName';
Future<String> get storageUrl async => cache.shorebirdArtifactUrl(fileName);
@override
String? get checksum => null;
@@ -358,7 +423,7 @@ class PatchArtifact extends CachedArtifact {
artifactName += 'windows-x64.zip';
}
return '${cache.storageBaseUrl}/${cache.storageBucket}/shorebird/${shorebirdEnv.shorebirdEngineRevision}/$artifactName';
return cache.shorebirdArtifactUrl(artifactName);
}
Future<bool> _supportsArm64Patch() async {
@@ -18,6 +18,7 @@ import 'package:shorebird_cli/src/deployment_track.dart';
import 'package:shorebird_cli/src/executables/executables.dart';
import 'package:shorebird_cli/src/logging/logging.dart';
import 'package:shorebird_cli/src/platform/platform.dart';
import 'package:shorebird_cli/src/shorebird_documentation.dart';
import 'package:shorebird_cli/src/shorebird_env.dart';
import 'package:shorebird_cli/src/shorebird_flutter.dart';
import 'package:shorebird_cli/src/shorebird_web_console.dart';
@@ -484,11 +485,12 @@ Please create a release using "shorebird release" and try again.
_handleErrorAndExit(
Exception('Cannot find patch build artifacts.'),
progress: createArtifactProgress,
message: '''
message:
'''
Cannot find release build artifacts.
Please run `shorebird cache clean` and try again. If the issue persists, please
file a bug report at https://github.com/shorebirdtech/shorebird/issues/new.
file a bug report at $openShorebirdIssueUrl.
Looked in:
- the libapp.so entries inside the built .aab
@@ -502,7 +504,7 @@ Looked in:
// the project filters it out via `ndk.abiFilters`, `splits.abi`, or
// `jniLibs.excludes`. Iterating only over present files lets a filtered
// release succeed instead of crashing on the first missing arch
// (https://github.com/shorebirdtech/shorebird/issues/3388).
// (https://git.tonycloud.org/flutter/shorebird/issues/3388).
final missingArchPaths = <String>[];
var uploadedArchCount = 0;
for (final arch in architectures) {
@@ -57,7 +57,7 @@ class DoctorCommand extends ShorebirdCommand {
shorebirdFlutterPrefix.write(' $flutterVersion');
}
output.writeln('''
Shorebird $packageVersion git@github.com:shorebirdtech/shorebird.git
Shorebird $packageVersion https://git.tonycloud.org/flutter/shorebird.git
$shorebirdFlutterPrefix revision ${shorebirdEnv.flutterRevision}
Engine revision ${shorebirdEnv.shorebirdEngineRevision}''');
@@ -76,7 +76,7 @@ Please make sure you are running "shorebird init" from within your Flutter proje
.getOrganizationMemberships();
if (organizationMemberships.isEmpty) {
logger.err(
'''You do not have any organizations. This should never happen. Please contact us on Discord or send us an email at contact@shorebird.dev.''',
'''You do not have any organizations. This should never happen. Please file an issue at $openShorebirdIssueUrl.''',
);
return ExitCode.software.code;
}
@@ -339,7 +339,7 @@ Reference the following commands to get started:
🚀 To push an update use: "${lightCyan.wrap('shorebird patch')}".
👀 To preview a release use: "${lightCyan.wrap('shorebird preview')}".
For more information about Shorebird, visit ${link(uri: Uri.parse('https://shorebird.dev'))}''',
For more information about Shorebird, visit ${link(uri: Uri.parse(docsUrl))}''',
);
await doctor.runValidators(
@@ -1,6 +1,8 @@
import 'package:mason_logger/mason_logger.dart';
import 'package:shorebird_cli/src/logging/logging.dart';
import 'package:shorebird_cli/src/shorebird_command.dart';
import 'package:shorebird_cli/src/shorebird_documentation.dart';
import 'package:shorebird_cli/src/shorebird_web_console.dart';
/// {@template login_ci_command}
/// `shorebird login:ci`
@@ -19,9 +21,9 @@ class LoginCiCommand extends ShorebirdCommand {
'''
shorebird login:ci has been replaced by API keys.
Create an API key at ${link(uri: Uri.parse('https://console.shorebird.dev'))} and set it as your ${lightCyan.wrap('SHOREBIRD_TOKEN')} environment variable.
Create an API key at ${link(uri: ShorebirdWebConsole.uri(''))} and set it as your ${lightCyan.wrap('SHOREBIRD_TOKEN')} environment variable.
Learn more: ${link(uri: Uri.parse('https://docs.shorebird.dev/account/api-keys/'))}''',
Learn more: ${link(uri: Uri.parse('$docsUrl/account/api-keys/'))}''',
);
return ExitCode.usage.code;
}
@@ -2,6 +2,7 @@ import 'package:mason_logger/mason_logger.dart';
import 'package:shorebird_cli/src/auth/auth.dart';
import 'package:shorebird_cli/src/logging/logging.dart';
import 'package:shorebird_cli/src/shorebird_command.dart';
import 'package:shorebird_cli/src/shorebird_web_console.dart';
/// {@template login_command}
/// `shorebird login`
@@ -33,7 +34,7 @@ class LoginCommand extends ShorebirdCommand {
try {
await auth.login(prompt: prompt);
} on UserNotFoundException catch (error) {
final consoleUri = Uri.https('console.shorebird.dev');
final consoleUri = ShorebirdWebConsole.uri('');
logger
..err('''
We could not find a Shorebird account for ${error.email}.''')
@@ -22,7 +22,7 @@ class LogoutCommand extends ShorebirdCommand {
return ExitCode.success.code;
}
final logoutProgress = logger.progress('Logging out of shorebird.dev');
final logoutProgress = logger.progress('Logging out of Shorebird');
await auth.logout();
logoutProgress.complete();
@@ -99,6 +99,7 @@ class AarPatcher extends Patcher {
required int releaseId,
required File releaseArtifact,
Directory? supplementDirectory,
String? releaseVersion,
}) async {
final releaseArtifacts = await codePushClientWrapper.getReleaseArtifacts(
appId: appId,
@@ -17,6 +17,7 @@ import 'package:shorebird_cli/src/patch_diff_checker.dart';
import 'package:shorebird_cli/src/platform/platform.dart';
import 'package:shorebird_cli/src/release_type.dart';
import 'package:shorebird_cli/src/shorebird_android_artifacts.dart';
import 'package:shorebird_cli/src/shorebird_documentation.dart';
import 'package:shorebird_cli/src/shorebird_flutter.dart';
import 'package:shorebird_cli/src/shorebird_validator.dart';
import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart';
@@ -36,13 +37,13 @@ class AndroidPatcher extends Patcher {
/// Android versions prior to 3.24.2 have a bug that can cause patches to
/// be erroneously uninstalled.
/// https://github.com/shorebirdtech/updater/issues/211 was fixed in 3.24.2
/// Fixed in Flutter 3.24.2.
static final updaterPatchErrorWarning =
'''
Your version of flutter contains a known issue that can cause patches to be erroneously uninstalled in apps that use package:flutter_foreground_task or other plugins that start their own Flutter engines.
This issue was fixed in Flutter 3.24.2. Please upgrade to a newer version of Flutter to avoid this issue.
See more info about the issue ${link(uri: Uri.parse('https://github.com/shorebirdtech/updater/issues/211'), message: 'on Github')}
See more info in the open issue tracker: ${link(uri: Uri.parse(openShorebirdIssueUrl), message: 'open issue tracker')}
''';
/// The `<arch>/libapp.so` directory resolved by [buildPatchArtifact]
@@ -50,7 +51,7 @@ See more info about the issue ${link(uri: Uri.parse('https://github.com/shorebir
/// AAB is unreadable, AGP's stripped output. Cached so
/// [createPatchArtifacts] reuses it instead of decoding the AAB a second
/// time.
/// See https://github.com/shorebirdtech/shorebird/issues/3388.
/// See https://git.tonycloud.org/flutter/shorebird/issues/3388.
Directory? _patchArchsBuildDir;
@override
@@ -93,7 +94,7 @@ See more info about the issue ${link(uri: Uri.parse('https://github.com/shorebir
final flutterVersion = await shorebirdFlutter.getVersion();
// Android versions prior to 3.24.2 have a bug that can cause patches to
// be erroneously uninstalled.
// https://github.com/shorebirdtech/updater/issues/211 was fixed in 3.24.2
// Fixed in Flutter 3.24.2.
if (flutterVersion != null && flutterVersion < Version(3, 24, 2)) {
logger.warn(updaterPatchErrorWarning);
}
@@ -122,7 +123,7 @@ See more info about the issue ${link(uri: Uri.parse('https://github.com/shorebir
..err('Cannot find patch build artifacts.')
..info('''
Please run `shorebird cache clean` and try again. If the issue persists, please
file a bug report at https://github.com/shorebirdtech/shorebird/issues/new.
file a bug report at $openShorebirdIssueUrl.
Looked in:
- the libapp.so entries inside the built .aab
@@ -140,6 +141,7 @@ Looked in:
required File releaseArtifact,
Directory? supplementDirectory,
Duration downloadMessageTimeout = const Duration(minutes: 1),
String? releaseVersion,
}) async {
final releaseArtifacts = await codePushClientWrapper.getReleaseArtifacts(
appId: appId,
@@ -150,7 +152,8 @@ Looked in:
final releaseArtifactPaths = <Arch, String>{};
final numArtifacts = releaseArtifacts.length;
// Direct users to https://github.com/shorebirdtech/shorebird/issues/2532
// Direct users to the troubleshooting docs until we can provide a better
// solution.
// until we can provide a better solution.
var artifactsDownloadCompleted = false;
unawaited(
@@ -161,7 +164,7 @@ Looked in:
logger.info(
'''
It seems like your download is taking longer than expected. If you are on Windows, this is a known issue.
Please refer to ${link(uri: Uri.parse('https://github.com/shorebirdtech/shorebird/issues/2532'))} for potential workarounds.''',
Please refer to ${link(uri: Uri.parse(troubleshootingUrl))} for potential workarounds.''',
);
}),
);
@@ -209,7 +212,7 @@ Please refer to ${link(uri: Uri.parse('https://github.com/shorebirdtech/shorebir
// Skip archs the build didn't produce (ndk.abiFilters / splits.abi /
// jniLibs.excludes). Without this, a release built for a subset of
// archs can't be patched without hitting PathNotFoundException
// (https://github.com/shorebirdtech/shorebird/issues/3388).
// (https://git.tonycloud.org/flutter/shorebird/issues/3388).
if (!patchArtifact.existsSync()) {
logger.detail(
'Skipping ${arch.arch}: no libapp.so at $patchArtifactPath. '
@@ -132,6 +132,7 @@ class IosFrameworkPatcher extends Patcher with ApplePatcherMixin {
required int releaseId,
required File releaseArtifact,
Directory? supplementDirectory,
String? releaseVersion,
}) async {
final unzipProgress = logger.progress('Extracting release artifact');
late final String releaseXcframeworkPath;
@@ -1,9 +1,12 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:crypto/crypto.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:meta/meta.dart';
import 'package:open_aot_patch_tools/open_aot_patch_tools.dart' as open_patch;
import 'package:path/path.dart' as p;
import 'package:shorebird_cli/src/archive/archive.dart';
import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart';
@@ -12,6 +15,7 @@ import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
import 'package:shorebird_cli/src/commands/patch/apple_patcher_mixin.dart';
import 'package:shorebird_cli/src/commands/patch/patcher.dart';
import 'package:shorebird_cli/src/common_arguments.dart';
import 'package:shorebird_cli/src/config/config.dart';
import 'package:shorebird_cli/src/doctor.dart';
import 'package:shorebird_cli/src/executables/executables.dart';
import 'package:shorebird_cli/src/extensions/arg_results.dart';
@@ -50,6 +54,42 @@ class IosPatcher extends Patcher
String get _appDillCopyPath =>
p.join(shorebirdEnv.buildDirectory.path, 'app.dill');
String get _interpreterPatchOutputPath =>
p.join(shorebirdEnv.buildDirectory.path, 'ios_interpreter_patch.vmcode');
String get _interpreterPatchBytecodeOutputPath => p.join(
shorebirdEnv.buildDirectory.path,
'ios_interpreter_patch.bytecode',
);
static const _flutterVersionDefine = 'FLUTTER_VERSION';
static const _flutterChannelDefine = 'FLUTTER_CHANNEL';
static const _flutterGitUrlDefine = 'FLUTTER_GIT_URL';
static const _flutterFrameworkRevisionDefine = 'FLUTTER_FRAMEWORK_REVISION';
static const _flutterEngineRevisionDefine = 'FLUTTER_ENGINE_REVISION';
static const _flutterDartVersionDefine = 'FLUTTER_DART_VERSION';
static const _flutterReservedDefines = [
_flutterVersionDefine,
_flutterChannelDefine,
_flutterGitUrlDefine,
_flutterFrameworkRevisionDefine,
_flutterEngineRevisionDefine,
_flutterDartVersionDefine,
'FLUTTER_ENABLED_FEATURE_FLAGS',
];
static const _runtimeFeatureFlags = [
(
configSetting: 'enable-windowing',
environmentOverride: 'FLUTTER_WINDOWING',
runtimeId: 'windowing',
),
(
configSetting: 'enable-accessibility-evaluations',
environmentOverride: 'FLUTTER_ACCESSIBILITY_EVALUATIONS',
runtimeId: 'accessibility_evaluations',
),
];
/// The last build's link percentage.
@visibleForTesting
double? lastBuildLinkPercentage;
@@ -129,18 +169,35 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''');
base64PublicKey: argResults.encodedPublicKey,
);
if (splitDebugInfoPath != null) {
Directory(splitDebugInfoPath!).createSync(recursive: true);
final runtimeMode = _iosPatchRuntimeMode;
if (runtimeMode == open_patch.runtimeModeNativeAot) {
if (!_allowsNativeIosAotPatch) {
logger.err(
'''
iOS native AOT patch artifacts are disabled by default because they require mapping downloaded native snapshot text as executable memory.
Set SHOREBIRD_IOS_NATIVE_AOT_PATCH=1 only for development-device validation.''',
);
throw ProcessExit(ExitCode.software.code);
}
if (splitDebugInfoPath != null) {
Directory(splitDebugInfoPath!).createSync(recursive: true);
}
await artifactBuilder.buildElfAotSnapshot(
appDillPath: ipaBuildResult.kernelFile.path,
outFilePath: _aotOutputPath,
genSnapshotArtifact: ShorebirdArtifact.genSnapshotIos,
additionalArgs: [
...ApplePatcherMixin.splitDebugInfoArgs(splitDebugInfoPath),
...obfuscationGenSnapshotArgs,
],
);
} else if (runtimeMode != open_patch.runtimeModeDartBytecodeInterpreter) {
logger.err('Unsupported iOS patch runtime mode: $runtimeMode');
throw ProcessExit(ExitCode.usage.code);
}
if (runtimeMode == open_patch.runtimeModeDartBytecodeInterpreter) {
await _buildInterpreterBytecodePatch(buildArgs);
}
await artifactBuilder.buildElfAotSnapshot(
appDillPath: ipaBuildResult.kernelFile.path,
outFilePath: _aotOutputPath,
genSnapshotArtifact: ShorebirdArtifact.genSnapshotIos,
additionalArgs: [
...ApplePatcherMixin.splitDebugInfoArgs(splitDebugInfoPath),
...obfuscationGenSnapshotArgs,
],
);
// Copy the kernel file to the build directory so that it can be used
// to generate a patch.
@@ -155,6 +212,7 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''');
required int releaseId,
required File releaseArtifact,
Directory? supplementDirectory,
String? releaseVersion,
}) async {
// Verify that we have built a patch .xcarchive
if (artifactManager.getXcarchiveDirectory()?.path == null) {
@@ -162,6 +220,27 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''');
throw ProcessExit(ExitCode.software.code);
}
final runtimeMode = _iosPatchRuntimeMode;
if (runtimeMode == open_patch.runtimeModeDartBytecodeInterpreter) {
return _createInterpreterPatchArtifacts(
appId: appId,
releaseVersion: releaseVersion,
);
}
if (runtimeMode != open_patch.runtimeModeNativeAot) {
logger.err('Unsupported iOS patch runtime mode: $runtimeMode');
throw ProcessExit(ExitCode.usage.code);
}
if (!_allowsNativeIosAotPatch) {
logger.err(
'''
iOS native AOT patch artifacts are disabled by default because they require mapping downloaded native snapshot text as executable memory.
Use the open interpreter route (`aot_patch_runtime_mode: dart-bytecode-interpreter`) for App Store-safe patches.
Set SHOREBIRD_IOS_NATIVE_AOT_PATCH=1 only for development-device validation.''',
);
throw ProcessExit(ExitCode.software.code);
}
final unzipProgress = logger.progress('Extracting release artifact');
late final String releaseXcarchivePath;
@@ -271,6 +350,518 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''');
};
}
String get _iosPatchRuntimeMode {
final environmentMode =
platform.environment['SHOREBIRD_IOS_PATCH_RUNTIME_MODE'];
if (environmentMode != null && environmentMode.trim().isNotEmpty) {
return environmentMode.trim();
}
if (_allowsNativeIosAotPatch) {
return open_patch.runtimeModeNativeAot;
}
return shorebirdEnv.getShorebirdYaml()?.aotPatchRuntimeMode ??
open_patch.runtimeModeDartBytecodeInterpreter;
}
bool get _allowsNativeIosAotPatch {
final value = platform.environment['SHOREBIRD_IOS_NATIVE_AOT_PATCH'];
return value == '1' || value == 'true' || value == 'yes';
}
Future<Map<Arch, PatchArtifactBundle>> _createInterpreterPatchArtifacts({
required String appId,
required String? releaseVersion,
}) async {
final shorebirdYaml = shorebirdEnv.getShorebirdYaml();
if (shorebirdYaml == null) {
logger.err('Unable to find shorebird.yaml for iOS interpreter patch.');
throw ProcessExit(ExitCode.software.code);
}
final bytecodePath = _configuredInterpreterBytecodePath(shorebirdYaml);
final bytecodeFile = bytecodePath == null
? File(_interpreterPatchBytecodeOutputPath)
: _projectRelativeFile(bytecodePath);
if (!bytecodeFile.existsSync()) {
logger.err(
bytecodePath == null
? '''
Unable to find generated iOS interpreter patch bytecode: ${bytecodeFile.path}
Run `shorebird patch ios` so the patch build can generate it, or set aot_patch_bytecode_path/SHOREBIRD_IOS_INTERPRETER_PATCH_PATH to a Dart bytecode payload.'''
: '''
iOS interpreter patch bytecode does not exist: ${bytecodeFile.path}''',
);
throw ProcessExit(ExitCode.software.code);
}
final patchSnapshot = bytecodeFile.readAsBytesSync();
if (_looksLikeNativeExecutable(patchSnapshot)) {
logger.err(
'''
iOS interpreter patch payload must be Dart bytecode data, not a native Mach-O or ELF artifact: ${bytecodeFile.path}''',
);
throw ProcessExit(ExitCode.software.code);
}
final keyId = _requiredPatchValue(
'aot_patch_key_id or SHOREBIRD_AOT_PATCH_KEY_ID',
platform.environment['SHOREBIRD_AOT_PATCH_KEY_ID'] ??
shorebirdYaml.aotPatchKeyId,
);
final keyHex = _requiredPatchValue(
'SHOREBIRD_AOT_PATCH_KEY_HEX',
platform.environment['SHOREBIRD_AOT_PATCH_KEY_HEX'] ??
shorebirdYaml.aotPatchKeyHex,
);
final appBuildId = _requiredPatchValue(
'release version or aot_patch_app_build_id',
releaseVersion ??
platform.environment['SHOREBIRD_AOT_PATCH_APP_BUILD_ID'] ??
shorebirdYaml.aotPatchAppBuildId,
);
final flavorId = _requiredPatchValue(
'aot_patch_flavor_id',
platform.environment['SHOREBIRD_AOT_PATCH_FLAVOR_ID'] ??
shorebirdYaml.aotPatchFlavorId,
);
final licenseType = _requiredPatchValue(
'aot_patch_license_type',
platform.environment['SHOREBIRD_AOT_PATCH_LICENSE_TYPE'] ??
shorebirdYaml.aotPatchLicenseType,
);
final sdkHash = _requiredPatchValue(
'aot_patch_sdk_hash',
platform.environment['SHOREBIRD_AOT_PATCH_SDK_HASH'] ??
shorebirdYaml.aotPatchSdkHash,
);
final baseSnapshotHash = _requiredPatchValue(
'aot_patch_base_snapshot_hash',
platform.environment['SHOREBIRD_AOT_PATCH_BASE_SNAPSHOT_HASH'] ??
shorebirdYaml.aotPatchBaseSnapshotHash,
);
final baseFlavorId =
platform.environment['SHOREBIRD_AOT_PATCH_BASE_FLAVOR_ID'] ??
shorebirdYaml.aotPatchBaseFlavorId;
final baseLicenseType =
platform.environment['SHOREBIRD_AOT_PATCH_BASE_LICENSE_TYPE'] ??
shorebirdYaml.aotPatchBaseLicenseType;
final obfuscationMapHash =
platform.environment['SHOREBIRD_AOT_PATCH_OBFUSCATION_MAP_HASH'] ??
shorebirdYaml.aotPatchObfuscationMapHash;
final offlineExpiresAt = _normalizeOptionalIso8601Utc(
platform.environment['SHOREBIRD_AOT_PATCH_OFFLINE_EXPIRES_AT'] ??
shorebirdYaml.aotPatchOfflineExpiresAt,
);
final metadata = open_patch.PatchMetadata(
appId: appId,
appBuildId: appBuildId,
baseFlavorId: _emptyToNull(baseFlavorId),
baseLicenseType: _emptyToNull(baseLicenseType),
flavorId: flavorId,
licenseType: licenseType,
sdkHash: sdkHash,
baseSnapshotHash: baseSnapshotHash,
patchSnapshotHash: open_patch.sha256Hex(patchSnapshot),
targetOs: 'ios',
targetArch: 'arm64',
runtimeMode: open_patch.runtimeModeDartBytecodeInterpreter,
obfuscationMapHash: _emptyToNull(obfuscationMapHash),
offlineExpiresAt: offlineExpiresAt,
);
final artifact = open_patch.linkArtifacts(
baseSnapshot: const [],
patchSnapshot: patchSnapshot,
metadata: metadata,
forceFullSnapshot: true,
);
final encrypted = open_patch.encryptArtifact(
artifact: artifact,
keyId: keyId,
key: open_patch.readKey(keyHex),
nonce: _readOrCreateNonce(),
);
final outputFile = File(_interpreterPatchOutputPath)
..createSync(recursive: true)
..writeAsStringSync(
const JsonEncoder.withIndent(' ').convert(encrypted.toJson()),
);
final patchFileSize = outputFile.statSync().size;
final hash = sha256.convert(outputFile.readAsBytesSync()).toString();
final hashSignature = await signHash(hash);
logger.info(
'Created iOS interpreter patch artifact at ${outputFile.path}.',
);
return {
Arch.arm64: PatchArtifactBundle(
arch: 'aarch64',
path: outputFile.path,
hash: hash,
size: patchFileSize,
hashSignature: hashSignature,
podfileLockHash: shorebirdEnv.iosPodfileLockHash,
),
};
}
Future<void> _buildInterpreterBytecodePatch(List<String> buildArgs) async {
final shorebirdYaml = shorebirdEnv.getShorebirdYaml();
final configuredBytecodePath = _configuredInterpreterBytecodePath(
shorebirdYaml,
);
if (configuredBytecodePath != null) {
logger.info(
'Using configured iOS interpreter bytecode patch: '
'${_projectRelativeFile(configuredBytecodePath).path}.',
);
return;
}
final inputFile = _projectRelativeFile(
target ?? p.join('lib', 'main.dart'),
);
if (!inputFile.existsSync()) {
logger.err(
'Unable to find Dart entrypoint for iOS interpreter patch: '
'${inputFile.path}',
);
throw ProcessExit(ExitCode.usage.code);
}
final packageConfigFile = File(
p.join(projectRoot.path, '.dart_tool', 'package_config.json'),
);
await artifactBuilder.buildDartBytecodeSnapshot(
inputFilePath: inputFile.path,
outFilePath: _interpreterPatchBytecodeOutputPath,
packageConfigPath: packageConfigFile.existsSync()
? packageConfigFile.path
: null,
dartDefines: _dartDefinesForBytecode(buildArgs),
experiments: _buildArgValues(buildArgs, 'enable-experiment'),
);
}
String? _configuredInterpreterBytecodePath(ShorebirdYaml? shorebirdYaml) {
return _emptyToNull(
platform.environment['SHOREBIRD_IOS_INTERPRETER_PATCH_PATH'] ??
shorebirdYaml?.aotPatchBytecodePath,
);
}
File _projectRelativeFile(String path) {
if (p.isAbsolute(path)) {
return File(path);
}
return File(p.join(projectRoot.path, path));
}
String _requiredPatchValue(String name, String? value) {
if (value == null || value.trim().isEmpty) {
logger.err('Missing required iOS interpreter patch value: $name.');
throw ProcessExit(ExitCode.usage.code);
}
return value.trim();
}
List<String> _dartDefinesForBytecode(List<String> buildArgs) {
final defines = <String>[];
for (final defineFilePath in _buildArgValues(
buildArgs,
CommonArguments.dartDefineFromFileArg.name,
)) {
defines.addAll(_readDartDefineFile(_projectRelativeFile(defineFilePath)));
}
defines.addAll(
_buildArgValues(buildArgs, CommonArguments.dartDefineArg.name),
);
if (flavor != null &&
!defines.any((define) => define.startsWith('FLUTTER_APP_FLAVOR='))) {
defines.add('FLUTTER_APP_FLAVOR=$flavor');
}
_assertNoReservedFlutterDefines(defines);
defines.addAll(_flutterVersionDartDefinesForBytecode());
if (_enabledRuntimeFeatureDefinesForBytecode() case final featureDefine?) {
defines.add(featureDefine);
}
return defines;
}
void _assertNoReservedFlutterDefines(List<String> defines) {
for (final reservedDefine in _flutterReservedDefines) {
if (defines.any((define) => define.startsWith(reservedDefine))) {
logger.err(
'$reservedDefine is used by Flutter and cannot be set using --'
'${CommonArguments.dartDefineArg.name} or --'
'${CommonArguments.dartDefineFromFileArg.name}.',
);
throw ProcessExit(ExitCode.usage.code);
}
}
}
List<String> _flutterVersionDartDefinesForBytecode() {
final versionFile = File(
p.join(
shorebirdEnv.flutterDirectory.path,
'bin',
'cache',
'flutter.version.json',
),
);
if (!versionFile.existsSync()) return const [];
final Json versionJson;
try {
versionJson = (jsonDecode(versionFile.readAsStringSync()) as Map)
.cast<String, Object?>();
} on Object catch (error) {
logger.err('Unable to parse ${versionFile.path}: $error');
throw ProcessExit(ExitCode.software.code);
}
String? stringValue(String key) {
final value = versionJson[key];
return value is String && value.isNotEmpty ? value : null;
}
String? shortRevision(String key) {
final revision = stringValue(key);
if (revision == null) return null;
return revision.length > 10 ? revision.substring(0, 10) : revision;
}
return [
if (stringValue('frameworkVersion') case final value?)
'$_flutterVersionDefine=$value',
if (stringValue('channel') case final value?)
'$_flutterChannelDefine=$value',
if (stringValue('repositoryUrl') case final value?)
'$_flutterGitUrlDefine=$value',
if (shortRevision('frameworkRevision') case final value?)
'$_flutterFrameworkRevisionDefine=$value',
if (shortRevision('engineRevision') case final value?)
'$_flutterEngineRevisionDefine=$value',
if (stringValue('dartSdkVersion') case final value?)
'$_flutterDartVersionDefine=$value',
];
}
String? _enabledRuntimeFeatureDefinesForBytecode() {
final channel = _flutterVersionJsonString('channel');
final runtimeIds = <String>[];
for (final feature in _runtimeFeatureFlags) {
if (_isRuntimeFeatureEnabledForBytecode(
configSetting: feature.configSetting,
environmentOverride: feature.environmentOverride,
channel: channel,
)) {
runtimeIds.add(feature.runtimeId);
}
}
if (runtimeIds.isEmpty) return null;
return 'FLUTTER_ENABLED_FEATURE_FLAGS=${runtimeIds.join(',')}';
}
bool _isRuntimeFeatureEnabledForBytecode({
required String configSetting,
required String environmentOverride,
required String? channel,
}) {
if (channel == 'stable' || channel == 'beta') {
return false;
}
return _projectFeatureConfig(configSetting) ??
_globalFeatureConfig(configSetting) ??
_environmentFeatureConfig(environmentOverride) ??
false;
}
bool? _projectFeatureConfig(String configSetting) {
final config = shorebirdEnv.getPubspecYaml()?.flutter?['config'];
if (config == null) return null;
if (config is! Map) {
logger.err(
'The "config" property of "flutter" in pubspec.yaml must be a map.',
);
throw ProcessExit(ExitCode.usage.code);
}
return _featureConfigBool(
config[configSetting],
name: configSetting,
source: 'flutter: config: in pubspec.yaml',
);
}
bool? _globalFeatureConfig(String configSetting) {
return _featureConfigBool(
shorebirdFlutter.getConfig()[configSetting],
name: configSetting,
source: 'flutter config',
);
}
bool? _environmentFeatureConfig(String environmentOverride) {
final value = platform.environment[environmentOverride];
if (value == null) return null;
return value.toLowerCase() == 'true';
}
bool? _featureConfigBool(
Object? value, {
required String name,
required String source,
}) {
if (value == null || value == '(Not set)') return null;
if (value is bool) return value;
if (value is String) {
if (value.toLowerCase() == 'true') return true;
if (value.toLowerCase() == 'false') return false;
}
logger.err(
'The "$name" property in $source must be a boolean, but got $value.',
);
throw ProcessExit(ExitCode.usage.code);
}
String? _flutterVersionJsonString(String key) {
final versionFile = File(
p.join(
shorebirdEnv.flutterDirectory.path,
'bin',
'cache',
'flutter.version.json',
),
);
if (!versionFile.existsSync()) return null;
try {
final versionJson = (jsonDecode(versionFile.readAsStringSync()) as Map)
.cast<String, Object?>();
final value = versionJson[key];
return value is String && value.isNotEmpty ? value : null;
} on Object catch (error) {
logger.err('Unable to parse ${versionFile.path}: $error');
throw ProcessExit(ExitCode.software.code);
}
}
List<String> _buildArgValues(List<String> args, String name) {
final values = <String>[];
final longOption = '--$name';
for (var i = 0; i < args.length; i++) {
final arg = args[i];
if (arg == longOption && i + 1 < args.length) {
values.add(args[++i]);
} else if (arg.startsWith('$longOption=')) {
values.add(arg.substring(longOption.length + 1));
} else if (name == CommonArguments.dartDefineArg.name &&
arg.startsWith('-D') &&
arg.length > 2) {
values.add(arg.substring(2));
}
}
return values;
}
List<String> _readDartDefineFile(File file) {
if (!file.existsSync()) {
logger.err(
'Did not find the file passed to --'
'${CommonArguments.dartDefineFromFileArg.name}: ${file.path}',
);
throw ProcessExit(ExitCode.usage.code);
}
final raw = file.readAsStringSync();
if (raw.trimLeft().startsWith('{')) {
try {
final map = (jsonDecode(raw) as Map).cast<String, Object?>();
return [
for (final entry in map.entries) '${entry.key}=${entry.value}',
];
} on FormatException catch (error) {
logger.err(
'Unable to parse Dart define file ${file.path}: $error',
);
throw ProcessExit(ExitCode.usage.code);
}
}
final defines = <String>[];
for (final line in const LineSplitter().convert(raw)) {
final define = _parseDartDefineEnvLine(line);
if (define != null) defines.add(define);
}
return defines;
}
String? _parseDartDefineEnvLine(String line) {
final trimmed = line.trim();
if (trimmed.isEmpty || trimmed.startsWith('#')) return null;
final separatorIndex = trimmed.indexOf('=');
if (separatorIndex <= 0) {
logger.err(
'Invalid Dart define file line for --'
'${CommonArguments.dartDefineFromFileArg.name}: $line',
);
throw ProcessExit(ExitCode.usage.code);
}
final key = trimmed.substring(0, separatorIndex).trim();
var value = trimmed.substring(separatorIndex + 1).trim();
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
value = value.substring(1, value.length - 1);
}
return '$key=$value';
}
String? _emptyToNull(String? value) {
if (value == null || value.trim().isEmpty) return null;
return value.trim();
}
String? _normalizeOptionalIso8601Utc(String? value) {
final trimmed = _emptyToNull(value);
if (trimmed == null) return null;
final parsed = DateTime.tryParse(trimmed);
if (parsed == null) {
logger.err('Invalid aot_patch_offline_expires_at value: $trimmed.');
throw ProcessExit(ExitCode.usage.code);
}
return parsed.toUtc().toIso8601String();
}
List<int> _readOrCreateNonce() {
final nonceHex = platform.environment['SHOREBIRD_AOT_PATCH_NONCE_HEX'];
if (nonceHex != null && nonceHex.trim().isNotEmpty) {
return open_patch.readNonce(nonceHex);
}
final random = Random.secure();
return List<int>.generate(12, (_) => random.nextInt(256));
}
bool _looksLikeNativeExecutable(List<int> bytes) {
if (bytes.length < 4) return false;
if (bytes[0] == 0x7f &&
bytes[1] == 0x45 &&
bytes[2] == 0x4c &&
bytes[3] == 0x46) {
return true;
}
final magic = bytes
.take(4)
.fold<int>(0, (value, byte) => (value << 8) | byte);
return const {
0xfeedface,
0xfeedfacf,
0xcefaedfe,
0xcffaedfe,
0xcafebabe,
0xbebafeca,
}.contains(magic);
}
@override
Future<String> extractReleaseVersionFromArtifact(File artifact) async {
final archivePath = artifactManager.getXcarchiveDirectory()?.path;
@@ -62,6 +62,7 @@ class LinuxPatcher extends Patcher {
required int releaseId,
required File releaseArtifact,
Directory? supplementDirectory,
String? releaseVersion,
}) async {
final createDiffProgress = logger.progress('Creating patch artifacts');
final patchArtifactPath = p.join(
@@ -176,6 +176,7 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''');
required int releaseId,
required File releaseArtifact,
Directory? supplementDirectory,
String? releaseVersion,
}) async {
final unzipProgress = logger.progress('Extracting release artifact');
final releaseAppDirectory = Directory.systemTemp.createTempSync();
@@ -1,8 +1,8 @@
import 'dart:io';
import 'package:mason_logger/mason_logger.dart';
import 'package:path/path.dart' as p;
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;
import 'package:scoped_deps/scoped_deps.dart';
import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart';
import 'package:shorebird_cli/src/artifact_builder/build_trace_session.dart';
@@ -105,7 +105,7 @@ To target the latest release (e.g. the release that was most recently updated) u
[DEPRECATED] Whether to publish the patch to the staging environment. Use --track=staging instead.''',
hide: true,
)
// Added for https://github.com/shorebirdtech/shorebird/issues/3223.
// Added for https://git.tonycloud.org/flutter/shorebird/issues/3223.
// Can be removed fall 2026 or later.
..addFlag(
'confirm',
@@ -458,10 +458,13 @@ Building with Flutter $flutterVersionString to determine the release version...
// calls made by Apple patchers outside the Flutter build.
final extraBuildArgs = <String>[];
if (obfuscationMapFile != null) {
final loadObfuscationMapArg = [
'--extra-gen-snapshot-options=--load-obfuscation-map',
obfuscationMapFile.path,
].join('=');
extraBuildArgs.addAll([
'--obfuscate',
'--extra-gen-snapshot-options='
'--load-obfuscation-map=${obfuscationMapFile.path}',
loadObfuscationMapArg,
]);
// Gate --strip on the release's Flutter revision (not the user's
@@ -539,6 +542,7 @@ Building patch with Flutter $flutterVersionString
releaseId: release.id,
releaseArtifact: releaseArchive,
supplementDirectory: supplementDirectory,
releaseVersion: release.version,
);
final dryRun = results['dry-run'] == true;
@@ -663,7 +667,7 @@ Please re-run the release command for this version or create a new release.''');
required Patcher patcher,
}) async {
try {
return patcher.assertUnpatchableDiffs(
return await patcher.assertUnpatchableDiffs(
releaseArtifact: releaseArtifact,
releaseArchive: releaseArchive,
patchArchive: patchArchive,
@@ -68,7 +68,7 @@ More info: ${troubleshootingUrl.toLink()}.
List<String> extraBuildArgs = const [];
/// Additional gen_snapshot arguments needed to match the release's
/// obfuscation flags. Used by Apple patchers for [buildElfAotSnapshot]
/// obfuscation flags. Used by Apple patchers for `buildElfAotSnapshot`
/// and linker calls.
List<String> get obfuscationGenSnapshotArgs => [
if (obfuscationMapPath != null) ...[
@@ -125,6 +125,7 @@ More info: ${troubleshootingUrl.toLink()}.
required int releaseId,
required File releaseArtifact,
Directory? supplementDirectory,
String? releaseVersion,
});
/// Updates the provided metadata to include patcher-specific fields.
@@ -275,7 +276,7 @@ More info: ${troubleshootingUrl.toLink()}.
/// native changes, even though the user may not have actually changed any
/// code or dependencies.
///
/// Context: https://github.com/shorebirdtech/shorebird/issues/2270
/// Context: https://git.tonycloud.org/flutter/shorebird/issues/2270
List<String> buildNameAndNumberArgsFromReleaseVersion(
String? releaseVersion,
) {
@@ -89,6 +89,7 @@ class WindowsPatcher extends Patcher {
required int releaseId,
required File releaseArtifact,
Directory? supplementDirectory,
String? releaseVersion,
}) async {
final createDiffProgress = logger.progress('Creating patch artifacts');
final patchArtifactPath = p.join(
@@ -8,6 +8,7 @@ import 'package:shorebird_cli/src/extensions/arg_results.dart';
import 'package:shorebird_cli/src/json_output.dart';
import 'package:shorebird_cli/src/logging/logging.dart';
import 'package:shorebird_cli/src/shorebird_command.dart';
import 'package:shorebird_cli/src/shorebird_documentation.dart';
import 'package:shorebird_cli/src/shorebird_env.dart';
import 'package:shorebird_cli/src/shorebird_validator.dart';
@@ -111,7 +112,7 @@ class PromoteCommand extends ShorebirdCommand {
'''
No production channel found for app $appId.
This is a bug and should never happen. Please file an issue at https://github.com/shorebirdtech/shorebird/issues/new?assignees=&labels=bug&projects=&template=bug_report.md&title=fix%3A+''',
This is a bug and should never happen. Please file an issue at $openShorebirdIssueUrl''',
);
return ExitCode.software.code;
}
@@ -13,6 +13,7 @@ import 'package:shorebird_cli/src/platform/platform.dart';
import 'package:shorebird_cli/src/release_type.dart';
import 'package:shorebird_cli/src/shorebird_android_artifacts.dart';
import 'package:shorebird_cli/src/shorebird_env.dart';
import 'package:shorebird_cli/src/shorebird_process.dart';
import 'package:shorebird_cli/src/shorebird_validator.dart';
import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
@@ -167,7 +168,7 @@ dependencyResolutionManagement {
+ }
+ maven {
- url 'https://storage.googleapis.com/download.flutter.io'
+ url 'https://download.shorebird.dev/download.flutter.io'
+ url '${ShorebirdProcess.defaultFlutterStorageBaseUrl}'
+ }
}
}
@@ -13,6 +13,7 @@ import 'package:shorebird_cli/src/metadata/metadata.dart';
import 'package:shorebird_cli/src/platform/platform.dart';
import 'package:shorebird_cli/src/release_type.dart';
import 'package:shorebird_cli/src/shorebird_android_artifacts.dart';
import 'package:shorebird_cli/src/shorebird_documentation.dart';
import 'package:shorebird_cli/src/shorebird_validator.dart';
import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
@@ -100,7 +101,7 @@ To change the version of this release, change your app's version in your pubspec
Split APKs are each given a different release version than what is specified in the pubspec.yaml.
See ${link(uri: Uri.parse('https://github.com/flutter/flutter/issues/39817'))} for more information about this issue.
Please comment and upvote ${link(uri: Uri.parse('https://github.com/shorebirdtech/shorebird/issues/1141'))} if you would like shorebird to support this.''',
Please file an issue at ${link(uri: Uri.parse(openShorebirdIssueUrl))} if you would like shorebird to support this.''',
);
throw ProcessExit(ExitCode.unavailable.code);
}
@@ -12,6 +12,7 @@ import 'package:shorebird_cli/src/extensions/arg_results.dart';
import 'package:shorebird_cli/src/logging/shorebird_logger.dart';
import 'package:shorebird_cli/src/platform/platform.dart';
import 'package:shorebird_cli/src/release_type.dart';
import 'package:shorebird_cli/src/shorebird_documentation.dart';
import 'package:shorebird_cli/src/shorebird_env.dart';
import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart';
import 'package:shorebird_cli/src/validators/validators.dart';
@@ -143,6 +144,6 @@ class MacosReleaser extends Releaser with AppleReleaserMixin {
macOS app created at ${artifactManager.getMacOSAppDirectory(flavor: flavor)!.path}.
${styleBold.wrap('Note:')} If you distribute your app via the Mac App Store using a .pkg installer, the packaging process may modify the binary and cause patch failures. See ${link(uri: Uri.parse('https://github.com/shorebirdtech/shorebird/issues/3223'))} for more information.
${styleBold.wrap('Note:')} If you distribute your app via the Mac App Store using a .pkg installer, the packaging process may modify the binary and cause patch failures. See ${link(uri: Uri.parse(troubleshootingUrl))} for more information.
''';
}
@@ -125,7 +125,7 @@ Defaults to "latest" which builds using the latest stable Flutter version.''',
hide: true,
negatable: false,
)
// Added for https://github.com/shorebirdtech/shorebird/issues/3223.
// Added for https://git.tonycloud.org/flutter/shorebird/issues/3223.
// Can be removed fall 2026 or later.
..addFlag(
'confirm',
@@ -461,9 +461,7 @@ $error''');
if (revision == null) {
final openIssueLink = link(
uri: Uri.parse(
'https://github.com/shorebirdtech/shorebird/issues/new?assignees=&labels=feature&projects=&template=feature_request.md&title=feat%3A+',
),
uri: Uri.parse(openShorebirdIssueUrl),
message: 'open an issue',
);
logger.err('''
@@ -241,7 +241,7 @@ abstract class Releaser {
// upload, so an interruption between the two leaves the release without its
// supplement. Now that the supplement drives patching decisions (e.g.
// obfuscation), we should make this atomic or recoverable.
// https://github.com/shorebirdtech/shorebird/issues/3630
// https://git.tonycloud.org/flutter/shorebird/issues/3630
Future<void> uploadSupplementArtifact({
required String appId,
required int releaseId,
@@ -25,6 +25,19 @@ class ShorebirdYaml {
this.baseUrl,
this.autoUpdate,
this.patchVerification,
this.aotPatchRuntimeMode,
this.aotPatchBytecodePath,
this.aotPatchKeyId,
this.aotPatchKeyHex,
this.aotPatchAppBuildId,
this.aotPatchBaseFlavorId,
this.aotPatchBaseLicenseType,
this.aotPatchFlavorId,
this.aotPatchLicenseType,
this.aotPatchSdkHash,
this.aotPatchBaseSnapshotHash,
this.aotPatchObfuscationMapHash,
this.aotPatchOfflineExpiresAt,
});
/// Creates a [ShorebirdYaml] from a JSON map.
@@ -61,6 +74,45 @@ class ShorebirdYaml {
/// The patch verification mode for the app.
final PatchVerification? patchVerification;
/// Runtime mode used for open AOT patch artifacts.
final String? aotPatchRuntimeMode;
/// Path to a full Dart bytecode patch payload for the iOS interpreter route.
final String? aotPatchBytecodePath;
/// Key id used to encrypt open AOT patch artifacts.
final String? aotPatchKeyId;
/// Development-only AES-256 key used to encrypt open AOT patch artifacts.
final String? aotPatchKeyHex;
/// App build id to bind into open AOT patch artifacts.
final String? aotPatchAppBuildId;
/// Optional source flavor id this patch can replace.
final String? aotPatchBaseFlavorId;
/// Optional source license type this patch can replace.
final String? aotPatchBaseLicenseType;
/// Target flavor id after the patch is applied.
final String? aotPatchFlavorId;
/// Target license type after the patch is applied.
final String? aotPatchLicenseType;
/// SDK hash expected by the runtime for open AOT patch artifacts.
final String? aotPatchSdkHash;
/// Base bytecode snapshot hash expected by the runtime.
final String? aotPatchBaseSnapshotHash;
/// Optional obfuscation map hash expected by the runtime.
final String? aotPatchObfuscationMapHash;
/// Optional offline expiration timestamp for the patch artifact.
final String? aotPatchOfflineExpiresAt;
}
/// Extension on [ShorebirdYaml] to get the app id for a specific flavor.
@@ -20,6 +20,19 @@ ShorebirdYaml _$ShorebirdYamlFromJson(Map json) => $checkedCreate(
'base_url',
'auto_update',
'patch_verification',
'aot_patch_runtime_mode',
'aot_patch_bytecode_path',
'aot_patch_key_id',
'aot_patch_key_hex',
'aot_patch_app_build_id',
'aot_patch_base_flavor_id',
'aot_patch_base_license_type',
'aot_patch_flavor_id',
'aot_patch_license_type',
'aot_patch_sdk_hash',
'aot_patch_base_snapshot_hash',
'aot_patch_obfuscation_map_hash',
'aot_patch_offline_expires_at',
],
);
final val = ShorebirdYaml(
@@ -34,6 +47,55 @@ ShorebirdYaml _$ShorebirdYamlFromJson(Map json) => $checkedCreate(
'patch_verification',
(v) => $enumDecodeNullable(_$PatchVerificationEnumMap, v),
),
aotPatchRuntimeMode: $checkedConvert(
'aot_patch_runtime_mode',
(v) => v as String?,
),
aotPatchBytecodePath: $checkedConvert(
'aot_patch_bytecode_path',
(v) => v as String?,
),
aotPatchKeyId: $checkedConvert('aot_patch_key_id', (v) => v as String?),
aotPatchKeyHex: $checkedConvert(
'aot_patch_key_hex',
(v) => v as String?,
),
aotPatchAppBuildId: $checkedConvert(
'aot_patch_app_build_id',
(v) => v as String?,
),
aotPatchBaseFlavorId: $checkedConvert(
'aot_patch_base_flavor_id',
(v) => v as String?,
),
aotPatchBaseLicenseType: $checkedConvert(
'aot_patch_base_license_type',
(v) => v as String?,
),
aotPatchFlavorId: $checkedConvert(
'aot_patch_flavor_id',
(v) => v as String?,
),
aotPatchLicenseType: $checkedConvert(
'aot_patch_license_type',
(v) => v as String?,
),
aotPatchSdkHash: $checkedConvert(
'aot_patch_sdk_hash',
(v) => v as String?,
),
aotPatchBaseSnapshotHash: $checkedConvert(
'aot_patch_base_snapshot_hash',
(v) => v as String?,
),
aotPatchObfuscationMapHash: $checkedConvert(
'aot_patch_obfuscation_map_hash',
(v) => v as String?,
),
aotPatchOfflineExpiresAt: $checkedConvert(
'aot_patch_offline_expires_at',
(v) => v as String?,
),
);
return val;
},
@@ -42,6 +104,19 @@ ShorebirdYaml _$ShorebirdYamlFromJson(Map json) => $checkedCreate(
'baseUrl': 'base_url',
'autoUpdate': 'auto_update',
'patchVerification': 'patch_verification',
'aotPatchRuntimeMode': 'aot_patch_runtime_mode',
'aotPatchBytecodePath': 'aot_patch_bytecode_path',
'aotPatchKeyId': 'aot_patch_key_id',
'aotPatchKeyHex': 'aot_patch_key_hex',
'aotPatchAppBuildId': 'aot_patch_app_build_id',
'aotPatchBaseFlavorId': 'aot_patch_base_flavor_id',
'aotPatchBaseLicenseType': 'aot_patch_base_license_type',
'aotPatchFlavorId': 'aot_patch_flavor_id',
'aotPatchLicenseType': 'aot_patch_license_type',
'aotPatchSdkHash': 'aot_patch_sdk_hash',
'aotPatchBaseSnapshotHash': 'aot_patch_base_snapshot_hash',
'aotPatchObfuscationMapHash': 'aot_patch_obfuscation_map_hash',
'aotPatchOfflineExpiresAt': 'aot_patch_offline_expires_at',
},
);
@@ -53,6 +128,19 @@ Map<String, dynamic> _$ShorebirdYamlToJson(
'base_url': instance.baseUrl,
'auto_update': instance.autoUpdate,
'patch_verification': _$PatchVerificationEnumMap[instance.patchVerification],
'aot_patch_runtime_mode': instance.aotPatchRuntimeMode,
'aot_patch_bytecode_path': instance.aotPatchBytecodePath,
'aot_patch_key_id': instance.aotPatchKeyId,
'aot_patch_key_hex': instance.aotPatchKeyHex,
'aot_patch_app_build_id': instance.aotPatchAppBuildId,
'aot_patch_base_flavor_id': instance.aotPatchBaseFlavorId,
'aot_patch_base_license_type': instance.aotPatchBaseLicenseType,
'aot_patch_flavor_id': instance.aotPatchFlavorId,
'aot_patch_license_type': instance.aotPatchLicenseType,
'aot_patch_sdk_hash': instance.aotPatchSdkHash,
'aot_patch_base_snapshot_hash': instance.aotPatchBaseSnapshotHash,
'aot_patch_obfuscation_map_hash': instance.aotPatchObfuscationMapHash,
'aot_patch_offline_expires_at': instance.aotPatchOfflineExpiresAt,
};
const _$PatchVerificationEnumMap = {
@@ -55,7 +55,7 @@ class PatchExecutable {
// A Windows-specific error code indicating that the Microsoft C++ runtime
// (VCRUNTIME140.dll) could not be found.
// More info: https://github.com/shorebirdtech/shorebird/issues/2329
// More info: https://git.tonycloud.org/flutter/shorebird/issues/2329
const vcRuntimeNotFoundExitCode = -1073741515;
if (result.exitCode == vcRuntimeNotFoundExitCode && platform.isWindows) {
messageDetails =
@@ -11,8 +11,8 @@ part 'build_environment_metadata.g.dart';
/// later failures in their builds.
///
/// We do not collect Personally Identifying Information (e.g. no paths,
/// argument lists, etc.) in accordance with our privacy policy:
/// https://shorebird.dev/privacy/
/// argument lists, etc.) in accordance with the configured server privacy
/// policy.
/// {@endtemplate}
@JsonSerializable()
class BuildEnvironmentMetadata extends Equatable {
@@ -12,8 +12,8 @@ part 'create_patch_metadata.g.dart';
/// later failures in their builds.
///
/// We do not collect Personally Identifying Information (e.g. no paths,
/// argument lists, etc.) in accordance with our privacy policy:
/// https://shorebird.dev/privacy/
/// argument lists, etc.) in accordance with the configured server privacy
/// policy.
/// {@endtemplate}
@JsonSerializable()
class CreatePatchMetadata extends Equatable {
@@ -12,8 +12,8 @@ part 'update_release_metadata.g.dart';
/// later failures in their builds.
///
/// We do not collect Personally Identifying Information (e.g. no paths,
/// argument lists, etc.) in accordance with our privacy policy:
/// https://shorebird.dev/privacy/
/// argument lists, etc.) in accordance with the configured server privacy
/// policy.
/// {@endtemplate}
@JsonSerializable()
class UpdateReleaseMetadata extends Equatable {
@@ -11,6 +11,7 @@ import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
import 'package:shorebird_cli/src/formatters/formatters.dart';
import 'package:shorebird_cli/src/http_client/http_client.dart';
import 'package:shorebird_cli/src/logging/logging.dart';
import 'package:shorebird_cli/src/shorebird_env.dart';
/// A reference to a [NetworkChecker] instance.
final networkCheckerRef = create(NetworkChecker.new);
@@ -35,13 +36,16 @@ class NetworkCheckerException implements Exception {
/// {@endtemplate}
class NetworkChecker {
/// The URLs to check for network reachability.
static final urlsToCheck = [
'https://api.shorebird.dev',
'https://console.shorebird.dev',
'https://oauth2.googleapis.com',
'https://storage.googleapis.com',
'https://cdn.shorebird.cloud',
].map(Uri.parse).toList();
static List<Uri> get urlsToCheck {
final hostedUri =
shorebirdEnv.hostedUri ?? Uri.parse(ShorebirdEnv.defaultHostedUrl);
final urls = [hostedUri, shorebirdEnv.authServiceUri];
final seen = <String>{};
return [
for (final url in urls)
if (seen.add(url.toString())) url,
];
}
/// Verify that each of [urlsToCheck] responds to an HTTP GET request.
Future<void> checkReachability() async {
@@ -102,7 +102,7 @@ class Apple {
// Ideally, we would use `xcodebuild -list` to detect schemes/flavors.
// Unfortunately, many projects contain schemes that are not flavors, and we
// don't want to create flavors for these schemes. See
// https://github.com/shorebirdtech/shorebird/issues/1703 for an example.
// https://git.tonycloud.org/flutter/shorebird/issues/1703 for an example.
// Instead, we look in `[platform]/Runner.xcodeproj/xcshareddata/xcschemes`
// for xcscheme files (which seem to be 1-to-1 with schemes in Xcode) and
// filter out schemes that are marked as "wasCreatedForAppExtension".
@@ -28,6 +28,15 @@ enum ShorebirdArtifact {
/// The gen_snapshot executable for macOS that creates x64 snapshots.
genSnapshotMacosX64,
/// The host dartaotruntime executable.
dartAotRuntime,
/// The host dart2bytecode snapshot.
dart2BytecodeSnapshot,
/// The Flutter product platform dill used to compile Flutter bytecode.
flutterProductPlatformDill,
}
/// A reference to a [ShorebirdArtifacts] instance.
@@ -68,6 +77,12 @@ class ShorebirdCachedArtifacts implements ShorebirdArtifacts {
return _genSnapshotMacOsArm64File.path;
case ShorebirdArtifact.genSnapshotMacosX64:
return _genSnapshotMacOsX64File.path;
case ShorebirdArtifact.dartAotRuntime:
return _dartAotRuntimeFile.path;
case ShorebirdArtifact.dart2BytecodeSnapshot:
return _dart2BytecodeSnapshotFile.path;
case ShorebirdArtifact.flutterProductPlatformDill:
return _flutterProductPlatformDillFile.path;
}
}
@@ -164,6 +179,63 @@ class ShorebirdCachedArtifacts implements ShorebirdArtifacts {
),
);
}
File get _dartAotRuntimeFile {
return File(
p.join(
shorebirdEnv.flutterDirectory.path,
'bin',
'cache',
'dart-sdk',
'bin',
'dartaotruntime',
),
);
}
File get _dart2BytecodeSnapshotFile {
return File(
p.join(
shorebirdEnv.flutterDirectory.path,
'bin',
'cache',
'dart-sdk',
'bin',
'snapshots',
'dart2bytecode.dart.snapshot',
),
);
}
File get _flutterProductPlatformDillFile {
final workspaceIosPlatformDill = File(
p.join(
shorebirdEnv.flutterDirectory.path,
'engine',
'src',
'out',
'ios_release',
'flutter_patched_sdk',
'platform_strong.dill',
),
);
if (workspaceIosPlatformDill.existsSync()) {
return workspaceIosPlatformDill;
}
return File(
p.join(
shorebirdEnv.flutterDirectory.path,
'bin',
'cache',
'artifacts',
'engine',
'common',
'flutter_patched_sdk_product',
'platform_strong.dill',
),
);
}
}
/// {@template shorebird_local_engine_artifacts}
@@ -188,6 +260,12 @@ class ShorebirdLocalEngineArtifacts implements ShorebirdArtifacts {
return _genSnapshotMacosArm64File.path;
case ShorebirdArtifact.genSnapshotMacosX64:
return _genSnapshotMacosX64File.path;
case ShorebirdArtifact.dartAotRuntime:
return _dartAotRuntimeFile.path;
case ShorebirdArtifact.dart2BytecodeSnapshot:
return _dart2BytecodeSnapshotFile.path;
case ShorebirdArtifact.flutterProductPlatformDill:
return _flutterProductPlatformDillFile.path;
}
}
@@ -265,4 +343,57 @@ class ShorebirdLocalEngineArtifacts implements ShorebirdArtifacts {
),
);
}
File get _dartAotRuntimeFile {
final localEngineHost = engineConfig.localEngineHost;
if (localEngineHost == null) {
throw StateError(
'localEngineHost is required for local engine artifacts',
);
}
return File(
p.join(
engineConfig.localEngineSrcPath!,
'out',
localEngineHost,
'dartaotruntime',
),
);
}
File get _dart2BytecodeSnapshotFile {
final localEngineHost = engineConfig.localEngineHost;
if (localEngineHost == null) {
throw StateError(
'localEngineHost is required for local engine artifacts',
);
}
return File(
p.join(
engineConfig.localEngineSrcPath!,
'out',
localEngineHost,
'dart-sdk',
'bin',
'snapshots',
'dart2bytecode.dart.snapshot',
),
);
}
File get _flutterProductPlatformDillFile {
final localEngine = engineConfig.localEngine;
if (localEngine == null) {
throw StateError('localEngine is required for local engine artifacts');
}
return File(
p.join(
engineConfig.localEngineSrcPath!,
'out',
localEngine,
'flutter_patched_sdk',
'platform_strong.dill',
),
);
}
}
@@ -12,6 +12,7 @@ import 'package:shorebird_cli/src/json_output.dart';
import 'package:shorebird_cli/src/logging/logging.dart';
import 'package:shorebird_cli/src/platform.dart';
import 'package:shorebird_cli/src/shorebird_artifacts.dart';
import 'package:shorebird_cli/src/shorebird_documentation.dart';
import 'package:shorebird_cli/src/shorebird_env.dart';
import 'package:shorebird_cli/src/shorebird_flutter.dart';
import 'package:shorebird_cli/src/shorebird_process.dart';
@@ -259,7 +260,7 @@ ${lightCyan.wrap('shorebird release android -- --no-pub lib/main.dart')}''';
shorebirdFlutterPrefix.write(' $flutterVersion');
}
logger.info('''
Shorebird $packageVersion git@github.com:shorebirdtech/shorebird.git
Shorebird $packageVersion https://git.tonycloud.org/flutter/shorebird.git
$shorebirdFlutterPrefix revision ${shorebirdEnv.flutterRevision}
Engine revision ${shorebirdEnv.shorebirdEngineRevision}''');
}
@@ -340,9 +341,7 @@ Engine • revision ${shorebirdEnv.shorebirdEngineRevision}''');
exitCode != ExitCode.success.code &&
logger.level != Level.verbose) {
final fileAnIssue = link(
uri: Uri.parse(
'https://github.com/shorebirdtech/shorebird/issues/new/choose',
),
uri: Uri.parse(openShorebirdIssueUrl),
message: 'file an issue',
);
logger.info('''
@@ -1,8 +1,15 @@
// cspell:words swiftobjective ckotlinjava havent
import 'package:mason_logger/mason_logger.dart';
/// Link to the Shorebird documentation page.
const docsUrl = 'https://docs.shorebird.dev';
/// Link to the open Shorebird repository.
const openShorebirdRepositoryUrl =
'https://git.tonycloud.org/flutter/shorebird';
/// Link to the open Shorebird issue tracker.
const openShorebirdIssueUrl = '$openShorebirdRepositoryUrl/issues/new';
/// Link to the open Shorebird documentation page.
const docsUrl = '$openShorebirdRepositoryUrl/src/branch/main/docs';
/// Link to the Flutter version page on the Shorebird documentation.
const flutterVersionUrl = '$docsUrl/getting-started/flutter-version';
@@ -57,6 +57,15 @@ class ShorebirdEnv {
final String? _flutterRevisionOverride;
final String? _flutterProjectRootOverride;
/// Default URL for the open self-hosted Shorebird server.
static const defaultHostedUrl = 'http://localhost:8080';
/// Default auth service URL for the open self-hosted Shorebird server.
static const defaultAuthServiceUrl = '$defaultHostedUrl/auth';
/// Default JWT issuer used by the open self-hosted server.
static const defaultJwtIssuer = 'shorebird-auth';
/// The application config directory for the Shorebird CLI.
Directory get configDirectory {
return Directory(applicationConfigHome(executableName));
@@ -69,21 +78,45 @@ class ShorebirdEnv {
/// The root directory of the Shorebird install.
///
/// Assumes we are running from $ROOT/bin/cache.
/// Historically the CLI was launched from a snapshot under $ROOT/bin/cache.
/// Compiled open-source CLI bundles place the executable under $ROOT/bin, so
/// first look for the install metadata and fall back to the legacy layout.
Directory get shorebirdRoot {
return File(platform.script.toFilePath()).parent.parent.parent;
final scriptDirectory = File(platform.script.toFilePath()).parent;
var candidate = scriptDirectory;
while (true) {
final flutterVersionFile = File(
p.join(candidate.path, 'bin', 'internal', 'flutter.version'),
);
if (flutterVersionFile.existsSync()) return candidate;
final parent = candidate.parent;
if (parent.path == candidate.path) break;
candidate = parent;
}
return scriptDirectory.parent.parent;
}
/// The Shorebird engine revision.
String get shorebirdEngineRevision {
final file = File(
p.join(flutterDirectory.path, 'bin', 'internal', 'engine.version'),
);
try {
return file.readAsStringSync().trim();
} on FileSystemException {
throw CacheCorruptedException('Could not read ${file.path}.');
final files = [
File(p.join(flutterDirectory.path, 'bin', 'internal', 'engine.version')),
File(p.join(shorebirdRoot.path, 'bin', 'internal', 'engine.version')),
];
for (final file in files) {
if (!file.existsSync()) continue;
try {
return file.readAsStringSync().trim();
} on FileSystemException {
break;
}
}
throw CacheCorruptedException(
'Could not read ${files.map((file) => file.path).join(' or ')}.',
);
}
/// Get the Shorebird Flutter revision.
@@ -259,31 +292,29 @@ class ShorebirdEnv {
}
/// The base URL for the Shorebird auth service. Can be overridden with the
/// `AUTH_SERVICE_URL` environment variable. Defaults to
/// `https://auth.shorebird.dev`.
/// `AUTH_SERVICE_URL` environment variable. Defaults to the open self-hosted
/// server auth endpoint.
Uri get authServiceUri => Uri.parse(
platform.environment['AUTH_SERVICE_URL'] ?? 'https://auth.shorebird.dev',
platform.environment['AUTH_SERVICE_URL'] ?? defaultAuthServiceUrl,
);
/// The expected JWT issuer for Shorebird-issued tokens. Can be overridden
/// with the `SHOREBIRD_JWT_ISSUER` environment variable. Defaults to
/// `https://auth.shorebird.dev`.
/// with the `SHOREBIRD_JWT_ISSUER` environment variable. Defaults to the open
/// self-hosted server issuer.
String get jwtIssuer =>
platform.environment['SHOREBIRD_JWT_ISSUER'] ??
'https://auth.shorebird.dev';
platform.environment['SHOREBIRD_JWT_ISSUER'] ?? defaultJwtIssuer;
/// The base URL for the Shorebird code push server that overrides the default
/// used by [CodePushClient]. If none is provided, [CodePushClient] will use
/// its default.
/// The base URL for the Shorebird code push server.
Uri? get hostedUri {
try {
final baseUrl =
platform.environment['SHOREBIRD_HOSTED_URL'] ??
getShorebirdYaml()?.baseUrl ??
userConfig.hostedUri?.toString();
userConfig.hostedUri?.toString() ??
defaultHostedUrl;
return _parseHostedUri(baseUrl);
} on Exception {
return null;
return Uri.parse(defaultHostedUrl);
}
}
@@ -31,8 +31,17 @@ class ShorebirdFlutter {
static const executable = 'flutter';
/// The Shorebird Flutter fork git URL.
static const String flutterGitUrl =
'https://github.com/shorebirdtech/flutter.git';
static const String defaultFlutterGitUrl =
'https://git.tonycloud.org/flutter/flutter.git';
/// The Flutter fork git URL used when installing vended Flutter revisions.
String get flutterGitUrl {
final override = platform.environment['SHOREBIRD_FLUTTER_GIT_URL'];
if (override != null && override.trim().isNotEmpty) {
return override.trim();
}
return defaultFlutterGitUrl;
}
/// Arguments to pass to `flutter precache`.
List<String> get precacheArgs => ['--android', if (platform.isMacOS) '--ios'];
@@ -25,6 +25,10 @@ class ShorebirdProcess {
ProcessWrapper? processWrapper, // For mocking ShorebirdProcess.
}) : processWrapper = processWrapper ?? ProcessWrapper();
/// Default Flutter artifact mirror for the open self-hosted setup.
static const defaultFlutterStorageBaseUrl =
'${ShorebirdEnv.defaultHostedUrl}/download.flutter.io';
/// The underlying process wrapper.
final ProcessWrapper processWrapper;
@@ -38,7 +42,7 @@ class ShorebirdProcess {
/// `bin/shorebird.dart`, so `flutter build` stderr is absent from the
/// shorebird log file on a build failure users see the real error on
/// screen but the log only has `Failed to build AAB. Exited with code 1`
/// (https://github.com/shorebirdtech/shorebird/issues/3703). Piping
/// (https://git.tonycloud.org/flutter/shorebird/issues/3703). Piping
/// through Dart would capture stderr but turns `stdout.hasTerminal` false
/// on the child side, regressing the interactive UX; a pty or per-fd
/// shell tee would fix both but costs a dependency / POSIX-only path.
@@ -266,7 +270,12 @@ $stderr''');
if (executable == 'flutter') {
// If this ever changes we also need to update the `shorebird` shell
// wrapper which downloads runs Flutter to fetch artifacts the first time.
return {'FLUTTER_STORAGE_BASE_URL': 'https://download.shorebird.dev'};
return {
'FLUTTER_STORAGE_BASE_URL':
platform.environment['SHOREBIRD_FLUTTER_STORAGE_BASE_URL'] ??
platform.environment['FLUTTER_STORAGE_BASE_URL'] ??
defaultFlutterStorageBaseUrl,
};
}
return {};
@@ -5,6 +5,7 @@ import 'package:shorebird_cli/src/auth/auth.dart';
import 'package:shorebird_cli/src/logging/logging.dart';
import 'package:shorebird_cli/src/platform.dart';
import 'package:shorebird_cli/src/shorebird_env.dart';
import 'package:shorebird_cli/src/shorebird_web_console.dart';
import 'package:shorebird_cli/src/validators/validators.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
@@ -83,7 +84,7 @@ class ShorebirdValidator {
'''If you already have an account, run ${lightCyan.wrap('shorebird login')} to sign in.''',
)
..info(
'''If you don't have a Shorebird account, go to ${link(uri: Uri.parse('https://console.shorebird.dev'))} to create one.''',
'''If you don't have a Shorebird account, go to ${link(uri: ShorebirdWebConsole.uri(''))} to create one.''',
);
throw UserNotAuthorizedException();
}
@@ -1,8 +1,18 @@
import 'package:shorebird_cli/src/shorebird_env.dart';
/// Shorebird Web Console URLs.
class ShorebirdWebConsole {
/// Returns a [Uri] for the Shorebird Web Console.
static Uri uri(String path) {
return Uri.parse('https://console.shorebird.dev/$path');
final baseUri =
shorebirdEnv.hostedUri ?? Uri.parse(ShorebirdEnv.defaultHostedUrl);
final pathSegments = [
...baseUri.pathSegments.where((segment) => segment.isNotEmpty),
...path.split('/').where((segment) => segment.isNotEmpty),
];
return baseUri.replace(
pathSegments: pathSegments,
);
}
/// Returns a [Uri] for the Shorebird Web Console login page.
@@ -8,7 +8,7 @@ import 'package:xml/xml.dart';
/// Checks that android/app/src/main/AndroidManifest.xml contains the INTERNET
/// permission, which is required for Shorebird to work.
///
/// See https://github.com/shorebirdtech/shorebird/issues/160.
/// See https://git.tonycloud.org/flutter/shorebird/issues/160.
class AndroidInternetPermissionValidator extends Validator {
/// Path to the main AndroidManifest.xml file.
final String _mainAndroidManifestPath = p.join(
+3 -1
View File
@@ -1,7 +1,7 @@
name: shorebird_cli
description: Command-line tool to interact with Shorebird's services.
version: 1.6.108
repository: https://github.com/shorebirdtech/shorebird
repository: https://git.tonycloud.org/flutter/shorebird-workspace
resolution: workspace
publish_to: none
@@ -31,6 +31,8 @@ dependencies:
path: ../jwt
mason_logger: ^0.3.5
meta: ^1.16.0
open_aot_patch_tools:
path: ../open_aot_patch_tools
path: ^1.9.1
pem: ^2.0.5
platform: ^3.1.6
@@ -1469,7 +1469,7 @@ Reason: Exited with code 70.'''),
.having(
(e) => e.fixRecommendation,
'fixRecommendation',
'''Please file a bug at https://github.com/shorebirdtech/shorebird/issues/new with the logs for this command.''',
'''Please file a bug at https://git.tonycloud.org/flutter/shorebird/issues/new with the logs for this command.''',
),
),
);
@@ -1710,7 +1710,7 @@ Reason: Exited with code 70.'''),
.having(
(e) => e.fixRecommendation,
'fixRecommendation',
'''Please file a bug at https://github.com/shorebirdtech/shorebird/issues/new with the logs for this command.''',
'''Please file a bug at https://git.tonycloud.org/flutter/shorebird/issues/new with the logs for this command.''',
),
),
);
@@ -1924,7 +1924,7 @@ Reason: Exited with code 70.'''),
.having(
(e) => e.fixRecommendation,
'fixRecommendation',
'''Please file a bug at https://github.com/shorebirdtech/shorebird/issues/new with the logs for this command.''',
'''Please file a bug at https://git.tonycloud.org/flutter/shorebird/issues/new with the logs for this command.''',
),
),
);
@@ -2039,6 +2039,106 @@ Reason: Exited with code 70.'''),
});
});
});
group('buildDartBytecodeSnapshot', () {
setUp(() {
when(
() => shorebirdArtifacts.getArtifactPath(
artifact: ShorebirdArtifact.dartAotRuntime,
),
).thenReturn('dartaotruntime');
when(
() => shorebirdArtifacts.getArtifactPath(
artifact: ShorebirdArtifact.dart2BytecodeSnapshot,
),
).thenReturn('dart2bytecode.dart.snapshot');
when(
() => shorebirdArtifacts.getArtifactPath(
artifact: ShorebirdArtifact.flutterProductPlatformDill,
),
).thenReturn('platform_strong.dill');
});
test('invokes dart2bytecode with release Flutter arguments', () async {
final outFile = await runWithOverrides(
() => builder.buildDartBytecodeSnapshot(
inputFilePath: '/app/lib/main.dart',
outFilePath: '/path/to/out.bytecode',
packageConfigPath: '/app/.dart_tool/package_config.json',
dartDefines: ['FOO=bar'],
experiments: ['records'],
),
);
expect(outFile.path, '/path/to/out.bytecode');
verify(
() => shorebirdProcess.stream(
'dartaotruntime',
[
'dart2bytecode.dart.snapshot',
'--platform=platform_strong.dill',
'--target=flutter',
'--packages=/app/.dart_tool/package_config.json',
'-DFOO=bar',
'-Ddart.vm.profile=false',
'-Ddart.vm.product=true',
'--enable-experiment=records',
'-o',
'/path/to/out.bytecode',
'/app/lib/main.dart',
],
runInShell: false,
),
).called(1);
});
test('omits package config when none is provided', () async {
await runWithOverrides(
() => builder.buildDartBytecodeSnapshot(
inputFilePath: '/app/lib/main.dart',
outFilePath: '/path/to/out.bytecode',
),
);
final captured =
verify(
() => shorebirdProcess.stream(
'dartaotruntime',
captureAny(),
runInShell: false,
),
).captured.single
as List<String>;
expect(
captured,
isNot(contains('--packages=/app/.dart_tool/package_config.json')),
);
});
group('when build fails', () {
setUp(() {
when(
() => shorebirdProcess.stream(
any(),
any(),
runInShell: any(named: 'runInShell'),
),
).thenAnswer((_) async => ExitCode.software.code);
});
test('throws ArtifactBuildException', () {
expect(
() => runWithOverrides(
() => builder.buildDartBytecodeSnapshot(
inputFilePath: '/app/lib/main.dart',
outFilePath: '/path/to/out.bytecode',
),
),
throwsA(isA<ArtifactBuildException>()),
);
});
});
});
});
group('buildWindowsApp', () {
@@ -974,7 +974,7 @@ void main() {
'`shorebird login:ci`. '
'This format is deprecated and will stop working in a future '
'release. '
'Create an API key at https://console.shorebird.dev instead.',
'Create an API key at ${ShorebirdEnv.defaultHostedUrl} instead.',
),
).called(1);
verify(
+118 -30
View File
@@ -173,6 +173,52 @@ void main() {
});
});
group('artifactBaseUrl', () {
test('uses the default open artifact mirror root', () {
final url = runWithOverrides(() => cache.artifactBaseUrl);
expect(
url,
equals(Cache.defaultArtifactBaseUrl),
);
});
test('uses SHOREBIRD_ARTIFACT_BASE_URL when set', () {
when(() => platform.environment).thenReturn({
'SHOREBIRD_ARTIFACT_BASE_URL': 'https://artifacts.example.com/open/',
});
final url = runWithOverrides(() => cache.artifactBaseUrl);
expect(url, equals('https://artifacts.example.com/open'));
});
test('can override the storage base URL and bucket separately', () {
when(() => platform.environment).thenReturn({
'SHOREBIRD_STORAGE_BASE_URL': 'https://storage.example.com/',
'SHOREBIRD_STORAGE_BUCKET': '/open-shorebird/',
});
final url = runWithOverrides(() => cache.artifactBaseUrl);
expect(url, equals('https://storage.example.com/open-shorebird'));
});
test('builds per-engine artifact URLs from the configured root', () {
when(() => platform.environment).thenReturn({
'SHOREBIRD_ARTIFACT_BASE_URL': 'https://artifacts.example.com/open/',
});
final url = runWithOverrides(
() => cache.shorebirdArtifactUrl('patch-linux-x64.zip'),
);
expect(
url,
equals(
'https://artifacts.example.com/open/shorebird/'
'$shorebirdEngineRevision/patch-linux-x64.zip',
),
);
});
});
group('clear', () {
test('deletes the cache directory', () async {
final shorebirdCacheDirectory = runWithOverrides(
@@ -357,33 +403,41 @@ void main() {
);
});
test('skips optional artifacts if a 404 is returned', () async {
when(() => httpClient.send(any())).thenAnswer((invocation) async {
final request =
invocation.positionalArguments.first as http.BaseRequest;
final fileName = p.basename(request.url.path);
if (fileName.startsWith('aot-tools')) {
test(
'skips optional legacy aot-tools if enabled and a 404 is returned',
() async {
when(() => platform.environment).thenReturn({
Cache.legacyAotToolsEnvironmentVariable: '1',
});
cache = runWithOverrides(Cache.new);
when(() => httpClient.send(any())).thenAnswer((invocation) async {
final request =
invocation.positionalArguments.first as http.BaseRequest;
final fileName = p.basename(request.url.path);
if (fileName.startsWith('aot-tools')) {
return http.StreamedResponse(
const Stream.empty(),
HttpStatus.notFound,
reasonPhrase: 'Not Found',
);
}
return http.StreamedResponse(
const Stream.empty(),
HttpStatus.notFound,
reasonPhrase: 'Not Found',
Stream.value(ZipEncoder().encode(Archive())),
HttpStatus.ok,
);
}
return http.StreamedResponse(
Stream.value(ZipEncoder().encode(Archive())),
HttpStatus.ok,
});
await expectLater(
runWithOverrides(() => cache.updateAll(Duration.zero)),
completes,
);
});
await expectLater(
runWithOverrides(() => cache.updateAll(Duration.zero)),
completes,
);
verify(
() => logger.detail(
'''[cache] optional artifact: "aot-tools.dill" was not found, skipping...''',
),
).called(1);
});
verify(
() => logger.detail(
'''[cache] optional artifact: "aot-tools.dill" was not found, skipping...''',
),
).called(1);
},
);
test('downloads correct artifacts', () async {
final patchArtifactDirectory = runWithOverrides(
@@ -397,6 +451,22 @@ void main() {
expect(patchArtifactDirectory.existsSync(), isTrue);
});
test('does not download legacy aot-tools by default', () async {
await expectLater(
runWithOverrides(() => cache.updateAll(Duration.zero)),
completes,
);
final requests = verify(
() => httpClient.send(captureAny()),
).captured.cast<http.BaseRequest>().map((r) => r.url).toList();
expect(
requests.map((request) => request.path).join('\n'),
isNot(contains('aot-tools.dill')),
);
});
group('when extraction fails', () {
setUp(() {
when(
@@ -454,12 +524,11 @@ void main() {
).captured.cast<http.BaseRequest>().map((r) => r.url).toList();
String perEngine(String name) =>
'${cache.storageBaseUrl}/${cache.storageBucket}/shorebird/$shorebirdEngineRevision/$name';
'${cache.artifactBaseUrl}/shorebird/$shorebirdEngineRevision/$name';
final expected = [
perEngine('patch-darwin-x64.zip'),
'https://github.com/google/bundletool/releases/download/1.18.1/bundletool-all-1.18.1.jar',
perEngine('aot-tools.dill'),
].map(Uri.parse).toList();
expect(requests, equals(expected));
@@ -478,12 +547,11 @@ void main() {
).captured.cast<http.BaseRequest>().map((r) => r.url).toList();
String perEngine(String name) =>
'${cache.storageBaseUrl}/${cache.storageBucket}/shorebird/$shorebirdEngineRevision/$name';
'${cache.artifactBaseUrl}/shorebird/$shorebirdEngineRevision/$name';
final expected = [
perEngine('patch-windows-x64.zip'),
'https://github.com/google/bundletool/releases/download/1.18.1/bundletool-all-1.18.1.jar',
perEngine('aot-tools.dill'),
].map(Uri.parse).toList();
expect(requests, equals(expected));
@@ -502,16 +570,36 @@ void main() {
).captured.cast<http.BaseRequest>().map((r) => r.url).toList();
String perEngine(String name) =>
'${cache.storageBaseUrl}/${cache.storageBucket}/shorebird/$shorebirdEngineRevision/$name';
'${cache.artifactBaseUrl}/shorebird/$shorebirdEngineRevision/$name';
final expected = [
perEngine('patch-linux-x64.zip'),
'https://github.com/google/bundletool/releases/download/1.18.1/bundletool-all-1.18.1.jar',
perEngine('aot-tools.dill'),
].map(Uri.parse).toList();
expect(requests, equals(expected));
});
test('pulls legacy aot-tools only when explicitly enabled', () async {
when(() => platform.environment).thenReturn({
Cache.legacyAotToolsEnvironmentVariable: 'true',
});
cache = runWithOverrides(Cache.new);
await expectLater(
runWithOverrides(() => cache.updateAll(Duration.zero)),
completes,
);
final requests = verify(
() => httpClient.send(captureAny()),
).captured.cast<http.BaseRequest>().map((r) => r.url).toList();
String perEngine(String name) =>
'${cache.artifactBaseUrl}/shorebird/$shorebirdEngineRevision/$name';
expect(requests, contains(Uri.parse(perEngine('aot-tools.dill'))));
});
});
});
});
@@ -101,6 +101,12 @@ void main() {
() => shorebirdEnv.flutterRevision,
).thenReturn(shorebirdFlutterRevision);
when(() => shorebirdEnv.logsDirectory).thenReturn(logsDirectory);
when(
() => shorebirdEnv.hostedUri,
).thenReturn(Uri.parse('http://localhost:8080'));
when(
() => shorebirdEnv.authServiceUri,
).thenReturn(Uri.parse('http://localhost:8080/auth'));
when(() => doctor.initAndDoctorValidators).thenReturn([validator]);
when(
() => doctor.runValidators(any(), applyFixes: any(named: 'applyFixes')),
@@ -122,7 +128,7 @@ void main() {
verify(
() => logger.info('''
Shorebird v$packageVersion git@github.com:shorebirdtech/shorebird.git
Shorebird v$packageVersion https://git.tonycloud.org/flutter/shorebird.git
Flutter revision ${shorebirdEnv.flutterRevision}
Engine revision $shorebirdEngineRevision
'''),
@@ -146,7 +152,7 @@ Engine • revision $shorebirdEngineRevision
verify(
() => logger.info('''
Shorebird v$packageVersion git@github.com:shorebirdtech/shorebird.git
Shorebird v$packageVersion https://git.tonycloud.org/flutter/shorebird.git
Flutter $flutterVersion revision ${shorebirdEnv.flutterRevision}
Engine revision $shorebirdEngineRevision
'''),
@@ -178,7 +184,7 @@ Engine • revision $shorebirdEngineRevision
expect(
msg,
equals('''
Shorebird $packageVersion git@github.com:shorebirdtech/shorebird.git
Shorebird $packageVersion https://git.tonycloud.org/flutter/shorebird.git
Flutter revision ${shorebirdEnv.flutterRevision}
Engine revision $shorebirdEngineRevision
@@ -218,7 +224,7 @@ OpenJDK 64-Bit Server VM (build 17.0.9+0-17.0.9b1087.7-11185874, mixed mode)'''
expect(
msg.replaceAll(Platform.lineTerminator, '\n'),
equals('''
Shorebird $packageVersion git@github.com:shorebirdtech/shorebird.git
Shorebird $packageVersion https://git.tonycloud.org/flutter/shorebird.git
Flutter revision ${shorebirdEnv.flutterRevision}
Engine revision $shorebirdEngineRevision
@@ -275,7 +281,7 @@ OpenJDK 64-Bit Server VM (build 17.0.9+0-17.0.9b1087.7-11185874, mixed mode)'''
expect(
msg.replaceAll(Platform.lineTerminator, '\n'),
equals('''
Shorebird $packageVersion git@github.com:shorebirdtech/shorebird.git
Shorebird $packageVersion https://git.tonycloud.org/flutter/shorebird.git
Flutter revision ${shorebirdEnv.flutterRevision}
Engine revision $shorebirdEngineRevision
@@ -16,6 +16,7 @@ import 'package:shorebird_cli/src/logging/logging.dart';
import 'package:shorebird_cli/src/platform.dart';
import 'package:shorebird_cli/src/platform/apple/apple.dart';
import 'package:shorebird_cli/src/pubspec_editor.dart';
import 'package:shorebird_cli/src/shorebird_documentation.dart';
import 'package:shorebird_cli/src/shorebird_env.dart';
import 'package:shorebird_cli/src/shorebird_process.dart';
import 'package:shorebird_cli/src/shorebird_validator.dart';
@@ -393,7 +394,7 @@ Please make sure you are running "shorebird init" from within your Flutter proje
expect(exitCode, equals(ExitCode.software.code));
verify(
() => logger.err(
'''You do not have any organizations. This should never happen. Please contact us on Discord or send us an email at contact@shorebird.dev.''',
'''You do not have any organizations. This should never happen. Please file an issue at $openShorebirdIssueUrl.''',
),
).called(1);
});
@@ -1482,7 +1483,7 @@ flutter:
'''📦 To create a new release use: "${lightCyan.wrap('shorebird release')}".''',
'''🚀 To push an update use: "${lightCyan.wrap('shorebird patch')}".''',
'''👀 To preview a release use: "${lightCyan.wrap('shorebird preview')}".''',
'''For more information about Shorebird, visit ${link(uri: Uri.parse('https://shorebird.dev'))}''',
'''For more information about Shorebird, visit ${link(uri: Uri.parse(docsUrl))}''',
'',
]),
),
@@ -4,6 +4,7 @@ import 'package:scoped_deps/scoped_deps.dart';
import 'package:shorebird_cli/src/auth/auth.dart';
import 'package:shorebird_cli/src/commands/commands.dart';
import 'package:shorebird_cli/src/logging/logging.dart';
import 'package:shorebird_cli/src/shorebird_env.dart';
import 'package:test/test.dart';
import '../mocks.dart';
@@ -12,6 +13,7 @@ void main() {
group(LoginCiCommand, () {
late Auth auth;
late ShorebirdLogger logger;
late ShorebirdEnv shorebirdEnv;
late LoginCiCommand command;
R runWithOverrides<R>(R Function() body) {
@@ -20,6 +22,7 @@ void main() {
values: {
authRef.overrideWith(() => auth),
loggerRef.overrideWith(() => logger),
shorebirdEnvRef.overrideWith(() => shorebirdEnv),
},
);
}
@@ -27,6 +30,10 @@ void main() {
setUp(() {
auth = MockAuth();
logger = MockShorebirdLogger();
shorebirdEnv = MockShorebirdEnv();
when(
() => shorebirdEnv.hostedUri,
).thenReturn(Uri.parse(ShorebirdEnv.defaultHostedUrl));
command = runWithOverrides(LoginCiCommand.new);
});
@@ -50,9 +57,14 @@ void main() {
final message = captured.single as String;
expect(message, contains('shorebird login:ci has been replaced'));
expect(message, contains('console.shorebird.dev'));
expect(message, contains(ShorebirdEnv.defaultHostedUrl));
expect(message, contains('SHOREBIRD_TOKEN'));
expect(message, contains('docs.shorebird.dev/account/api-keys'));
expect(
message,
contains(
'git.tonycloud.org/flutter/shorebird/src/branch/main/docs/account/api-keys',
),
);
});
test('does not trigger any auth flow', () async {
@@ -8,6 +8,7 @@ import 'package:scoped_deps/scoped_deps.dart';
import 'package:shorebird_cli/src/auth/auth.dart';
import 'package:shorebird_cli/src/commands/login_command.dart';
import 'package:shorebird_cli/src/logging/logging.dart';
import 'package:shorebird_cli/src/shorebird_env.dart';
import 'package:test/test.dart';
import '../mocks.dart';
@@ -20,6 +21,7 @@ void main() {
late http.Client httpClient;
late Directory applicationConfigHome;
late ShorebirdLogger logger;
late ShorebirdEnv shorebirdEnv;
late LoginCommand command;
R runWithOverrides<R>(R Function() body) {
@@ -28,6 +30,7 @@ void main() {
values: {
authRef.overrideWith(() => auth),
loggerRef.overrideWith(() => logger),
shorebirdEnvRef.overrideWith(() => shorebirdEnv),
},
);
}
@@ -37,9 +40,13 @@ void main() {
auth = MockAuth();
httpClient = MockHttpClient();
logger = MockShorebirdLogger();
shorebirdEnv = MockShorebirdEnv();
when(() => auth.isAuthenticated).thenReturn(false);
when(() => auth.client).thenReturn(httpClient);
when(
() => shorebirdEnv.hostedUri,
).thenReturn(Uri.parse(ShorebirdEnv.defaultHostedUrl));
when(
() => auth.credentialsFilePath,
).thenReturn(p.join(applicationConfigHome.path, 'credentials.json'));
@@ -117,7 +124,7 @@ void main() {
() => logger.err('We could not find a Shorebird account for $email.'),
).called(1);
verify(
() => logger.info(any(that: contains('console.shorebird.dev'))),
() => logger.info(any(that: contains(ShorebirdEnv.defaultHostedUrl))),
).called(1);
});
@@ -56,7 +56,7 @@ void main() {
final result = await runWithOverrides(command.run);
expect(result, equals(ExitCode.success.code));
verify(() => logger.progress('Logging out of shorebird.dev')).called(1);
verify(() => logger.progress('Logging out of Shorebird')).called(1);
verify(() => auth.logout()).called(1);
});
});
@@ -294,7 +294,7 @@ void main() {
).thenAnswer((_) async => aabFile);
});
// See https://github.com/shorebirdtech/updater/issues/211
// Historical updater bug fixed in Flutter 3.24.2.
group('when flutter version contains updater issue 211', () {
setUp(() {
setUpProjectRootArtifacts();
@@ -366,7 +366,7 @@ void main() {
verify(
() => logger.info('''
Please run `shorebird cache clean` and try again. If the issue persists, please
file a bug report at https://github.com/shorebirdtech/shorebird/issues/new.
file a bug report at https://git.tonycloud.org/flutter/shorebird/issues/new.
Looked in:
- the libapp.so entries inside the built .aab
@@ -834,7 +834,7 @@ Looked in:
() => logger.info(
any(
that: contains(
'https://github.com/shorebirdtech/shorebird/issues/2532',
'git.tonycloud.org/flutter/shorebird/src/branch/main/docs/code-push/troubleshooting',
),
),
),
@@ -1,11 +1,14 @@
import 'dart:convert';
import 'dart:io';
import 'package:args/args.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
import 'package:open_aot_patch_tools/open_aot_patch_tools.dart' as open_patch;
import 'package:path/path.dart' as p;
import 'package:platform/platform.dart';
import 'package:pub_semver/pub_semver.dart';
import 'package:pubspec_parse/pubspec_parse.dart';
import 'package:scoped_deps/scoped_deps.dart';
import 'package:shorebird_cli/src/archive_analysis/apple_archive_differ.dart';
import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart';
@@ -23,6 +26,7 @@ import 'package:shorebird_cli/src/logging/logging.dart';
import 'package:shorebird_cli/src/metadata/metadata.dart';
import 'package:shorebird_cli/src/os/operating_system_interface.dart';
import 'package:shorebird_cli/src/patch_diff_checker.dart';
import 'package:shorebird_cli/src/platform.dart' as cli_platform;
import 'package:shorebird_cli/src/platform/platform.dart';
import 'package:shorebird_cli/src/release_type.dart';
import 'package:shorebird_cli/src/shorebird_artifacts.dart';
@@ -59,6 +63,7 @@ void main() {
late ShorebirdLogger logger;
late OperatingSystemInterface operatingSystemInterface;
late PatchDiffChecker patchDiffChecker;
late Platform platform;
late Progress progress;
late ShorebirdArtifacts shorebirdArtifacts;
late ShorebirdProcess shorebirdProcess;
@@ -83,6 +88,7 @@ void main() {
loggerRef.overrideWith(() => logger),
osInterfaceRef.overrideWith(() => operatingSystemInterface),
patchDiffCheckerRef.overrideWith(() => patchDiffChecker),
cli_platform.platformRef.overrideWith(() => platform),
processRef.overrideWith(() => shorebirdProcess),
shorebirdArtifactsRef.overrideWith(() => shorebirdArtifacts),
shorebirdEnvRef.overrideWith(() => shorebirdEnv),
@@ -117,7 +123,9 @@ void main() {
flavorValidator = MockFlavorValidator();
operatingSystemInterface = MockOperatingSystemInterface();
patchDiffChecker = MockPatchDiffChecker();
platform = MockPlatform();
progress = MockProgress();
flutterDirectory = Directory.systemTemp.createTempSync();
projectRoot = Directory.systemTemp.createTempSync();
logger = MockShorebirdLogger();
shorebirdArtifacts = MockShorebirdArtifacts();
@@ -134,6 +142,9 @@ void main() {
when(() => argResults.wasParsed(any())).thenReturn(false);
when(() => logger.progress(any())).thenReturn(progress);
when(
() => platform.environment,
).thenReturn({'SHOREBIRD_IOS_NATIVE_AOT_PATCH': '1'});
when(
() => shorebirdEnv.getShorebirdProjectRoot(),
@@ -144,9 +155,11 @@ void main() {
when(() => shorebirdEnv.iosSupplementDirectory).thenReturn(
Directory(p.join(projectRoot.path, 'build', 'shorebird', 'ios')),
);
when(() => shorebirdEnv.flutterDirectory).thenReturn(flutterDirectory);
when(() => shorebirdEnv.iosPodfileLockHash).thenReturn(null);
when(aotTools.isLinkDebugInfoSupported).thenAnswer((_) async => false);
when(() => shorebirdFlutter.getConfig()).thenReturn({});
patcher = IosPatcher(
argParser: argParser,
@@ -841,6 +854,251 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}'''),
expect(copiedKernelFile.existsSync(), isTrue);
});
group('when using the interpreter patch route', () {
setUp(() {
when(() => platform.environment).thenReturn({});
File(p.join(projectRoot.path, 'lib', 'main.dart'))
..createSync(recursive: true)
..writeAsStringSync('void main() {}');
File(p.join(projectRoot.path, '.dart_tool', 'package_config.json'))
..createSync(recursive: true)
..writeAsStringSync('{"configVersion":2,"packages":[]}');
File(
p.join(
flutterDirectory.path,
'bin',
'cache',
'flutter.version.json',
),
)
..createSync(recursive: true)
..writeAsStringSync(
jsonEncode({
'frameworkVersion': '3.44.0-0.0.pre',
'channel': '[user-branch]',
'repositoryUrl': 'https://example.com/flutter.git',
'frameworkRevision': '1234567890abcdef',
'engineRevision': 'abcdef1234567890',
'dartSdkVersion': '3.13.0',
}),
);
when(() => shorebirdEnv.getShorebirdYaml()).thenReturn(
const ShorebirdYaml(
appId: 'app-id',
aotPatchRuntimeMode: 'dart-bytecode-interpreter',
),
);
when(
() => artifactBuilder.buildDartBytecodeSnapshot(
inputFilePath: any(named: 'inputFilePath'),
outFilePath: any(named: 'outFilePath'),
packageConfigPath: any(named: 'packageConfigPath'),
dartDefines: any(named: 'dartDefines'),
experiments: any(named: 'experiments'),
),
).thenAnswer(
(invocation) async {
final outFilePath =
invocation.namedArguments[#outFilePath] as String;
return File(outFilePath)
..createSync(recursive: true)
..writeAsStringSync('bytecode');
},
);
});
test('generates a bytecode patch and skips native AOT', () async {
await runWithOverrides(patcher.buildPatchArtifact);
verify(
() => artifactBuilder.buildIpa(
codesign: any(named: 'codesign'),
args: any(named: 'args'),
flavor: any(named: 'flavor'),
target: any(named: 'target'),
base64PublicKey: any(named: 'base64PublicKey'),
ddMaxBytes: any(named: 'ddMaxBytes'),
),
).called(1);
verify(
() => artifactBuilder.buildDartBytecodeSnapshot(
inputFilePath: p.join(projectRoot.path, 'lib', 'main.dart'),
outFilePath: p.join(
projectRoot.path,
'build',
'ios_interpreter_patch.bytecode',
),
packageConfigPath: p.join(
projectRoot.path,
'.dart_tool',
'package_config.json',
),
dartDefines: any(named: 'dartDefines'),
experiments: any(named: 'experiments'),
),
).called(1);
verifyNever(
() => artifactBuilder.buildElfAotSnapshot(
appDillPath: any(named: 'appDillPath'),
outFilePath: any(named: 'outFilePath'),
genSnapshotArtifact: any(named: 'genSnapshotArtifact'),
additionalArgs: any(named: 'additionalArgs'),
),
);
});
test(
'forwards Dart defines and experiments to bytecode compiler',
() async {
File(p.join(projectRoot.path, 'defines.json')).writeAsStringSync(
'{"FROM_FILE":"yes"}',
);
when(() => argResults.rest).thenReturn([
'ios',
'--dart-define=FROM_ARG=yes',
'--dart-define-from-file=defines.json',
'--enable-experiment=records',
]);
await runWithOverrides(patcher.buildPatchArtifact);
final captured =
verify(
() => artifactBuilder.buildDartBytecodeSnapshot(
inputFilePath: any(named: 'inputFilePath'),
outFilePath: any(named: 'outFilePath'),
packageConfigPath: any(named: 'packageConfigPath'),
dartDefines: captureAny(named: 'dartDefines'),
experiments: ['records'],
),
).captured.single
as List<String>;
expect(
captured,
containsAllInOrder([
'FROM_FILE=yes',
'FROM_ARG=yes',
'FLUTTER_VERSION=3.44.0-0.0.pre',
'FLUTTER_CHANNEL=[user-branch]',
'FLUTTER_GIT_URL=https://example.com/flutter.git',
'FLUTTER_FRAMEWORK_REVISION=1234567890',
'FLUTTER_ENGINE_REVISION=abcdef1234',
'FLUTTER_DART_VERSION=3.13.0',
]),
);
},
);
test('adds enabled Flutter runtime feature flags', () async {
when(() => shorebirdFlutter.getConfig()).thenReturn({
'enable-windowing': 'true',
});
when(() => platform.environment).thenReturn({
'FLUTTER_ACCESSIBILITY_EVALUATIONS': 'true',
});
await runWithOverrides(patcher.buildPatchArtifact);
final captured =
verify(
() => artifactBuilder.buildDartBytecodeSnapshot(
inputFilePath: any(named: 'inputFilePath'),
outFilePath: any(named: 'outFilePath'),
packageConfigPath: any(named: 'packageConfigPath'),
dartDefines: captureAny(named: 'dartDefines'),
experiments: any(named: 'experiments'),
),
).captured.single
as List<String>;
expect(
captured,
contains(
'FLUTTER_ENABLED_FEATURE_FLAGS='
'windowing,accessibility_evaluations',
),
);
});
test(
'respects project feature config before global and environment',
() async {
when(() => shorebirdEnv.getPubspecYaml()).thenReturn(
Pubspec(
'app',
flutter: {
'config': {
'enable-windowing': false,
'enable-accessibility-evaluations': true,
},
},
),
);
when(() => shorebirdFlutter.getConfig()).thenReturn({
'enable-windowing': 'true',
'enable-accessibility-evaluations': 'false',
});
when(() => platform.environment).thenReturn({
'FLUTTER_WINDOWING': 'true',
'FLUTTER_ACCESSIBILITY_EVALUATIONS': 'false',
});
await runWithOverrides(patcher.buildPatchArtifact);
final captured =
verify(
() => artifactBuilder.buildDartBytecodeSnapshot(
inputFilePath: any(named: 'inputFilePath'),
outFilePath: any(named: 'outFilePath'),
packageConfigPath: any(named: 'packageConfigPath'),
dartDefines: captureAny(named: 'dartDefines'),
experiments: any(named: 'experiments'),
),
).captured.single
as List<String>;
expect(
captured,
contains(
'FLUTTER_ENABLED_FEATURE_FLAGS=accessibility_evaluations',
),
);
expect(
captured,
isNot(
contains('FLUTTER_ENABLED_FEATURE_FLAGS=windowing'),
),
);
},
);
test('rejects user-provided Flutter version defines', () async {
when(() => argResults.rest).thenReturn([
'ios',
'--dart-define=FLUTTER_VERSION=custom',
]);
await expectLater(
() => runWithOverrides(patcher.buildPatchArtifact),
exitsWithCode(ExitCode.usage),
);
verify(
() => logger.err(
'FLUTTER_VERSION is used by Flutter and cannot be set using '
'--dart-define or --dart-define-from-file.',
),
).called(1);
verifyNever(
() => artifactBuilder.buildDartBytecodeSnapshot(
inputFilePath: any(named: 'inputFilePath'),
outFilePath: any(named: 'outFilePath'),
packageConfigPath: any(named: 'packageConfigPath'),
dartDefines: any(named: 'dartDefines'),
experiments: any(named: 'experiments'),
),
);
});
});
group('when extraBuildArgs has obfuscation flags', () {
late File obfuscationMapFile;
@@ -857,13 +1115,17 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}'''),
});
test('includes obfuscation flags in build args', () async {
patcher.obfuscationMapPath = obfuscationMapFile.path;
patcher.extraBuildArgs = [
'--obfuscate',
'--extra-gen-snapshot-options='
'--load-obfuscation-map=${obfuscationMapFile.path}',
'--split-debug-info=build/shorebird/symbols',
];
final loadObfuscationMapArg = [
'--extra-gen-snapshot-options=--load-obfuscation-map',
obfuscationMapFile.path,
].join('=');
patcher
..obfuscationMapPath = obfuscationMapFile.path
..extraBuildArgs = [
'--obfuscate',
loadObfuscationMapArg,
'--split-debug-info=build/shorebird/symbols',
];
await runWithOverrides(patcher.buildPatchArtifact);
final captured = verify(
@@ -1069,6 +1331,138 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}'''),
});
});
group('when using the interpreter patch route', () {
const keyHex =
'000102030405060708090a0b0c0d0e0f'
'101112131415161718191a1b1c1d1e1f';
const nonceHex = '000102030405060708090a0b';
final patchBytes = utf8.encode('dart-bytecode-payload');
setUp(() {
when(() => platform.environment).thenReturn({
'SHOREBIRD_AOT_PATCH_KEY_HEX': keyHex,
'SHOREBIRD_AOT_PATCH_NONCE_HEX': nonceHex,
});
File(p.join(projectRoot.path, 'build', 'patch.interp'))
..createSync(recursive: true)
..writeAsBytesSync(patchBytes);
when(() => shorebirdEnv.getShorebirdYaml()).thenReturn(
const ShorebirdYaml(
appId: appId,
aotPatchRuntimeMode: 'dart-bytecode-interpreter',
aotPatchBytecodePath: 'build/patch.interp',
aotPatchKeyId: 'test-key',
aotPatchBaseFlavorId: 'free',
aotPatchBaseLicenseType: 'free',
aotPatchFlavorId: 'pro',
aotPatchLicenseType: 'pro',
aotPatchSdkHash: 'sdk-hash',
aotPatchBaseSnapshotHash: 'base-snapshot-hash',
aotPatchOfflineExpiresAt: '2030-01-01T00:00:00Z',
),
);
});
test('returns an encrypted full-snapshot bytecode artifact', () async {
final patchBundle = await runWithOverrides(
() => patcher.createPatchArtifacts(
appId: appId,
releaseId: releaseId,
releaseArtifact: releaseArtifactFile,
releaseVersion: '1.2.3+4',
),
);
final bundle = patchBundle[Arch.arm64]!;
expect(bundle.path, endsWith('ios_interpreter_patch.vmcode'));
final encrypted = open_patch.EncryptedPatchArtifact.fromJson(
(jsonDecode(File(bundle.path).readAsStringSync()) as Map)
.cast<String, Object?>(),
);
expect(encrypted.payloadKind, open_patch.payloadKindFullSnapshot);
expect(
encrypted.metadata.runtimeMode,
open_patch.runtimeModeDartBytecodeInterpreter,
);
expect(encrypted.metadata.appId, appId);
expect(encrypted.metadata.appBuildId, '1.2.3+4');
expect(encrypted.metadata.baseFlavorId, 'free');
expect(encrypted.metadata.baseLicenseType, 'free');
expect(encrypted.metadata.flavorId, 'pro');
expect(encrypted.metadata.licenseType, 'pro');
expect(encrypted.metadata.targetOs, 'ios');
expect(encrypted.metadata.targetArch, 'arm64');
expect(
encrypted.metadata.offlineExpiresAt,
'2030-01-01T00:00:00.000Z',
);
expect(
encrypted.decrypt(open_patch.readKey(keyHex)).payload,
equals(patchBytes),
);
verifyNever(
() => apple.runLinker(
kernelFile: any(named: 'kernelFile'),
aotOutputFile: any(named: 'aotOutputFile'),
releaseArtifact: any(named: 'releaseArtifact'),
splitDebugInfoArgs: any(named: 'splitDebugInfoArgs'),
vmCodeFile: any(named: 'vmCodeFile'),
ddMaxBytes: any(named: 'ddMaxBytes'),
),
);
});
test(
'uses generated bytecode when no override path is configured',
() async {
File(
p.join(
projectRoot.path,
'build',
'ios_interpreter_patch.bytecode',
),
)
..createSync(recursive: true)
..writeAsBytesSync(patchBytes);
when(() => shorebirdEnv.getShorebirdYaml()).thenReturn(
const ShorebirdYaml(
appId: appId,
aotPatchRuntimeMode: 'dart-bytecode-interpreter',
aotPatchKeyId: 'test-key',
aotPatchBaseFlavorId: 'free',
aotPatchBaseLicenseType: 'free',
aotPatchFlavorId: 'pro',
aotPatchLicenseType: 'pro',
aotPatchSdkHash: 'sdk-hash',
aotPatchBaseSnapshotHash: 'base-snapshot-hash',
),
);
final patchBundle = await runWithOverrides(
() => patcher.createPatchArtifacts(
appId: appId,
releaseId: releaseId,
releaseArtifact: releaseArtifactFile,
releaseVersion: '1.2.3+4',
),
);
final encrypted = open_patch.EncryptedPatchArtifact.fromJson(
(jsonDecode(
File(patchBundle[Arch.arm64]!.path).readAsStringSync(),
)
as Map)
.cast<String, Object?>(),
);
expect(
encrypted.decrypt(open_patch.readKey(keyHex)).payload,
equals(patchBytes),
);
},
);
});
group('when uses linker', () {
const linkPercentage = 50.0;
late File analyzeSnapshotFile;
@@ -815,7 +815,7 @@ dependencyResolutionManagement {
+ }
+ maven {
- url 'https://storage.googleapis.com/download.flutter.io'
+ url 'https://download.shorebird.dev/download.flutter.io'
+ url '${ShorebirdProcess.defaultFlutterStorageBaseUrl}'
+ }
}
}
@@ -931,7 +931,7 @@ To change the version of this release, change your app's version in your pubspec
expect(
runWithOverrides(() => releaser.postReleaseInstructions),
contains(
'https://github.com/shorebirdtech/shorebird/issues/3223',
'git.tonycloud.org/flutter/shorebird/src/branch/main/docs/code-push/troubleshooting',
),
);
});
@@ -105,6 +105,47 @@ patch_verification: install_only
expect(shorebirdYaml.patchVerification, PatchVerification.installOnly);
});
test('can be deserialized with open AOT patch metadata', () {
const yaml = '''
app_id: test_app_id
aot_patch_runtime_mode: dart-bytecode-interpreter
aot_patch_bytecode_path: build/patch.interp
aot_patch_key_id: test-key
aot_patch_key_hex: 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
aot_patch_app_build_id: 1.2.3+4
aot_patch_base_flavor_id: free
aot_patch_base_license_type: free
aot_patch_flavor_id: pro
aot_patch_license_type: pro
aot_patch_sdk_hash: sdk-hash
aot_patch_base_snapshot_hash: base-hash
aot_patch_obfuscation_map_hash: obfuscation-hash
aot_patch_offline_expires_at: 2030-01-01T00:00:00Z
''';
final shorebirdYaml = checkedYamlDecode(
yaml,
(m) => ShorebirdYaml.fromJson(m!),
);
expect(
shorebirdYaml.aotPatchRuntimeMode,
'dart-bytecode-interpreter',
);
expect(shorebirdYaml.aotPatchBytecodePath, 'build/patch.interp');
expect(shorebirdYaml.aotPatchKeyId, 'test-key');
expect(shorebirdYaml.aotPatchAppBuildId, '1.2.3+4');
expect(shorebirdYaml.aotPatchBaseFlavorId, 'free');
expect(shorebirdYaml.aotPatchBaseLicenseType, 'free');
expect(shorebirdYaml.aotPatchFlavorId, 'pro');
expect(shorebirdYaml.aotPatchLicenseType, 'pro');
expect(shorebirdYaml.aotPatchSdkHash, 'sdk-hash');
expect(shorebirdYaml.aotPatchBaseSnapshotHash, 'base-hash');
expect(shorebirdYaml.aotPatchObfuscationMapHash, 'obfuscation-hash');
expect(
shorebirdYaml.aotPatchOfflineExpiresAt,
'2030-01-01T00:00:00Z',
);
});
test('throws when patch_verification has invalid value', () {
const yaml = '''
app_id: test_app_id
@@ -11,6 +11,7 @@ import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
import 'package:shorebird_cli/src/http_client/http_client.dart';
import 'package:shorebird_cli/src/logging/logging.dart';
import 'package:shorebird_cli/src/network_checker.dart';
import 'package:shorebird_cli/src/shorebird_env.dart';
import 'package:test/test.dart';
import 'fakes.dart';
@@ -23,6 +24,7 @@ void main() {
late http.Client httpClient;
late ShorebirdLogger logger;
late Progress progress;
late ShorebirdEnv shorebirdEnv;
late NetworkChecker networkChecker;
R runWithOverrides<R>(R Function() body) {
@@ -33,6 +35,7 @@ void main() {
codePushClientWrapperRef.overrideWith(() => codePushClientWrapper),
httpClientRef.overrideWith(() => httpClient),
loggerRef.overrideWith(() => logger),
shorebirdEnvRef.overrideWith(() => shorebirdEnv),
},
);
}
@@ -48,8 +51,15 @@ void main() {
httpClient = MockHttpClient();
logger = MockShorebirdLogger();
progress = MockProgress();
shorebirdEnv = MockShorebirdEnv();
when(() => logger.progress(any())).thenReturn(progress);
when(
() => shorebirdEnv.hostedUri,
).thenReturn(Uri.parse(ShorebirdEnv.defaultHostedUrl));
when(
() => shorebirdEnv.authServiceUri,
).thenReturn(Uri.parse(ShorebirdEnv.defaultAuthServiceUrl));
networkChecker = NetworkChecker();
});
@@ -63,11 +73,26 @@ void main() {
});
test('logs reachability for each checked url', () async {
await runWithOverrides(networkChecker.checkReachability);
final checkedUrlCount = await runWithOverrides(() async {
await networkChecker.checkReachability();
return NetworkChecker.urlsToCheck.length;
});
verify(
() => progress.complete(any(that: contains('OK'))),
).called(NetworkChecker.urlsToCheck.length);
).called(checkedUrlCount);
});
test('checks self-hosted urls from ShorebirdEnv', () async {
final hostedUri = Uri.parse('https://open.example.com');
final authServiceUri = Uri.parse('https://auth.example.com');
when(() => shorebirdEnv.hostedUri).thenReturn(hostedUri);
when(() => shorebirdEnv.authServiceUri).thenReturn(authServiceUri);
await runWithOverrides(networkChecker.checkReachability);
verify(() => httpClient.get(hostedUri)).called(1);
verify(() => httpClient.get(authServiceUri)).called(1);
});
});
@@ -77,11 +102,14 @@ void main() {
});
test('logs reachability for each checked url', () async {
await runWithOverrides(networkChecker.checkReachability);
final checkedUrlCount = await runWithOverrides(() async {
await networkChecker.checkReachability();
return NetworkChecker.urlsToCheck.length;
});
verify(
() => progress.fail(any(that: contains('unreachable'))),
).called(NetworkChecker.urlsToCheck.length);
).called(checkedUrlCount);
});
});
});
@@ -209,12 +209,99 @@ void main() {
),
);
});
test('returns correct path for dartaotruntime', () {
expect(
runWithOverrides(
() => artifacts.getArtifactPath(
artifact: ShorebirdArtifact.dartAotRuntime,
),
),
equals(
p.join(
flutterDirectory.path,
'bin',
'cache',
'dart-sdk',
'bin',
'dartaotruntime',
),
),
);
});
test('returns correct path for dart2bytecode snapshot', () {
expect(
runWithOverrides(
() => artifacts.getArtifactPath(
artifact: ShorebirdArtifact.dart2BytecodeSnapshot,
),
),
equals(
p.join(
flutterDirectory.path,
'bin',
'cache',
'dart-sdk',
'bin',
'snapshots',
'dart2bytecode.dart.snapshot',
),
),
);
});
test('returns correct path for Flutter product platform dill', () {
expect(
runWithOverrides(
() => artifacts.getArtifactPath(
artifact: ShorebirdArtifact.flutterProductPlatformDill,
),
),
equals(
p.join(
flutterDirectory.path,
'bin',
'cache',
'artifacts',
'engine',
'common',
'flutter_patched_sdk_product',
'platform_strong.dill',
),
),
);
});
test('prefers workspace iOS Flutter platform dill when present', () {
final workspacePlatformDill = File(
p.join(
flutterDirectory.path,
'engine',
'src',
'out',
'ios_release',
'flutter_patched_sdk',
'platform_strong.dill',
),
)..createSync(recursive: true);
expect(
runWithOverrides(
() => artifacts.getArtifactPath(
artifact: ShorebirdArtifact.flutterProductPlatformDill,
),
),
equals(workspacePlatformDill.path),
);
});
});
});
group(ShorebirdLocalEngineArtifacts, () {
late String localEngineSrcPath;
late String localEngine;
late String localEngineHost;
late EngineConfig engineConfig;
late ShorebirdLocalEngineArtifacts artifacts;
@@ -228,6 +315,7 @@ void main() {
setUp(() {
localEngineSrcPath = 'local_engine_src_path';
localEngine = 'local_engine';
localEngineHost = 'local_engine_host';
engineConfig = MockEngineConfig();
artifacts = const ShorebirdLocalEngineArtifacts();
@@ -235,6 +323,7 @@ void main() {
() => engineConfig.localEngineSrcPath,
).thenReturn(localEngineSrcPath);
when(() => engineConfig.localEngine).thenReturn(localEngine);
when(() => engineConfig.localEngineHost).thenReturn(localEngineHost);
});
group('getArtifactPath', () {
@@ -353,6 +442,64 @@ void main() {
),
);
});
test('returns correct path for dartaotruntime', () {
expect(
runWithOverrides(
() => artifacts.getArtifactPath(
artifact: ShorebirdArtifact.dartAotRuntime,
),
),
equals(
p.join(
localEngineSrcPath,
'out',
localEngineHost,
'dartaotruntime',
),
),
);
});
test('returns correct path for dart2bytecode snapshot', () {
expect(
runWithOverrides(
() => artifacts.getArtifactPath(
artifact: ShorebirdArtifact.dart2BytecodeSnapshot,
),
),
equals(
p.join(
localEngineSrcPath,
'out',
localEngineHost,
'dart-sdk',
'bin',
'snapshots',
'dart2bytecode.dart.snapshot',
),
),
);
});
test('returns correct path for Flutter product platform dill', () {
expect(
runWithOverrides(
() => artifacts.getArtifactPath(
artifact: ShorebirdArtifact.flutterProductPlatformDill,
),
),
equals(
p.join(
localEngineSrcPath,
'out',
localEngine,
'flutter_patched_sdk',
'platform_strong.dill',
),
),
);
});
});
});
}
@@ -226,7 +226,7 @@ ${lightCyan.wrap("shorebird release android '--' --no-pub lib/main.dart")}'''),
verify(
() => logger.info('''
Shorebird $packageVersion git@github.com:shorebirdtech/shorebird.git
Shorebird $packageVersion https://git.tonycloud.org/flutter/shorebird.git
Flutter $flutterVersion revision $flutterRevision
Engine revision $shorebirdEngineRevision'''),
).called(1);
@@ -93,6 +93,27 @@ void main() {
});
});
group('shorebirdRoot', () {
test('returns correct directory for legacy snapshot layout', () {
expect(
runWithOverrides(() => shorebirdEnv.shorebirdRoot.path),
equals(shorebirdRoot.path),
);
});
test('returns correct directory for compiled executable layout', () {
platformScript = Uri.file(
p.join(shorebirdRoot.path, 'bin', 'shorebird'),
);
when(() => platform.script).thenReturn(platformScript);
expect(
runWithOverrides(() => shorebirdEnv.shorebirdRoot.path),
equals(shorebirdRoot.path),
);
});
});
group('getShorebirdYamlFile', () {
test('returns correct file', () {
final tempDir = Directory.systemTemp.createTempSync();
@@ -775,6 +796,18 @@ dependencies:
equals(engineRevision),
);
});
test('falls back to packaged engine revision metadata', () {
const engineRevision = 'test-revision';
File(p.join(shorebirdRoot.path, 'bin', 'internal', 'engine.version'))
..createSync(recursive: true)
..writeAsStringSync(engineRevision, flush: true);
expect(
runWithOverrides(() => shorebirdEnv.shorebirdEngineRevision),
equals(engineRevision),
);
});
});
group('hostedUrl', () {
@@ -843,11 +876,14 @@ base_url: https://yaml.example.com''');
);
});
test('returns null when there is no env override or shorebird.yaml', () {
expect(runWithOverrides(() => shorebirdEnv.hostedUri), isNull);
test('defaults to the open self-hosted server URL', () {
expect(
runWithOverrides(() => shorebirdEnv.hostedUri),
equals(Uri.parse(ShorebirdEnv.defaultHostedUrl)),
);
});
test('returns null when unable to read shorebird.yaml', () {
test('falls back to default when unable to read shorebird.yaml', () {
final directory = Directory.systemTemp.createTempSync();
// This is not valid utf8 so readAsString will throw.
File(
@@ -859,7 +895,7 @@ base_url: https://yaml.example.com''');
() => runWithOverrides(() => shorebirdEnv.hostedUri),
getCurrentDirectory: () => directory,
),
isNull,
equals(Uri.parse(ShorebirdEnv.defaultHostedUrl)),
);
});
});
@@ -931,7 +967,7 @@ base_url: https://yaml.example.com''');
when(() => platform.environment).thenReturn({});
expect(
runWithOverrides(() => shorebirdEnv.authServiceUri),
equals(Uri.parse('https://auth.shorebird.dev')),
equals(Uri.parse(ShorebirdEnv.defaultAuthServiceUrl)),
);
});
@@ -951,7 +987,7 @@ base_url: https://yaml.example.com''');
when(() => platform.environment).thenReturn({});
expect(
runWithOverrides(() => shorebirdEnv.jwtIssuer),
equals('https://auth.shorebird.dev'),
equals(ShorebirdEnv.defaultJwtIssuer),
);
});
@@ -96,6 +96,7 @@ void main() {
),
).thenAnswer((_) async => 'origin/flutter_release/3.10.6');
when(() => logger.progress(any())).thenReturn(progress);
when(() => platform.environment).thenReturn(const {});
when(() => platform.isMacOS).thenReturn(false);
when(() => shorebirdEnv.flutterDirectory).thenReturn(flutterDirectory);
when(() => shorebirdEnv.flutterRevision).thenReturn(flutterRevision);
@@ -116,6 +117,27 @@ void main() {
when(() => precacheProcessResult.stderr).thenReturn('');
});
group('flutterGitUrl', () {
test('defaults to the open Flutter fork', () {
expect(
runWithOverrides(() => shorebirdFlutter.flutterGitUrl),
ShorebirdFlutter.defaultFlutterGitUrl,
);
});
test('uses SHOREBIRD_FLUTTER_GIT_URL when set', () {
const flutterGitUrl = 'https://example.com/open/flutter.git';
when(
() => platform.environment,
).thenReturn(const {'SHOREBIRD_FLUTTER_GIT_URL': flutterGitUrl});
expect(
runWithOverrides(() => shorebirdFlutter.flutterGitUrl),
flutterGitUrl,
);
});
});
group('precacheArgs', () {
group('when running on macOS', () {
setUp(() {
@@ -935,7 +957,7 @@ origin/flutter_release/3.10.6''';
verify(
() => git.clone(
url: ShorebirdFlutter.flutterGitUrl,
url: ShorebirdFlutter.defaultFlutterGitUrl,
outputDirectory: p.join(flutterDirectory.parent.path, revision),
args: ['--filter=tree:0', '--no-checkout'],
),
@@ -962,7 +984,7 @@ origin/flutter_release/3.10.6''';
);
verify(
() => git.clone(
url: ShorebirdFlutter.flutterGitUrl,
url: ShorebirdFlutter.defaultFlutterGitUrl,
outputDirectory: p.join(flutterDirectory.parent.path, revision),
args: ['--filter=tree:0', '--no-checkout'],
),
@@ -18,7 +18,7 @@ import 'mocks.dart';
void main() {
group('ShorebirdProcess', () {
const flutterStorageBaseUrlEnv = {
'FLUTTER_STORAGE_BASE_URL': 'https://download.shorebird.dev',
'FLUTTER_STORAGE_BASE_URL': ShorebirdProcess.defaultFlutterStorageBaseUrl,
};
late EngineConfig engineConfig;
@@ -64,6 +64,7 @@ void main() {
when(() => logger.level).thenReturn(Level.info);
when(() => platform.environment).thenReturn({});
when(() => platform.isWindows).thenReturn(false);
});
@@ -156,6 +157,31 @@ void main() {
).called(1);
});
test('can use an open Flutter storage mirror', () async {
when(() => platform.environment).thenReturn({
'SHOREBIRD_FLUTTER_STORAGE_BASE_URL':
'https://artifacts.example.com/flutter',
});
await runWithOverrides(
() => shorebirdProcess.run('flutter', [
'--version',
], workingDirectory: '~'),
);
verify(
() => processWrapper.run(
any(),
['--version'],
environment: {
'FLUTTER_STORAGE_BASE_URL':
'https://artifacts.example.com/flutter',
},
workingDirectory: '~',
),
).called(1);
});
test('does not replace flutter with our local flutter if'
' useVendedFlutter is false', () async {
await runWithOverrides(
@@ -42,6 +42,10 @@ void main() {
shorebirdEnv = MockShorebirdEnv();
validator = MockValidator();
shorebirdValidator = runWithOverrides(ShorebirdValidator.new);
when(
() => shorebirdEnv.hostedUri,
).thenReturn(Uri.parse(ShorebirdEnv.defaultHostedUrl));
});
group('PreconditionFailedException', () {
@@ -93,7 +97,7 @@ void main() {
'''If you already have an account, run ${lightCyan.wrap('shorebird login')} to sign in.''',
),
() => logger.info(
'''If you don't have a Shorebird account, go to ${link(uri: Uri.parse('https://console.shorebird.dev'))} to create one.''',
'''If you don't have a Shorebird account, go to ${link(uri: Uri.parse(ShorebirdEnv.defaultHostedUrl))} to create one.''',
),
]);
});
@@ -1,19 +1,53 @@
import 'package:mocktail/mocktail.dart';
import 'package:scoped_deps/scoped_deps.dart';
import 'package:shorebird_cli/src/shorebird_env.dart';
import 'package:shorebird_cli/src/shorebird_web_console.dart';
import 'package:test/test.dart';
import 'mocks.dart';
void main() {
group(ShorebirdWebConsole, () {
late ShorebirdEnv shorebirdEnv;
R runWithOverrides<R>(R Function() body) {
return runScoped(
() => body(),
values: {shorebirdEnvRef.overrideWith(() => shorebirdEnv)},
);
}
setUp(() {
shorebirdEnv = MockShorebirdEnv();
when(
() => shorebirdEnv.hostedUri,
).thenReturn(Uri.parse(ShorebirdEnv.defaultHostedUrl));
});
test('uri returns the correct uri with the received path', () {
expect(
ShorebirdWebConsole.uri('path'),
Uri.parse('https://console.shorebird.dev/path'),
runWithOverrides(() => ShorebirdWebConsole.uri('path')),
Uri.parse('${ShorebirdEnv.defaultHostedUrl}/path'),
);
});
test('appReleaseUri returns the correct uri to an app release', () {
expect(
ShorebirdWebConsole.appReleaseUri('appId', 123),
Uri.parse('https://console.shorebird.dev/apps/appId/releases/123'),
runWithOverrides(
() => ShorebirdWebConsole.appReleaseUri('appId', 123),
),
Uri.parse('${ShorebirdEnv.defaultHostedUrl}/apps/appId/releases/123'),
);
});
test('uses the configured hosted url', () {
when(
() => shorebirdEnv.hostedUri,
).thenReturn(Uri.parse('https://open.example.com/base'));
expect(
runWithOverrides(() => ShorebirdWebConsole.uri('path')),
Uri.parse('https://open.example.com/base/path'),
);
});
});
@@ -95,7 +95,10 @@ class CodePushClient {
...standardHeaders,
...?customHeaders,
}),
hostedUri = hostedUri ?? Uri.https('api.shorebird.dev');
hostedUri = hostedUri ?? defaultHostedUri;
/// Default URL for the open self-hosted Shorebird server.
static final Uri defaultHostedUri = Uri.parse('http://localhost:8080');
/// The standard headers applied to all requests.
static const standardHeaders = <String, String>{'x-version': packageVersion};
@@ -1,7 +1,7 @@
name: shorebird_code_push_client
description: Library which allows Dart applications to interact with the Shorebird CodePush API
version: 0.9.0+1
repository: https://github.com/shorebirdtech/shorebird
repository: https://git.tonycloud.org/flutter/shorebird-workspace
resolution: workspace
publish_to: none
@@ -57,6 +57,10 @@ void main() {
expect(CodePushClient(), isNotNull);
});
test('defaults to the open self-hosted server URL', () {
expect(CodePushClient().hostedUri, CodePushClient.defaultHostedUri);
});
group('CodePushException', () {
test('toString is correct', () {
const exceptionWithDetails = CodePushException(
@@ -4,16 +4,17 @@ The Shorebird CodePush Protocol is a Dart library which contains common interfac
### Regenerating from the OpenAPI spec
Everything under `lib/src/` is generated from the public Shorebird
CodePush OpenAPI spec at [api.shorebird.dev/openapi.json](https://api.shorebird.dev/openapi.json)
(also served as [openapi.yaml](https://api.shorebird.dev/openapi.yaml)
for easier human review) by
[space_gen](https://github.com/eseidel/space_gen). To regenerate
against the latest published spec:
Everything under `lib/src/` is generated from an OpenAPI spec by
[space_gen](https://github.com/eseidel/space_gen). For this open workspace,
regenerate from the checked open spec at
`../shorebird-server/internal/api/handlers/openapi.yaml` or from the
self-hosted server's `http://localhost:8080/openapi.yaml` endpoint. Do not
regenerate from Shorebird's hosted API, or the generated protocol may drift
back toward closed service behavior.
```sh
dart run packages/shorebird_code_push_protocol/tool/gen.dart \
-i https://api.shorebird.dev/openapi.json \
-i http://localhost:8080/openapi.yaml \
-o packages/shorebird_code_push_protocol
```
@@ -1,7 +1,7 @@
name: shorebird_code_push_protocol
description: Library which contains common interfaces used by Shorebird CodePush
version: 0.1.0+1
repository: https://github.com/shorebirdtech/shorebird
repository: https://git.tonycloud.org/flutter/shorebird-workspace
resolution: workspace
publish_to: none
@@ -9,9 +9,13 @@
//
// Usage (from the repo root):
// dart run packages/shorebird_code_push_protocol/tool/gen.dart \
// -i https://api.shorebird.dev/openapi.json \
// -i http://localhost:8080/openapi.yaml \
// -o packages/shorebird_code_push_protocol
//
// Do not regenerate this package from Shorebird's hosted API. Use the checked
// open spec at ../shorebird-server/internal/api/handlers/openapi.yaml or the
// self-hosted server's OpenAPI endpoint.
//
// The generator does not touch `lib/extensions/` or
// `lib/shorebird_code_push_protocol.dart`; those are hand-written and
// re-export the generated types.
+9 -5
View File
@@ -8,19 +8,23 @@ set -e
# Needed because if it is set, cd may print the path it changed to.
unset CDPATH
# Either clones or pulls the Shorebird Flutter repository, depending on whether FLUTTER_PATH exists.
# Either clones or pulls the configured Flutter repository, depending on
# whether FLUTTER_PATH exists.
function update_flutter {
local flutter_git_url="${SHOREBIRD_FLUTTER_GIT_URL:-https://git.tonycloud.org/flutter/flutter.git}"
local flutter_storage_base_url="${SHOREBIRD_FLUTTER_STORAGE_BASE_URL:-${FLUTTER_STORAGE_BASE_URL:-http://localhost:8080/download.flutter.io}}"
if [[ -d "$FLUTTER_PATH" ]]; then
git -C "$FLUTTER_PATH" fetch
else
git clone --filter=tree:0 https://github.com/shorebirdtech/flutter.git --no-checkout "$FLUTTER_PATH"
git clone --filter=tree:0 "$flutter_git_url" --no-checkout "$FLUTTER_PATH"
fi
# -c to avoid printing a warning about being in a detached head state.
git -C "$FLUTTER_PATH" -c advice.detachedHead=false checkout "$FLUTTER_VERSION"
SHOREBIRD_ENGINE_VERSION=`cat "$FLUTTER_PATH/bin/internal/engine.version"`
echo "Shorebird Engine • revision $SHOREBIRD_ENGINE_VERSION"
# Install Shorebird Flutter Artifacts
FLUTTER_STORAGE_BASE_URL=https://download.shorebird.dev $FLUTTER_PATH/bin/flutter --version
# Install Flutter artifacts through the configured open mirror.
FLUTTER_STORAGE_BASE_URL="$flutter_storage_base_url" "$FLUTTER_PATH/bin/flutter" --version
}
function pub_get_with_retry {
@@ -215,7 +219,7 @@ function shared::execute() {
>&2 echo "Error: The shorebird directory is not a clone of the GitHub project."
>&2 echo " The shorebird tools requires Git in order to operate properly;"
>&2 echo " to install Shorebird, see the instructions at:"
>&2 echo " https://github.com/shorebirdtech/shorebird"
>&2 echo " https://git.tonycloud.org/flutter/shorebird"
exit 1
fi