Compare commits
2 Commits
08b8f07bfb
...
07a654e771
| Author | SHA1 | Date | |
|---|---|---|---|
| 07a654e771 | |||
| d9088aeec6 |
+82
-69
@@ -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.
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
## Shorebird 🐦
|
||||
## Shorebird
|
||||
|
||||
[](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-code-push" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=449946&theme=neutral" alt="Shorebird Code Push - Flutter over the air updates | Product Hunt" style="width: 128px; height: 27px;" width="128" height="27" /></a>
|
||||
|
||||
[](https://console.shorebird.dev/ci)
|
||||
[](https://github.com/shorebirdtech/shorebird/actions/workflows/main.yaml)
|
||||
[](https://github.com/shorebirdtech/shorebird/actions/workflows/e2e.yaml)
|
||||
[](https://codecov.io/gh/shorebirdtech/shorebird)
|
||||
[](./LICENSE-MIT)
|
||||
[](./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
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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,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:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
+1
-1
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user