From c3dc6f5e2cb2aad516f67ba2889d90d67f43c321 Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Mon, 20 Nov 2023 15:27:34 -0600 Subject: [PATCH] feat(shorebird_cli): `shorebird patch ios-alpha` generates link file (#1511) --- packages/shorebird_cli/bin/shorebird.dart | 1 + .../src/commands/patch/patch_ios_command.dart | 74 +++++++- .../lib/src/executables/aot_tools.dart | 7 + .../shorebird_cli/lib/src/shorebird_env.dart | 14 ++ .../patch/patch_ios_command_test.dart | 159 +++++++++++++++++- packages/shorebird_cli/test/src/mocks.dart | 2 + .../test/src/shorebird_env_test.dart | 23 +++ 7 files changed, 271 insertions(+), 9 deletions(-) diff --git a/packages/shorebird_cli/bin/shorebird.dart b/packages/shorebird_cli/bin/shorebird.dart index aeb140fe..93e38747 100644 --- a/packages/shorebird_cli/bin/shorebird.dart +++ b/packages/shorebird_cli/bin/shorebird.dart @@ -28,6 +28,7 @@ Future main(List args) async { adbRef, androidSdkRef, androidStudioRef, + aotToolsRef, artifactManagerRef, authRef, bundletoolRef, diff --git a/packages/shorebird_cli/lib/src/commands/patch/patch_ios_command.dart b/packages/shorebird_cli/lib/src/commands/patch/patch_ios_command.dart index 32ae6e34..ed46ec05 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/patch_ios_command.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/patch_ios_command.dart @@ -11,6 +11,7 @@ import 'package:shorebird_cli/src/command.dart'; import 'package:shorebird_cli/src/config/config.dart'; import 'package:shorebird_cli/src/deployment_track.dart'; import 'package:shorebird_cli/src/doctor.dart'; +import 'package:shorebird_cli/src/executables/executables.dart'; import 'package:shorebird_cli/src/formatters/file_size_formatter.dart'; import 'package:shorebird_cli/src/ios.dart'; import 'package:shorebird_cli/src/logger.dart'; @@ -220,6 +221,56 @@ Current Flutter Revision: $originalFlutterRevision return ExitCode.software.code; } + final appDirectory = getAppDirectory(); + + if (appDirectory == null) { + logger.err('Unable to find .app directory within .xcarchive.'); + return ExitCode.software.code; + } + + final base = File( + p.join( + appDirectory.path, + 'Frameworks', + 'App.framework', + 'App', + ), + ); + + if (!base.existsSync()) { + logger.err('Unable to find base AOT file at ${base.path}'); + return ExitCode.software.code; + } + + final patch = File(_aotOutputPath); + + if (!patch.existsSync()) { + logger.err('Unable to find patch AOT file at ${patch.path}'); + return ExitCode.software.code; + } + + final analyzeSnapshot = shorebirdEnv.analyzeSnapshotFile; + + if (!analyzeSnapshot.existsSync()) { + logger.err('Unable to find analyze_snapshot at ${analyzeSnapshot.path}'); + return ExitCode.software.code; + } + + final linkProgress = logger.progress('Linking AOT files'); + try { + await aotTools.link( + base: base.path, + patch: patch.path, + analyzeSnapshot: analyzeSnapshot.path, + workingDirectory: _buildDirectory, + ); + } catch (error) { + linkProgress.fail('Failed to link AOT files: $error'); + return ExitCode.software.code; + } + + linkProgress.complete(); + if (dryRun) { logger ..info('No issues detected.') @@ -227,14 +278,14 @@ Current Flutter Revision: $originalFlutterRevision return ExitCode.success.code; } - final aotFile = File(_aotOutputPath); - final aotFileSize = aotFile.statSync().size; + final patchFile = File(_vmcodeOutputPath); + final patchFileSize = patchFile.statSync().size; final summary = [ '''📱 App: ${lightCyan.wrap(app.displayName)} ${lightCyan.wrap('($appId)')}''', if (flavor != null) '🍧 Flavor: ${lightCyan.wrap(flavor)}', 'đŸ“Ļ Release Version: ${lightCyan.wrap(releaseVersion)}', - '''đŸ•šī¸ Platform: ${lightCyan.wrap(releasePlatform.name)} ${lightCyan.wrap('[$arch (${formatBytes(aotFileSize)})]')}''', + '''đŸ•šī¸ Platform: ${lightCyan.wrap(releasePlatform.name)} ${lightCyan.wrap('[$arch (${formatBytes(patchFileSize)})]')}''', if (isStaging) '🟠 Track: ${lightCyan.wrap('Staging')}' else @@ -268,9 +319,9 @@ ${summary.join('\n')} patchArtifactBundles: { Arch.arm64: PatchArtifactBundle( arch: arch, - path: aotFile.path, - hash: _hashFn(aotFile.readAsBytesSync()), - size: aotFileSize, + path: patchFile.path, + hash: _hashFn(patchFile.readAsBytesSync()), + size: patchFileSize, ), }, ); @@ -278,12 +329,21 @@ ${summary.join('\n')} return ExitCode.success.code; } - String get _aotOutputPath => p.join( + String get _buildDirectory => p.join( shorebirdEnv.getShorebirdProjectRoot()!.path, 'build', + ); + + String get _aotOutputPath => p.join( + _buildDirectory, 'out.aot', ); + String get _vmcodeOutputPath => p.join( + _buildDirectory, + 'out.vmcode', + ); + Future _buildPatch() async { final target = results['target'] as String?; final flavor = results['flavor'] as String?; diff --git a/packages/shorebird_cli/lib/src/executables/aot_tools.dart b/packages/shorebird_cli/lib/src/executables/aot_tools.dart index fd2640b0..9d48b36f 100644 --- a/packages/shorebird_cli/lib/src/executables/aot_tools.dart +++ b/packages/shorebird_cli/lib/src/executables/aot_tools.dart @@ -1,7 +1,14 @@ import 'package:path/path.dart' as p; +import 'package:scoped/scoped.dart'; import 'package:shorebird_cli/src/cache.dart'; import 'package:shorebird_cli/src/process.dart'; +/// A reference to a [AotTools] instance. +final aotToolsRef = create(AotTools.new); + +/// The [AotTools] instance available in the current zone. +AotTools get aotTools => read(aotToolsRef); + /// Wrapper around the shorebird `aot-tools` executable. class AotTools { static const executableName = 'aot-tools'; diff --git a/packages/shorebird_cli/lib/src/shorebird_env.dart b/packages/shorebird_cli/lib/src/shorebird_env.dart index 4326da00..5f3ee2d6 100644 --- a/packages/shorebird_cli/lib/src/shorebird_env.dart +++ b/packages/shorebird_cli/lib/src/shorebird_env.dart @@ -92,6 +92,20 @@ class ShorebirdEnv { ); } + File get analyzeSnapshotFile { + return File( + p.join( + flutterDirectory.path, + 'bin', + 'cache', + 'artifacts', + 'engine', + 'ios-release', + 'analyze_snapshot_arm64', + ), + ); + } + /// The `shorebird.yaml` file for this project. File getShorebirdYamlFile({required Directory cwd}) { return File(p.join(cwd.path, 'shorebird.yaml')); diff --git a/packages/shorebird_cli/test/src/commands/patch/patch_ios_command_test.dart b/packages/shorebird_cli/test/src/commands/patch/patch_ios_command_test.dart index 93b73ed7..30464498 100644 --- a/packages/shorebird_cli/test/src/commands/patch/patch_ios_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/patch/patch_ios_command_test.dart @@ -15,6 +15,7 @@ import 'package:shorebird_cli/src/commands/patch/patch.dart'; import 'package:shorebird_cli/src/config/config.dart'; import 'package:shorebird_cli/src/deployment_track.dart'; import 'package:shorebird_cli/src/doctor.dart'; +import 'package:shorebird_cli/src/executables/aot_tools.dart'; import 'package:shorebird_cli/src/logger.dart'; import 'package:shorebird_cli/src/os/operating_system_interface.dart'; import 'package:shorebird_cli/src/patch_diff_checker.dart'; @@ -44,6 +45,7 @@ void main() { const releasePlatform = ReleasePlatform.ios; const platformName = 'ios'; const elfAotSnapshotFileName = 'out.aot'; + const linkFileName = 'out.vmcode'; const ipaPath = 'build/ios/ipa/Runner.ipa'; const infoPlistContent = ''' @@ -122,12 +124,14 @@ flutter: group(PatchIosCommand, () { late ArgResults argResults; + late AotTools aotTools; late Auth auth; late CodePushClientWrapper codePushClientWrapper; late Directory flutterDirectory; late Directory shorebirdRoot; late Directory projectRoot; late File genSnapshotFile; + late File analyzeSnapshotFile; late Doctor doctor; late IosArchiveDiffer archiveDiffer; late Progress progress; @@ -150,6 +154,7 @@ flutter: return runScoped( body, values: { + aotToolsRef.overrideWith(() => aotTools), authRef.overrideWith(() => auth), codePushClientWrapperRef.overrideWith(() => codePushClientWrapper), doctorRef.overrideWith(() => doctor), @@ -184,6 +189,21 @@ flutter: ) ..createSync(recursive: true) ..writeAsStringSync(infoPlistContent); + File( + p.join( + projectRoot.path, + 'build', + 'ios', + 'archive', + 'Runner.xcarchive', + 'Products', + 'Applications', + 'Runner.app', + 'Frameworks', + 'App.framework', + 'App', + ), + ).createSync(recursive: true); File(p.join(projectRoot.path, ipaPath)).createSync(recursive: true); } @@ -204,6 +224,9 @@ flutter: File( p.join(projectRoot.path, 'build', elfAotSnapshotFileName), ).createSync(recursive: true); + File( + p.join(projectRoot.path, 'build', linkFileName), + ).createSync(recursive: true); } setUpAll(() { @@ -219,6 +242,7 @@ flutter: setUp(() { argResults = MockArgResults(); + aotTools = MockAotTools(); auth = MockAuth(); codePushClientWrapper = MockCodePushClientWrapper(); doctor = MockDoctor(); @@ -238,6 +262,18 @@ flutter: 'gen_snapshot_arm64', ), ); + analyzeSnapshotFile = File( + p.join( + flutterDirectory.path, + 'bin', + 'cache', + 'artifacts', + 'engine', + 'android-arm-release', + 'darwin-x64', + 'analyze_snapshot', + ), + )..createSync(recursive: true); archiveDiffer = MockIosArchiveDiffer(); progress = MockProgress(); logger = MockLogger(); @@ -260,6 +296,14 @@ flutter: when(() => argResults['codesign']).thenReturn(true); when(() => argResults['staging']).thenReturn(false); when(() => argResults.rest).thenReturn([]); + when( + () => aotTools.link( + base: any(named: 'base'), + patch: any(named: 'patch'), + analyzeSnapshot: any(named: 'analyzeSnapshot'), + workingDirectory: any(named: 'workingDirectory'), + ), + ).thenAnswer((_) async {}); when(() => auth.isAuthenticated).thenReturn(true); when(() => auth.client).thenReturn(httpClient); when( @@ -292,8 +336,9 @@ flutter: when(flutterValidator.validate).thenAnswer((_) async => []); when(() => logger.confirm(any())).thenReturn(true); when(() => logger.progress(any())).thenReturn(progress); - when(() => operatingSystemInterface.which('flutter')) - .thenReturn('/path/to/flutter'); + when( + () => operatingSystemInterface.which('flutter'), + ).thenReturn('/path/to/flutter'); when(() => platform.operatingSystem).thenReturn(Platform.macOS); when(() => platform.environment).thenReturn({}); when(() => platform.script).thenReturn(shorebirdRoot.uri); @@ -304,6 +349,9 @@ flutter: ).thenReturn(projectRoot); when(() => shorebirdEnv.flutterDirectory).thenReturn(flutterDirectory); when(() => shorebirdEnv.genSnapshotFile).thenReturn(genSnapshotFile); + when( + () => shorebirdEnv.analyzeSnapshotFile, + ).thenReturn(analyzeSnapshotFile); when(() => shorebirdEnv.flutterRevision).thenReturn(flutterRevision); when(() => shorebirdEnv.isRunningOnCI).thenReturn(false); when(() => shorebirdFlutter.useRevision(revision: any(named: 'revision'))) @@ -819,6 +867,113 @@ Please re-run the release command for this version or create a new release.'''), ); }); + test('exits with code 70 if appDirectory is not found', () async { + setUpProjectRoot(); + setUpProjectRootArtifacts(); + + File( + p.join( + projectRoot.path, + 'build', + 'ios', + 'archive', + 'Runner.xcarchive', + 'Products', + 'Applications', + 'Runner.app', + ), + ).deleteSync(recursive: true); + + final exitCode = await runWithOverrides(command.run); + + expect(exitCode, equals(ExitCode.software.code)); + verify( + () => logger.err('Unable to find .app directory within .xcarchive.'), + ).called(1); + }); + + test('exits with code 70 if base app is not found', () async { + setUpProjectRoot(); + setUpProjectRootArtifacts(); + + final base = File( + p.join( + projectRoot.path, + 'build', + 'ios', + 'archive', + 'Runner.xcarchive', + 'Products', + 'Applications', + 'Runner.app', + 'Frameworks', + 'App.framework', + 'App', + ), + )..deleteSync(recursive: true); + + final exitCode = await runWithOverrides(command.run); + + expect(exitCode, equals(ExitCode.software.code)); + verify( + () => logger.err('Unable to find base AOT file at ${base.path}'), + ).called(1); + }); + + test('exits with code 70 if patch AOT file is not found', () async { + setUpProjectRoot(); + setUpProjectRootArtifacts(); + + final patch = File( + p.join(projectRoot.path, 'build', elfAotSnapshotFileName), + )..deleteSync(recursive: true); + + final exitCode = await runWithOverrides(command.run); + + expect(exitCode, equals(ExitCode.software.code)); + verify( + () => logger.err('Unable to find patch AOT file at ${patch.path}'), + ).called(1); + }); + + test('exits with code 70 if analyze snapshot is not found', () async { + setUpProjectRoot(); + setUpProjectRootArtifacts(); + + analyzeSnapshotFile.deleteSync(recursive: true); + + final exitCode = await runWithOverrides(command.run); + + expect(exitCode, equals(ExitCode.software.code)); + verify( + () => logger.err( + 'Unable to find analyze_snapshot at ${analyzeSnapshotFile.path}', + ), + ).called(1); + }); + + test('exits with code 70 if linking fails', () async { + final exception = Exception('oops'); + when( + () => aotTools.link( + base: any(named: 'base'), + patch: any(named: 'patch'), + analyzeSnapshot: any(named: 'analyzeSnapshot'), + workingDirectory: any(named: 'workingDirectory'), + ), + ).thenThrow(exception); + + setUpProjectRoot(); + setUpProjectRootArtifacts(); + + final exitCode = await runWithOverrides(command.run); + + expect(exitCode, equals(ExitCode.software.code)); + verify( + () => progress.fail('Failed to link AOT files: $exception'), + ).called(1); + }); + test('does not create patch on --dry-run', () async { when(() => argResults['dry-run']).thenReturn(true); setUpProjectRoot(); diff --git a/packages/shorebird_cli/test/src/mocks.dart b/packages/shorebird_cli/test/src/mocks.dart index cd0264d1..26c38bfb 100644 --- a/packages/shorebird_cli/test/src/mocks.dart +++ b/packages/shorebird_cli/test/src/mocks.dart @@ -38,6 +38,8 @@ class MockAndroidSdk extends Mock implements AndroidSdk {} class MockAndroidStudio extends Mock implements AndroidStudio {} +class MockAotTools extends Mock implements AotTools {} + class MockAppMetadata extends Mock implements AppMetadata {} class MockAppleDevice extends Mock implements AppleDevice {} diff --git a/packages/shorebird_cli/test/src/shorebird_env_test.dart b/packages/shorebird_cli/test/src/shorebird_env_test.dart index 4b662bb9..126ca663 100644 --- a/packages/shorebird_cli/test/src/shorebird_env_test.dart +++ b/packages/shorebird_cli/test/src/shorebird_env_test.dart @@ -184,6 +184,29 @@ void main() { }); }); + group('analyzeSnapshotFile', () { + test('returns correct path', () { + expect( + runWithOverrides(() => shorebirdEnv.analyzeSnapshotFile.path), + equals( + p.join( + shorebirdRoot.path, + 'bin', + 'cache', + 'flutter', + flutterRevision, + 'bin', + 'cache', + 'artifacts', + 'engine', + 'ios-release', + 'analyze_snapshot_arm64', + ), + ), + ); + }); + }); + group('getPubspecYamlFile', () { test('returns correct file', () { final tempDir = Directory.systemTemp.createTempSync();