From d9088aeec68840613491d6711c88885c5a29651c Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 25 Jun 2026 17:21:56 +0800 Subject: [PATCH] Refactor Shorebird CLI to support configurable URLs and improve test coverage - Updated AAR releaser test to use ShorebirdProcess.defaultFlutterStorageBaseUrl. - Changed macOS releaser test to reference a new troubleshooting URL. - Enhanced shorebird_yaml_test with deserialization for AOT patch metadata. - Modified network_checker_test to check self-hosted URLs from ShorebirdEnv. - Added tests for artifact paths in shorebird_artifacts_test. - Updated shorebird_cli_command_runner_test to reflect new repository URL. - Improved shorebird_env_test with additional tests for shorebirdRoot and engine revision fallback. - Adjusted shorebird_flutter_test to use environment variables for Flutter Git URL. - Updated shorebird_process_test to utilize default Flutter storage URL. - Enhanced shorebird_validator_test to link to the default hosted URL. - Updated shorebird_web_console_test to use the configured hosted URL. - Refactored code_push_client to use default hosted URL for API requests. - Updated README and generation scripts for the code_push_protocol package to reflect new OpenAPI spec source. - Modified shared.sh to allow configurable Flutter repository and storage URLs. Signed-off-by: Tony --- OPEN_SOURCE_REPLACEMENTS.md | 151 +++-- bin/shorebird.ps1 | 30 +- docs/account/api-keys/README.md | 14 + docs/code-push/troubleshooting/README.md | 32 + .../getting-started/flutter-version/README.md | 26 + packages/artifact_proxy/README.md | 120 ++-- packages/artifact_proxy/bin/server.dart | 39 +- packages/artifact_proxy/lib/config.dart | 8 +- .../lib/src/artifact_manifest_client.dart | 31 +- .../lib/src/artifact_proxy.dart | 70 +- .../lib/src/models/artifacts_manifest.dart | 2 +- packages/artifact_proxy/pubspec.yaml | 2 +- .../test/artifact_proxy_test.dart | 71 +- .../artifact_proxy/test/server_bin_test.dart | 49 ++ .../src/artifact_manifest_client_test.dart | 25 +- .../doc/open_aot_patch_design.md | 33 +- packages/redis_client/pubspec.yaml | 4 +- packages/scoped_deps/pubspec.yaml | 4 +- packages/shorebird_build_trace/pubspec.yaml | 4 +- packages/shorebird_ci/pubspec.yaml | 4 +- .../windows_archive_differ.dart | 2 +- .../artifact_builder/artifact_builder.dart | 57 +- .../lib/src/artifact_manager.dart | 4 +- packages/shorebird_cli/lib/src/auth/auth.dart | 3 +- packages/shorebird_cli/lib/src/cache.dart | 81 ++- .../lib/src/code_push_client_wrapper.dart | 8 +- .../lib/src/commands/doctor_command.dart | 2 +- .../lib/src/commands/init_command.dart | 4 +- .../lib/src/commands/login_ci_command.dart | 6 +- .../lib/src/commands/login_command.dart | 3 +- .../lib/src/commands/logout_command.dart | 2 +- .../lib/src/commands/patch/aar_patcher.dart | 1 + .../src/commands/patch/android_patcher.dart | 19 +- .../commands/patch/ios_framework_patcher.dart | 1 + .../lib/src/commands/patch/ios_patcher.dart | 613 +++++++++++++++++- .../lib/src/commands/patch/linux_patcher.dart | 1 + .../lib/src/commands/patch/macos_patcher.dart | 1 + .../lib/src/commands/patch/patch_command.dart | 14 +- .../lib/src/commands/patch/patcher.dart | 5 +- .../src/commands/patch/windows_patcher.dart | 1 + .../src/commands/patches/promote_command.dart | 3 +- .../src/commands/release/aar_releaser.dart | 3 +- .../commands/release/android_releaser.dart | 3 +- .../src/commands/release/macos_releaser.dart | 3 +- .../src/commands/release/release_command.dart | 6 +- .../lib/src/commands/release/releaser.dart | 2 +- .../lib/src/config/shorebird_yaml.dart | 52 ++ .../lib/src/config/shorebird_yaml.g.dart | 88 +++ .../lib/src/executables/patch_executable.dart | 2 +- .../metadata/build_environment_metadata.dart | 4 +- .../src/metadata/create_patch_metadata.dart | 4 +- .../src/metadata/update_release_metadata.dart | 4 +- .../lib/src/network_checker.dart | 18 +- .../lib/src/platform/apple/apple.dart | 2 +- .../lib/src/shorebird_artifacts.dart | 131 ++++ .../lib/src/shorebird_cli_command_runner.dart | 7 +- .../lib/src/shorebird_documentation.dart | 11 +- .../shorebird_cli/lib/src/shorebird_env.dart | 73 ++- .../lib/src/shorebird_flutter.dart | 13 +- .../lib/src/shorebird_process.dart | 13 +- .../lib/src/shorebird_validator.dart | 3 +- .../lib/src/shorebird_web_console.dart | 12 +- ...android_internet_permission_validator.dart | 2 +- packages/shorebird_cli/pubspec.yaml | 4 +- .../artifact_builder_test.dart | 106 ++- .../test/src/auth/auth_test.dart | 2 +- .../shorebird_cli/test/src/cache_test.dart | 148 ++++- .../src/commands/doctor_command_test.dart | 16 +- .../test/src/commands/init_command_test.dart | 5 +- .../src/commands/login_ci_command_test.dart | 16 +- .../test/src/commands/login_command_test.dart | 9 +- .../src/commands/logout_command_test.dart | 2 +- .../commands/patch/android_patcher_test.dart | 6 +- .../src/commands/patch/ios_patcher_test.dart | 408 +++++++++++- .../commands/release/aar_releaser_test.dart | 2 +- .../commands/release/macos_releaser_test.dart | 2 +- .../test/src/config/shorebird_yaml_test.dart | 41 ++ .../test/src/network_checker_test.dart | 36 +- .../test/src/shorebird_artifacts_test.dart | 147 +++++ .../shorebird_cli_command_runner_test.dart | 2 +- .../test/src/shorebird_env_test.dart | 48 +- .../test/src/shorebird_flutter_test.dart | 26 +- .../test/src/shorebird_process_test.dart | 28 +- .../test/src/shorebird_validator_test.dart | 6 +- .../test/src/shorebird_web_console_test.dart | 42 +- .../lib/src/code_push_client.dart | 5 +- .../shorebird_code_push_client/pubspec.yaml | 2 +- .../test/src/code_push_client_test.dart | 4 + .../shorebird_code_push_protocol/README.md | 15 +- .../shorebird_code_push_protocol/pubspec.yaml | 2 +- .../tool/gen.dart | 6 +- third_party/flutter/bin/internal/shared.sh | 12 +- 92 files changed, 2754 insertions(+), 400 deletions(-) create mode 100644 docs/account/api-keys/README.md create mode 100644 docs/code-push/troubleshooting/README.md create mode 100644 docs/getting-started/flutter-version/README.md create mode 100644 packages/artifact_proxy/test/server_bin_test.dart diff --git a/OPEN_SOURCE_REPLACEMENTS.md b/OPEN_SOURCE_REPLACEMENTS.md index 347741a2..940bd90e 100644 --- a/OPEN_SOURCE_REPLACEMENTS.md +++ b/OPEN_SOURCE_REPLACEMENTS.md @@ -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/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 = "" -``` +- 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: -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. diff --git a/bin/shorebird.ps1 b/bin/shorebird.ps1 index 21286a35..517bc355 100644 --- a/bin/shorebird.ps1 +++ b/bin/shorebird.ps1 @@ -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 { diff --git a/docs/account/api-keys/README.md b/docs/account/api-keys/README.md new file mode 100644 index 00000000..b734092a --- /dev/null +++ b/docs/account/api-keys/README.md @@ -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. diff --git a/docs/code-push/troubleshooting/README.md b/docs/code-push/troubleshooting/README.md new file mode 100644 index 00000000..5e2a65b4 --- /dev/null +++ b/docs/code-push/troubleshooting/README.md @@ -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. diff --git a/docs/getting-started/flutter-version/README.md b/docs/getting-started/flutter-version/README.md new file mode 100644 index 00000000..2d40059b --- /dev/null +++ b/docs/getting-started/flutter-version/README.md @@ -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. diff --git a/packages/artifact_proxy/README.md b/packages/artifact_proxy/README.md index d01cbb37..b0d50dbc 100644 --- a/packages/artifact_proxy/README.md +++ b/packages/artifact_proxy/README.md @@ -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 _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 > artifact_manifest.yaml +../../../scripts/write_artifact_manifest.py \ + --flutter-engine-revision \ + --output artifacts_manifest.yaml ``` -Then upload the `artifact_manifest.yaml` to `download.shorebird.dev/shorebird//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//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. diff --git a/packages/artifact_proxy/bin/server.dart b/packages/artifact_proxy/bin/server.dart index e52099be..185687d3 100644 --- a/packages/artifact_proxy/bin/server.dart +++ b/packages/artifact_proxy/bin/server.dart @@ -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 main() async { +Future main(List 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 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()); +} diff --git a/packages/artifact_proxy/lib/config.dart b/packages/artifact_proxy/lib/config.dart index a4cc9dee..28af1038 100644 --- a/packages/artifact_proxy/lib/config.dart +++ b/packages/artifact_proxy/lib/config.dart @@ -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', diff --git a/packages/artifact_proxy/lib/src/artifact_manifest_client.dart b/packages/artifact_proxy/lib/src/artifact_manifest_client.dart index 16d3f543..f2be64c4 100644 --- a/packages/artifact_proxy/lib/src/artifact_manifest_client.dart +++ b/packages/artifact_proxy/lib/src/artifact_manifest_client.dart @@ -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(maximumSize: 1000); - /// Fetches the [ArtifactsManifest] for the provided [revision] from the - /// shorebird storage bucket. + /// Fetches the [ArtifactsManifest] for the provided [revision]. Future getManifest(String revision) async { if (_cache.containsKey(revision)) return _cache[revision]!; final manifest = await _fetchManifest(revision); @@ -28,8 +34,9 @@ class ArtifactManifestClient { } Future _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'); +} diff --git a/packages/artifact_proxy/lib/src/artifact_proxy.dart b/packages/artifact_proxy/lib/src/artifact_proxy.dart index dc1106b4..2f5eca0b 100644 --- a/packages/artifact_proxy/lib/src/artifact_proxy.dart +++ b/packages/artifact_proxy/lib/src/artifact_proxy.dart @@ -13,31 +13,38 @@ const String _explainerHtml = """

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.

-See -https://docs.shorebird.dev/architecture for more information. +See the workspace README and docs/CI.md for open artifact mirror details.

Source code can be found here: -https://github.com/shorebirdtech/shorebird/tree/main/packages/artifact_proxy + href='https://git.tonycloud.org/flutter/shorebird'> + https://git.tonycloud.org/flutter/shorebird

-If you're seeing problems with your Shorebird install, or are interested in -replicating this proxy, please reach out to us over Discord. -https://shorebird.dev/contact +If you're seeing problems with this open artifact proxy, file an issue against +the repository that hosts this workspace.

"""; /// 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'); } diff --git a/packages/artifact_proxy/lib/src/models/artifacts_manifest.dart b/packages/artifact_proxy/lib/src/models/artifacts_manifest.dart index 3341a5e3..0df9a7a5 100644 --- a/packages/artifact_proxy/lib/src/models/artifacts_manifest.dart +++ b/packages/artifact_proxy/lib/src/models/artifacts_manifest.dart @@ -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 diff --git a/packages/artifact_proxy/pubspec.yaml b/packages/artifact_proxy/pubspec.yaml index 67418289..4a52bed7 100644 --- a/packages/artifact_proxy/pubspec.yaml +++ b/packages/artifact_proxy/pubspec.yaml @@ -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 diff --git a/packages/artifact_proxy/test/artifact_proxy_test.dart b/packages/artifact_proxy/test/artifact_proxy_test.dart index 8259def4..70af7bce 100644 --- a/packages/artifact_proxy/test/artifact_proxy_test.dart +++ b/packages/artifact_proxy/test/artifact_proxy_test.dart @@ -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 = diff --git a/packages/artifact_proxy/test/server_bin_test.dart b/packages/artifact_proxy/test/server_bin_test.dart new file mode 100644 index 00000000..4b374417 --- /dev/null +++ b/packages/artifact_proxy/test/server_bin_test.dart @@ -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; + } +} diff --git a/packages/artifact_proxy/test/src/artifact_manifest_client_test.dart b/packages/artifact_proxy/test/src/artifact_manifest_client_test.dart index adb063f7..87fc7802 100644 --- a/packages/artifact_proxy/test/src/artifact_manifest_client_test.dart +++ b/packages/artifact_proxy/test/src/artifact_manifest_client_test.dart @@ -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 diff --git a/packages/open_aot_patch_tools/doc/open_aot_patch_design.md b/packages/open_aot_patch_tools/doc/open_aot_patch_design.md index a864f847..fce330bc 100644 --- a/packages/open_aot_patch_tools/doc/open_aot_patch_design.md +++ b/packages/open_aot_patch_tools/doc/open_aot_patch_design.md @@ -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: aot_patch_base_snapshot_hash: ``` -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: diff --git a/packages/redis_client/pubspec.yaml b/packages/redis_client/pubspec.yaml index d348f41a..91894912 100644 --- a/packages/redis_client/pubspec.yaml +++ b/packages/redis_client/pubspec.yaml @@ -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 diff --git a/packages/scoped_deps/pubspec.yaml b/packages/scoped_deps/pubspec.yaml index d33163ca..1145e73a 100644 --- a/packages/scoped_deps/pubspec.yaml +++ b/packages/scoped_deps/pubspec.yaml @@ -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: diff --git a/packages/shorebird_build_trace/pubspec.yaml b/packages/shorebird_build_trace/pubspec.yaml index dff1fba6..deb1a805 100644 --- a/packages/shorebird_build_trace/pubspec.yaml +++ b/packages/shorebird_build_trace/pubspec.yaml @@ -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: diff --git a/packages/shorebird_ci/pubspec.yaml b/packages/shorebird_ci/pubspec.yaml index 1c8b0d83..a270f9e6 100644 --- a/packages/shorebird_ci/pubspec.yaml +++ b/packages/shorebird_ci/pubspec.yaml @@ -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 diff --git a/packages/shorebird_cli/lib/src/archive_analysis/windows_archive_differ.dart b/packages/shorebird_cli/lib/src/archive_analysis/windows_archive_differ.dart index 063036a9..8b3dca1d 100644 --- a/packages/shorebird_cli/lib/src/archive_analysis/windows_archive_differ.dart +++ b/packages/shorebird_cli/lib/src/archive_analysis/windows_archive_differ.dart @@ -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; } } diff --git a/packages/shorebird_cli/lib/src/artifact_builder/artifact_builder.dart b/packages/shorebird_cli/lib/src/artifact_builder/artifact_builder.dart index 8a2275e4..e4db14bf 100644 --- a/packages/shorebird_cli/lib/src/artifact_builder/artifact_builder.dart +++ b/packages/shorebird_cli/lib/src/artifact_builder/artifact_builder.dart @@ -112,7 +112,7 @@ ${lightCyan.wrap('shorebird release --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 ` 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 _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 buildDartBytecodeSnapshot({ + required String inputFilePath, + required String outFilePath, + String? packageConfigPath, + List dartDefines = const [], + List 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( + 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 buildWindowsApp({ String? target, diff --git a/packages/shorebird_cli/lib/src/artifact_manager.dart b/packages/shorebird_cli/lib/src/artifact_manager.dart index 5926d66d..218c566b 100644 --- a/packages/shorebird_cli/lib/src/artifact_manager.dart +++ b/packages/shorebird_cli/lib/src/artifact_manager.dart @@ -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 diff --git a/packages/shorebird_cli/lib/src/auth/auth.dart b/packages/shorebird_cli/lib/src/auth/auth.dart index 64baa46f..09581bef 100644 --- a/packages/shorebird_cli/lib/src/auth/auth.dart +++ b/packages/shorebird_cli/lib/src/auth/auth.dart @@ -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 diff --git a/packages/shorebird_cli/lib/src/cache.dart b/packages/shorebird_cli/lib/src/cache.dart index fd68f84e..a8f8974c 100644 --- a/packages/shorebird_cli/lib/src/cache.dart +++ b/packages/shorebird_cli/lib/src/cache.dart @@ -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 _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//...` 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 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 get storageUrl async => - '${cache.storageBaseUrl}/${cache.storageBucket}/shorebird/${shorebirdEnv.shorebirdEngineRevision}/$fileName'; + Future 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 _supportsArm64Patch() async { diff --git a/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart b/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart index c8e76c53..df915db3 100644 --- a/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart +++ b/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart @@ -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 = []; var uploadedArchCount = 0; for (final arch in architectures) { diff --git a/packages/shorebird_cli/lib/src/commands/doctor_command.dart b/packages/shorebird_cli/lib/src/commands/doctor_command.dart index b4437d2e..92e78496 100644 --- a/packages/shorebird_cli/lib/src/commands/doctor_command.dart +++ b/packages/shorebird_cli/lib/src/commands/doctor_command.dart @@ -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}'''); diff --git a/packages/shorebird_cli/lib/src/commands/init_command.dart b/packages/shorebird_cli/lib/src/commands/init_command.dart index 0e50c84d..1e85ebb8 100644 --- a/packages/shorebird_cli/lib/src/commands/init_command.dart +++ b/packages/shorebird_cli/lib/src/commands/init_command.dart @@ -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( diff --git a/packages/shorebird_cli/lib/src/commands/login_ci_command.dart b/packages/shorebird_cli/lib/src/commands/login_ci_command.dart index ba0b47ea..b34076a6 100644 --- a/packages/shorebird_cli/lib/src/commands/login_ci_command.dart +++ b/packages/shorebird_cli/lib/src/commands/login_ci_command.dart @@ -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; } diff --git a/packages/shorebird_cli/lib/src/commands/login_command.dart b/packages/shorebird_cli/lib/src/commands/login_command.dart index 97f035cc..4e153997 100644 --- a/packages/shorebird_cli/lib/src/commands/login_command.dart +++ b/packages/shorebird_cli/lib/src/commands/login_command.dart @@ -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}.''') diff --git a/packages/shorebird_cli/lib/src/commands/logout_command.dart b/packages/shorebird_cli/lib/src/commands/logout_command.dart index cc357820..b7aa994f 100644 --- a/packages/shorebird_cli/lib/src/commands/logout_command.dart +++ b/packages/shorebird_cli/lib/src/commands/logout_command.dart @@ -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(); diff --git a/packages/shorebird_cli/lib/src/commands/patch/aar_patcher.dart b/packages/shorebird_cli/lib/src/commands/patch/aar_patcher.dart index 53064e7f..509928c0 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/aar_patcher.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/aar_patcher.dart @@ -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, diff --git a/packages/shorebird_cli/lib/src/commands/patch/android_patcher.dart b/packages/shorebird_cli/lib/src/commands/patch/android_patcher.dart index e8547e18..08e92439 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/android_patcher.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/android_patcher.dart @@ -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 `/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 = {}; 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. ' diff --git a/packages/shorebird_cli/lib/src/commands/patch/ios_framework_patcher.dart b/packages/shorebird_cli/lib/src/commands/patch/ios_framework_patcher.dart index 99514019..a6cb463a 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/ios_framework_patcher.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/ios_framework_patcher.dart @@ -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; diff --git a/packages/shorebird_cli/lib/src/commands/patch/ios_patcher.dart b/packages/shorebird_cli/lib/src/commands/patch/ios_patcher.dart index f71145d1..4a5801ec 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/ios_patcher.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/ios_patcher.dart @@ -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> _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 _buildInterpreterBytecodePatch(List 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 _dartDefinesForBytecode(List buildArgs) { + final defines = []; + 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 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 _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(); + } 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 = []; + 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(); + 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 _buildArgValues(List args, String name) { + final values = []; + 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 _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(); + 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 = []; + 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 _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.generate(12, (_) => random.nextInt(256)); + } + + bool _looksLikeNativeExecutable(List 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(0, (value, byte) => (value << 8) | byte); + return const { + 0xfeedface, + 0xfeedfacf, + 0xcefaedfe, + 0xcffaedfe, + 0xcafebabe, + 0xbebafeca, + }.contains(magic); + } + @override Future extractReleaseVersionFromArtifact(File artifact) async { final archivePath = artifactManager.getXcarchiveDirectory()?.path; diff --git a/packages/shorebird_cli/lib/src/commands/patch/linux_patcher.dart b/packages/shorebird_cli/lib/src/commands/patch/linux_patcher.dart index 8173777b..15fafd5a 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/linux_patcher.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/linux_patcher.dart @@ -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( diff --git a/packages/shorebird_cli/lib/src/commands/patch/macos_patcher.dart b/packages/shorebird_cli/lib/src/commands/patch/macos_patcher.dart index 88cde955..fde2580e 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/macos_patcher.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/macos_patcher.dart @@ -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(); diff --git a/packages/shorebird_cli/lib/src/commands/patch/patch_command.dart b/packages/shorebird_cli/lib/src/commands/patch/patch_command.dart index 8651bd2b..03b8d7cc 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/patch_command.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/patch_command.dart @@ -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 = []; 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, diff --git a/packages/shorebird_cli/lib/src/commands/patch/patcher.dart b/packages/shorebird_cli/lib/src/commands/patch/patcher.dart index 16b2922a..c1857980 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/patcher.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/patcher.dart @@ -68,7 +68,7 @@ More info: ${troubleshootingUrl.toLink()}. List 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 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 buildNameAndNumberArgsFromReleaseVersion( String? releaseVersion, ) { diff --git a/packages/shorebird_cli/lib/src/commands/patch/windows_patcher.dart b/packages/shorebird_cli/lib/src/commands/patch/windows_patcher.dart index 9ba8f8f1..44e5506f 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/windows_patcher.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/windows_patcher.dart @@ -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( diff --git a/packages/shorebird_cli/lib/src/commands/patches/promote_command.dart b/packages/shorebird_cli/lib/src/commands/patches/promote_command.dart index 5147fd91..9a08fb86 100644 --- a/packages/shorebird_cli/lib/src/commands/patches/promote_command.dart +++ b/packages/shorebird_cli/lib/src/commands/patches/promote_command.dart @@ -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; } diff --git a/packages/shorebird_cli/lib/src/commands/release/aar_releaser.dart b/packages/shorebird_cli/lib/src/commands/release/aar_releaser.dart index e32e0bac..b956cdcc 100644 --- a/packages/shorebird_cli/lib/src/commands/release/aar_releaser.dart +++ b/packages/shorebird_cli/lib/src/commands/release/aar_releaser.dart @@ -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}' + } } } diff --git a/packages/shorebird_cli/lib/src/commands/release/android_releaser.dart b/packages/shorebird_cli/lib/src/commands/release/android_releaser.dart index bb24d545..698ec3d5 100644 --- a/packages/shorebird_cli/lib/src/commands/release/android_releaser.dart +++ b/packages/shorebird_cli/lib/src/commands/release/android_releaser.dart @@ -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); } diff --git a/packages/shorebird_cli/lib/src/commands/release/macos_releaser.dart b/packages/shorebird_cli/lib/src/commands/release/macos_releaser.dart index 4f07887e..ef80a55e 100644 --- a/packages/shorebird_cli/lib/src/commands/release/macos_releaser.dart +++ b/packages/shorebird_cli/lib/src/commands/release/macos_releaser.dart @@ -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. '''; } diff --git a/packages/shorebird_cli/lib/src/commands/release/release_command.dart b/packages/shorebird_cli/lib/src/commands/release/release_command.dart index fd9c4670..1ff3ac1c 100644 --- a/packages/shorebird_cli/lib/src/commands/release/release_command.dart +++ b/packages/shorebird_cli/lib/src/commands/release/release_command.dart @@ -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(''' diff --git a/packages/shorebird_cli/lib/src/commands/release/releaser.dart b/packages/shorebird_cli/lib/src/commands/release/releaser.dart index 09a55517..a8702254 100644 --- a/packages/shorebird_cli/lib/src/commands/release/releaser.dart +++ b/packages/shorebird_cli/lib/src/commands/release/releaser.dart @@ -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 uploadSupplementArtifact({ required String appId, required int releaseId, diff --git a/packages/shorebird_cli/lib/src/config/shorebird_yaml.dart b/packages/shorebird_cli/lib/src/config/shorebird_yaml.dart index f7906e85..0da62f50 100644 --- a/packages/shorebird_cli/lib/src/config/shorebird_yaml.dart +++ b/packages/shorebird_cli/lib/src/config/shorebird_yaml.dart @@ -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. diff --git a/packages/shorebird_cli/lib/src/config/shorebird_yaml.g.dart b/packages/shorebird_cli/lib/src/config/shorebird_yaml.g.dart index 32a49a5d..8c4744df 100644 --- a/packages/shorebird_cli/lib/src/config/shorebird_yaml.g.dart +++ b/packages/shorebird_cli/lib/src/config/shorebird_yaml.g.dart @@ -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 _$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 = { diff --git a/packages/shorebird_cli/lib/src/executables/patch_executable.dart b/packages/shorebird_cli/lib/src/executables/patch_executable.dart index 60667307..c46afaff 100644 --- a/packages/shorebird_cli/lib/src/executables/patch_executable.dart +++ b/packages/shorebird_cli/lib/src/executables/patch_executable.dart @@ -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 = diff --git a/packages/shorebird_cli/lib/src/metadata/build_environment_metadata.dart b/packages/shorebird_cli/lib/src/metadata/build_environment_metadata.dart index 5101dee9..8bcd3fd9 100644 --- a/packages/shorebird_cli/lib/src/metadata/build_environment_metadata.dart +++ b/packages/shorebird_cli/lib/src/metadata/build_environment_metadata.dart @@ -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 { diff --git a/packages/shorebird_cli/lib/src/metadata/create_patch_metadata.dart b/packages/shorebird_cli/lib/src/metadata/create_patch_metadata.dart index c5409a1e..c0548d89 100644 --- a/packages/shorebird_cli/lib/src/metadata/create_patch_metadata.dart +++ b/packages/shorebird_cli/lib/src/metadata/create_patch_metadata.dart @@ -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 { diff --git a/packages/shorebird_cli/lib/src/metadata/update_release_metadata.dart b/packages/shorebird_cli/lib/src/metadata/update_release_metadata.dart index 214c7eb1..5923c45e 100644 --- a/packages/shorebird_cli/lib/src/metadata/update_release_metadata.dart +++ b/packages/shorebird_cli/lib/src/metadata/update_release_metadata.dart @@ -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 { diff --git a/packages/shorebird_cli/lib/src/network_checker.dart b/packages/shorebird_cli/lib/src/network_checker.dart index 16eb70d0..58da3fe1 100644 --- a/packages/shorebird_cli/lib/src/network_checker.dart +++ b/packages/shorebird_cli/lib/src/network_checker.dart @@ -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 get urlsToCheck { + final hostedUri = + shorebirdEnv.hostedUri ?? Uri.parse(ShorebirdEnv.defaultHostedUrl); + final urls = [hostedUri, shorebirdEnv.authServiceUri]; + final seen = {}; + return [ + for (final url in urls) + if (seen.add(url.toString())) url, + ]; + } /// Verify that each of [urlsToCheck] responds to an HTTP GET request. Future checkReachability() async { diff --git a/packages/shorebird_cli/lib/src/platform/apple/apple.dart b/packages/shorebird_cli/lib/src/platform/apple/apple.dart index 0f3c6b79..5908a63b 100644 --- a/packages/shorebird_cli/lib/src/platform/apple/apple.dart +++ b/packages/shorebird_cli/lib/src/platform/apple/apple.dart @@ -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". diff --git a/packages/shorebird_cli/lib/src/shorebird_artifacts.dart b/packages/shorebird_cli/lib/src/shorebird_artifacts.dart index 410b5521..0e246b87 100644 --- a/packages/shorebird_cli/lib/src/shorebird_artifacts.dart +++ b/packages/shorebird_cli/lib/src/shorebird_artifacts.dart @@ -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', + ), + ); + } } diff --git a/packages/shorebird_cli/lib/src/shorebird_cli_command_runner.dart b/packages/shorebird_cli/lib/src/shorebird_cli_command_runner.dart index f2ccb661..38c0cd66 100644 --- a/packages/shorebird_cli/lib/src/shorebird_cli_command_runner.dart +++ b/packages/shorebird_cli/lib/src/shorebird_cli_command_runner.dart @@ -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(''' diff --git a/packages/shorebird_cli/lib/src/shorebird_documentation.dart b/packages/shorebird_cli/lib/src/shorebird_documentation.dart index 881f5854..5946d8a9 100644 --- a/packages/shorebird_cli/lib/src/shorebird_documentation.dart +++ b/packages/shorebird_cli/lib/src/shorebird_documentation.dart @@ -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'; diff --git a/packages/shorebird_cli/lib/src/shorebird_env.dart b/packages/shorebird_cli/lib/src/shorebird_env.dart index 3b828d66..252aa2e4 100644 --- a/packages/shorebird_cli/lib/src/shorebird_env.dart +++ b/packages/shorebird_cli/lib/src/shorebird_env.dart @@ -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); } } diff --git a/packages/shorebird_cli/lib/src/shorebird_flutter.dart b/packages/shorebird_cli/lib/src/shorebird_flutter.dart index 1490708e..7f6fa723 100644 --- a/packages/shorebird_cli/lib/src/shorebird_flutter.dart +++ b/packages/shorebird_cli/lib/src/shorebird_flutter.dart @@ -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 get precacheArgs => ['--android', if (platform.isMacOS) '--ios']; diff --git a/packages/shorebird_cli/lib/src/shorebird_process.dart b/packages/shorebird_cli/lib/src/shorebird_process.dart index a1ea58a4..a417df5a 100644 --- a/packages/shorebird_cli/lib/src/shorebird_process.dart +++ b/packages/shorebird_cli/lib/src/shorebird_process.dart @@ -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 {}; diff --git a/packages/shorebird_cli/lib/src/shorebird_validator.dart b/packages/shorebird_cli/lib/src/shorebird_validator.dart index 8f738611..a9bc00fa 100644 --- a/packages/shorebird_cli/lib/src/shorebird_validator.dart +++ b/packages/shorebird_cli/lib/src/shorebird_validator.dart @@ -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(); } diff --git a/packages/shorebird_cli/lib/src/shorebird_web_console.dart b/packages/shorebird_cli/lib/src/shorebird_web_console.dart index b649a42d..0aee27d2 100644 --- a/packages/shorebird_cli/lib/src/shorebird_web_console.dart +++ b/packages/shorebird_cli/lib/src/shorebird_web_console.dart @@ -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. diff --git a/packages/shorebird_cli/lib/src/validators/android_internet_permission_validator.dart b/packages/shorebird_cli/lib/src/validators/android_internet_permission_validator.dart index 46ce93bd..7b18e137 100644 --- a/packages/shorebird_cli/lib/src/validators/android_internet_permission_validator.dart +++ b/packages/shorebird_cli/lib/src/validators/android_internet_permission_validator.dart @@ -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( diff --git a/packages/shorebird_cli/pubspec.yaml b/packages/shorebird_cli/pubspec.yaml index 417a59bc..0d2a7134 100644 --- a/packages/shorebird_cli/pubspec.yaml +++ b/packages/shorebird_cli/pubspec.yaml @@ -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 diff --git a/packages/shorebird_cli/test/src/artifact_builder/artifact_builder_test.dart b/packages/shorebird_cli/test/src/artifact_builder/artifact_builder_test.dart index 4a92381e..62920bb3 100644 --- a/packages/shorebird_cli/test/src/artifact_builder/artifact_builder_test.dart +++ b/packages/shorebird_cli/test/src/artifact_builder/artifact_builder_test.dart @@ -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; + 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()), + ); + }); + }); + }); }); group('buildWindowsApp', () { diff --git a/packages/shorebird_cli/test/src/auth/auth_test.dart b/packages/shorebird_cli/test/src/auth/auth_test.dart index 3fb120bd..92f0fe7b 100644 --- a/packages/shorebird_cli/test/src/auth/auth_test.dart +++ b/packages/shorebird_cli/test/src/auth/auth_test.dart @@ -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( diff --git a/packages/shorebird_cli/test/src/cache_test.dart b/packages/shorebird_cli/test/src/cache_test.dart index cbe65256..0a458b79 100644 --- a/packages/shorebird_cli/test/src/cache_test.dart +++ b/packages/shorebird_cli/test/src/cache_test.dart @@ -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().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().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().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().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().map((r) => r.url).toList(); + + String perEngine(String name) => + '${cache.artifactBaseUrl}/shorebird/$shorebirdEngineRevision/$name'; + + expect(requests, contains(Uri.parse(perEngine('aot-tools.dill')))); + }); }); }); }); diff --git a/packages/shorebird_cli/test/src/commands/doctor_command_test.dart b/packages/shorebird_cli/test/src/commands/doctor_command_test.dart index bec69e6f..8e52cefb 100644 --- a/packages/shorebird_cli/test/src/commands/doctor_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/doctor_command_test.dart @@ -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 diff --git a/packages/shorebird_cli/test/src/commands/init_command_test.dart b/packages/shorebird_cli/test/src/commands/init_command_test.dart index 928fcff5..530c090f 100644 --- a/packages/shorebird_cli/test/src/commands/init_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/init_command_test.dart @@ -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))}''', '', ]), ), diff --git a/packages/shorebird_cli/test/src/commands/login_ci_command_test.dart b/packages/shorebird_cli/test/src/commands/login_ci_command_test.dart index cc2554c9..2c5ad47f 100644 --- a/packages/shorebird_cli/test/src/commands/login_ci_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/login_ci_command_test.dart @@ -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 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 { diff --git a/packages/shorebird_cli/test/src/commands/login_command_test.dart b/packages/shorebird_cli/test/src/commands/login_command_test.dart index 034d5d70..c72e60d9 100644 --- a/packages/shorebird_cli/test/src/commands/login_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/login_command_test.dart @@ -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 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); }); diff --git a/packages/shorebird_cli/test/src/commands/logout_command_test.dart b/packages/shorebird_cli/test/src/commands/logout_command_test.dart index 3ebd0887..f42d457a 100644 --- a/packages/shorebird_cli/test/src/commands/logout_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/logout_command_test.dart @@ -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); }); }); diff --git a/packages/shorebird_cli/test/src/commands/patch/android_patcher_test.dart b/packages/shorebird_cli/test/src/commands/patch/android_patcher_test.dart index f3815472..c72fc537 100644 --- a/packages/shorebird_cli/test/src/commands/patch/android_patcher_test.dart +++ b/packages/shorebird_cli/test/src/commands/patch/android_patcher_test.dart @@ -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', ), ), ), diff --git a/packages/shorebird_cli/test/src/commands/patch/ios_patcher_test.dart b/packages/shorebird_cli/test/src/commands/patch/ios_patcher_test.dart index 06aba60e..80d87b6f 100644 --- a/packages/shorebird_cli/test/src/commands/patch/ios_patcher_test.dart +++ b/packages/shorebird_cli/test/src/commands/patch/ios_patcher_test.dart @@ -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; + 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; + 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; + 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(), + ); + 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(), + ); + expect( + encrypted.decrypt(open_patch.readKey(keyHex)).payload, + equals(patchBytes), + ); + }, + ); + }); + group('when uses linker', () { const linkPercentage = 50.0; late File analyzeSnapshotFile; diff --git a/packages/shorebird_cli/test/src/commands/release/aar_releaser_test.dart b/packages/shorebird_cli/test/src/commands/release/aar_releaser_test.dart index 86aac3df..9f58d660 100644 --- a/packages/shorebird_cli/test/src/commands/release/aar_releaser_test.dart +++ b/packages/shorebird_cli/test/src/commands/release/aar_releaser_test.dart @@ -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}' + } } } diff --git a/packages/shorebird_cli/test/src/commands/release/macos_releaser_test.dart b/packages/shorebird_cli/test/src/commands/release/macos_releaser_test.dart index 9206332c..27e11e73 100644 --- a/packages/shorebird_cli/test/src/commands/release/macos_releaser_test.dart +++ b/packages/shorebird_cli/test/src/commands/release/macos_releaser_test.dart @@ -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', ), ); }); diff --git a/packages/shorebird_cli/test/src/config/shorebird_yaml_test.dart b/packages/shorebird_cli/test/src/config/shorebird_yaml_test.dart index 736be958..8f965468 100644 --- a/packages/shorebird_cli/test/src/config/shorebird_yaml_test.dart +++ b/packages/shorebird_cli/test/src/config/shorebird_yaml_test.dart @@ -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 diff --git a/packages/shorebird_cli/test/src/network_checker_test.dart b/packages/shorebird_cli/test/src/network_checker_test.dart index fc433432..667e943c 100644 --- a/packages/shorebird_cli/test/src/network_checker_test.dart +++ b/packages/shorebird_cli/test/src/network_checker_test.dart @@ -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 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); }); }); }); diff --git a/packages/shorebird_cli/test/src/shorebird_artifacts_test.dart b/packages/shorebird_cli/test/src/shorebird_artifacts_test.dart index 1d83aff4..d570ff1b 100644 --- a/packages/shorebird_cli/test/src/shorebird_artifacts_test.dart +++ b/packages/shorebird_cli/test/src/shorebird_artifacts_test.dart @@ -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', + ), + ), + ); + }); }); }); } diff --git a/packages/shorebird_cli/test/src/shorebird_cli_command_runner_test.dart b/packages/shorebird_cli/test/src/shorebird_cli_command_runner_test.dart index 89c085e7..e539a129 100644 --- a/packages/shorebird_cli/test/src/shorebird_cli_command_runner_test.dart +++ b/packages/shorebird_cli/test/src/shorebird_cli_command_runner_test.dart @@ -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); diff --git a/packages/shorebird_cli/test/src/shorebird_env_test.dart b/packages/shorebird_cli/test/src/shorebird_env_test.dart index 6220066e..64d227bc 100644 --- a/packages/shorebird_cli/test/src/shorebird_env_test.dart +++ b/packages/shorebird_cli/test/src/shorebird_env_test.dart @@ -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), ); }); diff --git a/packages/shorebird_cli/test/src/shorebird_flutter_test.dart b/packages/shorebird_cli/test/src/shorebird_flutter_test.dart index 6d2a3084..0a9a0e0b 100644 --- a/packages/shorebird_cli/test/src/shorebird_flutter_test.dart +++ b/packages/shorebird_cli/test/src/shorebird_flutter_test.dart @@ -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'], ), diff --git a/packages/shorebird_cli/test/src/shorebird_process_test.dart b/packages/shorebird_cli/test/src/shorebird_process_test.dart index 3b4c2055..c631d9a9 100644 --- a/packages/shorebird_cli/test/src/shorebird_process_test.dart +++ b/packages/shorebird_cli/test/src/shorebird_process_test.dart @@ -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( diff --git a/packages/shorebird_cli/test/src/shorebird_validator_test.dart b/packages/shorebird_cli/test/src/shorebird_validator_test.dart index 856a1556..1d6ac937 100644 --- a/packages/shorebird_cli/test/src/shorebird_validator_test.dart +++ b/packages/shorebird_cli/test/src/shorebird_validator_test.dart @@ -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.''', ), ]); }); diff --git a/packages/shorebird_cli/test/src/shorebird_web_console_test.dart b/packages/shorebird_cli/test/src/shorebird_web_console_test.dart index 36a5e17e..5bb156db 100644 --- a/packages/shorebird_cli/test/src/shorebird_web_console_test.dart +++ b/packages/shorebird_cli/test/src/shorebird_web_console_test.dart @@ -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 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'), ); }); }); diff --git a/packages/shorebird_code_push_client/lib/src/code_push_client.dart b/packages/shorebird_code_push_client/lib/src/code_push_client.dart index 58484feb..9bac5a9f 100644 --- a/packages/shorebird_code_push_client/lib/src/code_push_client.dart +++ b/packages/shorebird_code_push_client/lib/src/code_push_client.dart @@ -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 = {'x-version': packageVersion}; diff --git a/packages/shorebird_code_push_client/pubspec.yaml b/packages/shorebird_code_push_client/pubspec.yaml index e344bea8..7087b6b3 100644 --- a/packages/shorebird_code_push_client/pubspec.yaml +++ b/packages/shorebird_code_push_client/pubspec.yaml @@ -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 diff --git a/packages/shorebird_code_push_client/test/src/code_push_client_test.dart b/packages/shorebird_code_push_client/test/src/code_push_client_test.dart index fec38a67..64c3f6a3 100644 --- a/packages/shorebird_code_push_client/test/src/code_push_client_test.dart +++ b/packages/shorebird_code_push_client/test/src/code_push_client_test.dart @@ -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( diff --git a/packages/shorebird_code_push_protocol/README.md b/packages/shorebird_code_push_protocol/README.md index 05490bfc..71b933b5 100644 --- a/packages/shorebird_code_push_protocol/README.md +++ b/packages/shorebird_code_push_protocol/README.md @@ -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 ``` diff --git a/packages/shorebird_code_push_protocol/pubspec.yaml b/packages/shorebird_code_push_protocol/pubspec.yaml index 82caa819..e8ed1e05 100644 --- a/packages/shorebird_code_push_protocol/pubspec.yaml +++ b/packages/shorebird_code_push_protocol/pubspec.yaml @@ -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 diff --git a/packages/shorebird_code_push_protocol/tool/gen.dart b/packages/shorebird_code_push_protocol/tool/gen.dart index e276ec04..0d78895b 100644 --- a/packages/shorebird_code_push_protocol/tool/gen.dart +++ b/packages/shorebird_code_push_protocol/tool/gen.dart @@ -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. diff --git a/third_party/flutter/bin/internal/shared.sh b/third_party/flutter/bin/internal/shared.sh index 57ca8339..dfffafef 100755 --- a/third_party/flutter/bin/internal/shared.sh +++ b/third_party/flutter/bin/internal/shared.sh @@ -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 {