diff --git a/packages/shorebird_cli/lib/src/commands/patch/patch_android_command.dart b/packages/shorebird_cli/lib/src/commands/patch/patch_android_command.dart index 3150aba8..561e2be8 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/patch_android_command.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/patch_android_command.dart @@ -17,6 +17,7 @@ import 'package:shorebird_cli/src/logger.dart'; import 'package:shorebird_cli/src/patch_diff_checker.dart'; import 'package:shorebird_cli/src/shorebird_build_mixin.dart'; import 'package:shorebird_cli/src/shorebird_env.dart'; +import 'package:shorebird_cli/src/shorebird_flutter.dart'; import 'package:shorebird_cli/src/shorebird_release_version_mixin.dart'; import 'package:shorebird_cli/src/shorebird_validator.dart'; import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; @@ -103,6 +104,7 @@ class PatchAndroidCommand extends ShorebirdCommand final appId = shorebirdYaml.getAppId(flavor: flavor); final app = await codePushClientWrapper.getApp(appId: appId); + final originalFlutterRevision = shorebirdEnv.flutterRevision; final buildProgress = logger.progress('Building patch'); try { await buildAppBundle(flavor: flavor, target: target); @@ -144,26 +146,33 @@ Please re-run the release command for this version or create a new release.'''); return ExitCode.software.code; } - final shorebirdFlutterRevision = shorebirdEnv.flutterRevision; - if (release.flutterRevision != shorebirdFlutterRevision) { - logger - ..err(''' -Flutter revision mismatch. - + if (release.flutterRevision != originalFlutterRevision) { + logger.info(''' The release you are trying to patch was built with a different version of Flutter. Release Flutter Revision: ${release.flutterRevision} -Current Flutter Revision: $shorebirdFlutterRevision -''') - ..info( - ''' -Either create a new release using: - ${lightCyan.wrap('shorebird release android')} +Current Flutter Revision: $originalFlutterRevision'''); -Or change your Flutter version and try again using: - ${lightCyan.wrap('shorebird flutter versions use ${release.flutterRevision}')}''', + var flutterVersionProgress = logger.progress( + 'Switching to Flutter revision ${release.flutterRevision}', + ); + await shorebirdFlutter.useRevision(revision: release.flutterRevision); + flutterVersionProgress.complete(); + + final buildProgress = logger.progress('Building patch'); + try { + await buildAppBundle(flavor: flavor, target: target); + buildProgress.complete(); + } on ProcessException catch (error) { + buildProgress.fail('Failed to build: ${error.message}'); + return ExitCode.software.code; + } finally { + flutterVersionProgress = logger.progress( + 'Reverting to Flutter revision $originalFlutterRevision', ); - return ExitCode.software.code; + await shorebirdFlutter.useRevision(revision: originalFlutterRevision); + flutterVersionProgress.complete(); + } } final releaseArtifacts = await codePushClientWrapper.getReleaseArtifacts( 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 217f1f57..6323c453 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 @@ -3,6 +3,7 @@ import 'dart:io' hide Platform; import 'package:crypto/crypto.dart'; import 'package:mason_logger/mason_logger.dart'; +import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; @@ -16,6 +17,7 @@ import 'package:shorebird_cli/src/patch_diff_checker.dart'; import 'package:shorebird_cli/src/shorebird_artifact_mixin.dart'; import 'package:shorebird_cli/src/shorebird_build_mixin.dart'; import 'package:shorebird_cli/src/shorebird_env.dart'; +import 'package:shorebird_cli/src/shorebird_flutter.dart'; import 'package:shorebird_cli/src/shorebird_validator.dart'; import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; @@ -93,36 +95,18 @@ class PatchIosCommand extends ShorebirdCommand const arch = 'aarch64'; const channelName = 'stable'; const releasePlatform = ReleasePlatform.ios; - final target = results['target'] as String?; final flavor = results['flavor'] as String?; final shorebirdYaml = shorebirdEnv.getShorebirdYaml()!; final appId = shorebirdYaml.getAppId(flavor: flavor); final app = await codePushClientWrapper.getApp(appId: appId); - final buildProgress = logger.progress('Building release'); try { - await buildIpa(flavor: flavor, target: target); - } on ProcessException catch (error) { - buildProgress.fail('Failed to build: ${error.message}'); - return ExitCode.software.code; - } on BuildException catch (error) { - buildProgress.fail('Failed to build IPA'); - logger.err(error.message); + await _buildPatch(); + } catch (_) { return ExitCode.software.code; } - final File aotFile; - try { - final newestDillFile = newestAppDill(); - aotFile = await buildElfAotSnapshot(appDillPath: newestDillFile.path); - } catch (error) { - buildProgress.fail('$error'); - return ExitCode.software.code; - } - - buildProgress.complete(); - final detectReleaseVersionProgress = logger.progress( 'Detecting release version', ); @@ -159,26 +143,31 @@ Please re-run the release command for this version or create a new release.'''); return ExitCode.software.code; } - final shorebirdFlutterRevision = shorebirdEnv.flutterRevision; - if (release.flutterRevision != shorebirdFlutterRevision) { - logger - ..err(''' -Flutter revision mismatch. - + final originalFlutterRevision = shorebirdEnv.flutterRevision; + if (release.flutterRevision != originalFlutterRevision) { + logger.info(''' The release you are trying to patch was built with a different version of Flutter. Release Flutter Revision: ${release.flutterRevision} -Current Flutter Revision: $shorebirdFlutterRevision -''') - ..info( - ''' -Either create a new release using: - ${lightCyan.wrap('shorebird release ios-alpha')} +Current Flutter Revision: $originalFlutterRevision'''); -Or change your Flutter version and try again using: - ${lightCyan.wrap('shorebird flutter versions use ${release.flutterRevision}')}''', + var flutterVersionProgress = logger.progress( + 'Switching to Flutter revision ${release.flutterRevision}', + ); + await shorebirdFlutter.useRevision(revision: release.flutterRevision); + flutterVersionProgress.complete(); + + try { + await _buildPatch(); + } catch (_) { + return ExitCode.software.code; + } finally { + flutterVersionProgress = logger.progress( + 'Switching back to original Flutter revision $originalFlutterRevision', ); - return ExitCode.software.code; + await shorebirdFlutter.useRevision(revision: originalFlutterRevision); + flutterVersionProgress.complete(); + } } final releaseArtifact = await codePushClientWrapper.getReleaseArtifact( @@ -209,6 +198,7 @@ Or change your Flutter version and try again using: return ExitCode.success.code; } + final aotFile = File(_aotOutputPath); final aotFileSize = aotFile.statSync().size; final summary = [ @@ -257,4 +247,36 @@ ${summary.join('\n')} return ExitCode.success.code; } + + String get _aotOutputPath => + p.join(Directory.current.path, 'build', 'out.aot'); + + Future _buildPatch() async { + final target = results['target'] as String?; + final flavor = results['flavor'] as String?; + final buildProgress = logger.progress('Building patch'); + try { + await buildIpa(flavor: flavor, target: target); + } on ProcessException catch (error) { + buildProgress.fail('Failed to build: ${error.message}'); + rethrow; + } on BuildException catch (error) { + buildProgress.fail('Failed to build IPA'); + logger.err(error.message); + rethrow; + } + + try { + final newestDillFile = newestAppDill(); + await buildElfAotSnapshot( + appDillPath: newestDillFile.path, + outFilePath: _aotOutputPath, + ); + } catch (error) { + buildProgress.fail('$error'); + rethrow; + } + + buildProgress.complete(); + } } diff --git a/packages/shorebird_cli/lib/src/commands/patch/patch_ios_framework_command.dart b/packages/shorebird_cli/lib/src/commands/patch/patch_ios_framework_command.dart index 2dfe384c..b9c74b75 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/patch_ios_framework_command.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/patch_ios_framework_command.dart @@ -157,7 +157,10 @@ Please re-run the release command for this version or create a new release.'''); final File aotFile; try { final newestDillFile = newestAppDill(); - aotFile = await buildElfAotSnapshot(appDillPath: newestDillFile.path); + aotFile = await buildElfAotSnapshot( + appDillPath: newestDillFile.path, + outFilePath: p.join(Directory.current.path, 'build', 'out.aot'), + ); } catch (error) { buildProgress.fail('$error'); return ExitCode.software.code; diff --git a/packages/shorebird_cli/lib/src/shorebird_build_mixin.dart b/packages/shorebird_cli/lib/src/shorebird_build_mixin.dart index d5ea833c..8d3d6577 100644 --- a/packages/shorebird_cli/lib/src/shorebird_build_mixin.dart +++ b/packages/shorebird_cli/lib/src/shorebird_build_mixin.dart @@ -374,11 +374,13 @@ Either run `flutter pub get` manually, or follow the steps in ${link(uri: Uri.pa return diffPath; } - /// Creates an AOT snapshot of the given [appDillPath] and returns the - /// resulting snapshot file. + /// Creates an AOT snapshot of the given [appDillPath] at [outFilePath] and + /// returns the resulting file. // TODO(bryanoltman): make this work with the --local-engine flag. - Future buildElfAotSnapshot({required String appDillPath}) async { - final outFilePath = p.join(Directory.current.path, 'build', 'out.aot'); + Future buildElfAotSnapshot({ + required String appDillPath, + required String outFilePath, + }) async { final arguments = [ '--deterministic', '--snapshot-kind=app-aot-elf', diff --git a/packages/shorebird_cli/test/src/commands/patch/patch_android_command_test.dart b/packages/shorebird_cli/test/src/commands/patch/patch_android_command_test.dart index 16284791..305ec569 100644 --- a/packages/shorebird_cli/test/src/commands/patch/patch_android_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/patch/patch_android_command_test.dart @@ -61,11 +61,11 @@ class _MockHttpClient extends Mock implements http.Client {} class _MockShorebirdEnv extends Mock implements ShorebirdEnv {} +class _MockShorebirdFlutter extends Mock implements ShorebirdFlutter {} + class _MockShorebirdFlutterValidator extends Mock implements ShorebirdFlutterValidator {} -class _MockShorebirdFlutter extends Mock implements ShorebirdFlutter {} - class _MockShorebirdProcess extends Mock implements ShorebirdProcess {} class _MockShorebirdValidator extends Mock implements ShorebirdValidator {} @@ -244,6 +244,8 @@ flutter: when(() => shorebirdEnv.shorebirdRoot).thenReturn(shorebirdRoot); when(() => shorebirdEnv.flutterDirectory).thenReturn(flutterDirectory); when(() => shorebirdEnv.flutterRevision).thenReturn(flutterRevision); + when(() => shorebirdFlutter.useRevision(revision: any(named: 'revision'))) + .thenAnswer((_) async {}); when( () => shorebirdProcess.run( 'flutter', @@ -499,8 +501,8 @@ Please re-run the release command for this version or create a new release.'''), }); test( - 'errors when shorebird flutter revision ' - 'does not match release revision', () async { + '''switches to release flutter revision when shorebird flutter revision does not match''', + () async { const otherRevision = 'other-revision'; when(() => shorebirdEnv.flutterRevision).thenReturn(otherRevision); final tempDir = setUpTempDir(); @@ -511,17 +513,49 @@ Please re-run the release command for this version or create a new release.'''), getCurrentDirectory: () => tempDir, ); - expect(exitCode, ExitCode.software.code); + expect(exitCode, ExitCode.success.code); + // Verify that we switch back to the original revision once we're done. + verifyInOrder([ + () => shorebirdFlutter.useRevision(revision: release.flutterRevision), + () => shorebirdFlutter.useRevision(revision: otherRevision), + ]); verify( - () => logger.info(''' -Either create a new release using: - ${lightCyan.wrap('shorebird release android')} - -Or change your Flutter version and try again using: - ${lightCyan.wrap('shorebird flutter versions use ${release.flutterRevision}')}'''), + () => logger.info( + any( + that: stringContainsInOrder([ + '''The release you are trying to patch was built with a different version of Flutter.''', + 'Release Flutter Revision: ${release.flutterRevision}', + 'Current Flutter Revision: $otherRevision', + ]), + ), + ), ).called(1); }); + test( + 'exits with code 70 if build fails after switching flutter versions', + () async { + const otherRevision = 'other-revision'; + when(() => shorebirdEnv.flutterRevision).thenReturn(otherRevision); + when( + () => shorebirdFlutter.useRevision(revision: any(named: 'revision')), + ).thenAnswer((invocation) async { + // Cause builds to fail after switching flutter versions. + when(() => flutterBuildProcessResult.exitCode).thenReturn(1); + when(() => flutterBuildProcessResult.stderr).thenReturn('oops'); + }); + final tempDir = setUpTempDir(); + setUpTempArtifacts(tempDir); + + final exitCode = await IOOverrides.runZoned( + () => runWithOverrides(command.run), + getCurrentDirectory: () => tempDir, + ); + + expect(exitCode, equals(ExitCode.software.code)); + }, + ); + test('errors when detecting release version name fails', () async { final exception = Exception( 'Failed to extract version name from app bundle: oops', 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 5add6a6d..d6c5d8a8 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 @@ -292,6 +292,8 @@ flutter: when(() => shorebirdEnv.genSnapshotFile).thenReturn(genSnapshotFile); when(() => shorebirdEnv.flutterRevision).thenReturn(flutterRevision); when(() => shorebirdEnv.isRunningOnCI).thenReturn(false); + when(() => shorebirdFlutter.useRevision(revision: any(named: 'revision'))) + .thenAnswer((_) async {}); when( () => aotBuildProcessResult.exitCode, ).thenReturn(ExitCode.success.code); @@ -568,8 +570,8 @@ Please re-run the release command for this version or create a new release.'''), }); test( - 'errors when shorebird flutter revision ' - 'does not match release revision', () async { + '''switches to release flutter revision when shorebird flutter revision does not match''', + () async { const otherRevision = 'other-revision'; when(() => shorebirdEnv.flutterRevision).thenReturn(otherRevision); final tempDir = setUpTempDir(); @@ -580,17 +582,50 @@ Please re-run the release command for this version or create a new release.'''), getCurrentDirectory: () => tempDir, ); - expect(exitCode, ExitCode.software.code); - verify( - () => logger.info(''' -Either create a new release using: - ${lightCyan.wrap('shorebird release ios-alpha')} + expect(exitCode, ExitCode.success.code); + // Verify that we switch back to the original revision once we're done. + verifyInOrder([ + () => shorebirdFlutter.useRevision(revision: release.flutterRevision), + () => shorebirdFlutter.useRevision(revision: otherRevision), + ]); -Or change your Flutter version and try again using: - ${lightCyan.wrap('shorebird flutter versions use ${release.flutterRevision}')}'''), + verify( + () => logger.info( + any( + that: stringContainsInOrder([ + '''The release you are trying to patch was built with a different version of Flutter.''', + 'Release Flutter Revision: ${release.flutterRevision}', + 'Current Flutter Revision: $otherRevision', + ]), + ), + ), ).called(1); }); + test( + 'exits with code 70 if build fails after switching flutter versions', + () async { + const otherRevision = 'other-revision'; + when(() => shorebirdEnv.flutterRevision).thenReturn(otherRevision); + when( + () => shorebirdFlutter.useRevision(revision: any(named: 'revision')), + ).thenAnswer((invocation) async { + // Cause builds to fail after switching flutter versions. + when(() => flutterBuildProcessResult.exitCode).thenReturn(1); + when(() => flutterBuildProcessResult.stderr).thenReturn('oops'); + }); + final tempDir = setUpTempDir(); + setUpTempArtifacts(tempDir); + + final exitCode = await IOOverrides.runZoned( + () => runWithOverrides(command.run), + getCurrentDirectory: () => tempDir, + ); + + expect(exitCode, equals(ExitCode.software.code)); + }, + ); + test('exits with code 70 when release version cannot be determiend', () async { when(() => ipa.versionNumber).thenThrow(Exception('oops'));