fix: download arm64 patch binary on Apple Silicon (#3713)
Co-authored-by: Eric Seidel <eric@shorebird.dev>
This commit is contained in:
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user