fix: download arm64 patch binary on Apple Silicon (#3713)

Co-authored-by: Eric Seidel <eric@shorebird.dev>
This commit is contained in:
Brandon DeRosier
2026-05-02 15:11:43 -07:00
committed by GitHub
parent ddd58f628f
commit f8f4354fc3
3 changed files with 141 additions and 9 deletions
+26 -8
View File
@@ -1,3 +1,4 @@
import 'dart:ffi' show Abi;
import 'dart:io' hide Platform;
import 'package:http/http.dart' as http;
@@ -5,13 +6,16 @@ import 'package:path/path.dart' as p;
import 'package:platform/platform.dart';
import 'package:retry/retry.dart';
import 'package:scoped_deps/scoped_deps.dart';
import 'package:shorebird_cli/src/abi.dart';
import 'package:shorebird_cli/src/artifact_manager.dart';
import 'package:shorebird_cli/src/checksum_checker.dart';
import 'package:shorebird_cli/src/flutter_version_constraints.dart';
import 'package:shorebird_cli/src/http_client/http_client.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_env.dart';
import 'package:shorebird_cli/src/shorebird_flutter.dart';
import 'package:shorebird_cli/src/shorebird_process.dart';
/// {@template cache_update_failure}
@@ -147,8 +151,10 @@ abstract class CachedArtifact {
/// Should the artifact be marked executable.
bool get isExecutable;
/// The URL from which the artifact can be downloaded.
String get storageUrl;
/// The URL from which the artifact can be downloaded. Returned as a
/// future so subclasses can resolve runtime context (e.g. the active
/// Flutter version) before deciding which artifact to fetch.
Future<String> get storageUrl;
/// Whether the artifact is required for Shorebird to function.
/// If we fail to fetch it we will exit with an error.
@@ -197,7 +203,8 @@ abstract class CachedArtifact {
final updateProgress = logger.progress('Downloading $fileName...');
final request = http.Request('GET', Uri.parse(storageUrl));
final url = await storageUrl;
final request = http.Request('GET', Uri.parse(url));
final http.StreamedResponse response;
try {
response = await httpClient.send(request);
@@ -205,7 +212,7 @@ abstract class CachedArtifact {
throw CacheUpdateFailure('''
Failed to download $fileName: $error
If you're behind a firewall/proxy, please, make sure shorebird_cli is
allowed to access $storageUrl.''');
allowed to access $url.''');
}
if (response.statusCode != HttpStatus.ok) {
@@ -304,7 +311,7 @@ class AotToolsArtifact extends CachedArtifact {
);
@override
String get storageUrl =>
Future<String> get storageUrl async =>
'${cache.storageBaseUrl}/${cache.storageBucket}/shorebird/${shorebirdEnv.shorebirdEngineRevision}/$fileName';
@override
@@ -339,10 +346,12 @@ class PatchArtifact extends CachedArtifact {
}
@override
String get storageUrl {
Future<String> get storageUrl async {
var artifactName = 'patch-';
if (platform.isMacOS) {
artifactName += 'darwin-x64.zip';
final useArm64 =
abi.current == Abi.macosArm64 && await _supportsArm64Patch();
artifactName += useArm64 ? 'darwin-arm64.zip' : 'darwin-x64.zip';
} else if (platform.isLinux) {
artifactName += 'linux-x64.zip';
} else if (platform.isWindows) {
@@ -352,6 +361,15 @@ class PatchArtifact extends CachedArtifact {
return '${cache.storageBaseUrl}/${cache.storageBucket}/shorebird/${shorebirdEnv.shorebirdEngineRevision}/$artifactName';
}
Future<bool> _supportsArm64Patch() async {
final revision = shorebirdEnv.flutterRevision;
final version = await shorebirdFlutter.resolveFlutterVersion(revision);
return arm64PatchSupportConstraint.isSatisfiedBy(
version: version ?? arm64PatchSupportConstraint.minVersion,
revision: revision,
);
}
@override
String? get checksum => null;
}
@@ -371,7 +389,7 @@ class BundleToolArtifact extends CachedArtifact {
bool get isExecutable => false;
@override
String get storageUrl {
Future<String> get storageUrl async {
return 'https://github.com/google/bundletool/releases/download/1.18.1/bundletool-all-1.18.1.jar';
}
@@ -65,6 +65,17 @@ class FlutterSupportConstraint {
version >= minVersion || allowedRevisions.contains(revision);
}
/// Flutter support for downloading the `patch-darwin-arm64.zip` artifact
/// on Apple Silicon. The arm64 upload was added in shorebirdtech/flutter#129
/// (merged 2026-04-09); the tip of `flutter_release/3.41.7` and every
/// subsequent release branch ships with it.
///
/// Pre-floor pins fall back to `patch-darwin-x64.zip`, which works on
/// Apple Silicon when Rosetta is installed.
final arm64PatchSupportConstraint = FlutterSupportConstraint(
minVersion: Version(3, 41, 7),
);
/// Flutter support for `flutter build --shorebird-trace=<path>` for emitting
/// Chrome Trace Event Format build traces.
///
+104 -1
View File
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:ffi' show Abi;
import 'dart:io';
import 'package:archive/archive_io.dart';
@@ -7,14 +8,18 @@ import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
import 'package:path/path.dart' as p;
import 'package:platform/platform.dart';
import 'package:pub_semver/pub_semver.dart';
import 'package:scoped_deps/scoped_deps.dart';
import 'package:shorebird_cli/src/abi.dart';
import 'package:shorebird_cli/src/artifact_manager.dart';
import 'package:shorebird_cli/src/cache.dart';
import 'package:shorebird_cli/src/checksum_checker.dart';
import 'package:shorebird_cli/src/flutter_version_constraints.dart';
import 'package:shorebird_cli/src/http_client/http_client.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_flutter.dart';
import 'package:shorebird_cli/src/shorebird_process.dart';
import 'package:test/test.dart';
@@ -25,6 +30,7 @@ void main() {
group(Cache, () {
const shorebirdEngineRevision = 'test-revision';
late LocalAbi mockAbi;
late ArtifactManager artifactManager;
late Cache cache;
late ChecksumChecker checksumChecker;
@@ -35,12 +41,14 @@ void main() {
late Process chmodProcess;
late Progress progress;
late ShorebirdEnv shorebirdEnv;
late ShorebirdFlutter shorebirdFlutter;
late ShorebirdProcess shorebirdProcess;
R runWithOverrides<R>(R Function() body) {
return runScoped(
() => body(),
values: {
abiRef.overrideWith(() => mockAbi),
artifactManagerRef.overrideWith(() => artifactManager),
cacheRef.overrideWith(() => cache),
checksumCheckerRef.overrideWith(() => checksumChecker),
@@ -49,6 +57,7 @@ void main() {
platformRef.overrideWith(() => platform),
processRef.overrideWith(() => shorebirdProcess),
shorebirdEnvRef.overrideWith(() => shorebirdEnv),
shorebirdFlutterRef.overrideWith(() => shorebirdFlutter),
},
);
}
@@ -73,6 +82,7 @@ void main() {
});
setUp(() {
mockAbi = MockAbi();
artifactManager = MockArtifactManager();
chmodProcess = MockProcess();
checksumChecker = MockChecksumChecker();
@@ -81,8 +91,14 @@ void main() {
platform = MockPlatform();
progress = MockProgress();
shorebirdEnv = MockShorebirdEnv();
shorebirdFlutter = MockShorebirdFlutter();
shorebirdProcess = MockShorebirdProcess();
when(() => mockAbi.current).thenReturn(Abi.macosX64);
when(
() => shorebirdFlutter.resolveFlutterVersion(any()),
).thenAnswer((_) async => null);
shorebirdRoot = Directory.systemTemp.createTempSync();
when(
() => artifactManager.extractZip(
@@ -98,6 +114,9 @@ void main() {
when(
() => shorebirdEnv.shorebirdEngineRevision,
).thenReturn(shorebirdEngineRevision);
when(
() => shorebirdEnv.flutterRevision,
).thenReturn('test-flutter-revision');
when(() => shorebirdEnv.shorebirdRoot).thenReturn(shorebirdRoot);
when(() => platform.environment).thenReturn({});
@@ -204,6 +223,89 @@ void main() {
});
});
group('storageUrl', () {
group('when on macOS', () {
setUp(() {
setMockPlatform(Platform.macOS);
});
test(
'uses darwin-arm64 on Apple Silicon when the Flutter version '
'satisfies the arm64 patch constraint',
() async {
when(() => mockAbi.current).thenReturn(Abi.macosArm64);
when(
() => shorebirdFlutter.resolveFlutterVersion(any()),
).thenAnswer(
(_) async => arm64PatchSupportConstraint.minVersion,
);
final url = await runWithOverrides(
() => PatchArtifact(
cache: cache,
platform: platform,
).storageUrl,
);
expect(url, contains('patch-darwin-arm64.zip'));
},
);
test(
'uses darwin-x64 on Apple Silicon when the Flutter version '
'is below the arm64 patch constraint floor',
() async {
when(() => mockAbi.current).thenReturn(Abi.macosArm64);
when(
() => shorebirdFlutter.resolveFlutterVersion(any()),
).thenAnswer((_) async => Version(3, 41, 6));
final url = await runWithOverrides(
() => PatchArtifact(
cache: cache,
platform: platform,
).storageUrl,
);
expect(url, contains('patch-darwin-x64.zip'));
},
);
test('uses darwin-x64 on Intel', () async {
when(() => mockAbi.current).thenReturn(Abi.macosX64);
final url = await runWithOverrides(
() =>
PatchArtifact(cache: cache, platform: platform).storageUrl,
);
expect(url, contains('patch-darwin-x64.zip'));
});
});
group('when on Linux', () {
setUp(() {
setMockPlatform(Platform.linux);
});
test('uses linux-x64', () async {
final url = await runWithOverrides(
() =>
PatchArtifact(cache: cache, platform: platform).storageUrl,
);
expect(url, contains('patch-linux-x64.zip'));
});
});
group('when on Windows', () {
setUp(() {
setMockPlatform(Platform.windows);
});
test('uses windows-x64', () async {
final url = await runWithOverrides(
() =>
PatchArtifact(cache: cache, platform: platform).storageUrl,
);
expect(url, contains('patch-windows-x64.zip'));
});
});
});
group('when an exception happens', () {
test('throws CacheUpdateFailure', () async {
const exception = SocketException('test');
@@ -571,5 +673,6 @@ class _TestCachedArtifact extends CachedArtifact {
File get file => File(p.join(_location.path, fileName));
@override
String get storageUrl => 'https://example.com/test_artifact.exe';
Future<String> get storageUrl async =>
'https://example.com/test_artifact.exe';
}