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 b3698d2b..a011d31c 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,7 +3,6 @@ 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'; @@ -121,16 +120,15 @@ class PatchIosCommand extends ShorebirdCommand final detectReleaseVersionProgress = logger.progress( 'Detecting release version', ); + final String ipaPath; try { - final ipa = _ipaReader.read( - p.join( - Directory.current.path, - 'build', - 'ios', - 'ipa', - '${getIpaName()}.ipa', - ), - ); + ipaPath = getIpaPath(); + } catch (error) { + detectReleaseVersionProgress.fail('Could not find ipa file: $error'); + return ExitCode.software.code; + } + try { + final ipa = _ipaReader.read(ipaPath); releaseVersion = ipa.versionNumber; detectReleaseVersionProgress.complete( 'Detected release version $releaseVersion', diff --git a/packages/shorebird_cli/lib/src/commands/release/release_ios_command.dart b/packages/shorebird_cli/lib/src/commands/release/release_ios_command.dart index 6c09315c..86f2027e 100644 --- a/packages/shorebird_cli/lib/src/commands/release/release_ios_command.dart +++ b/packages/shorebird_cli/lib/src/commands/release/release_ios_command.dart @@ -95,11 +95,14 @@ make smaller updates to your app. final releaseVersionProgress = logger.progress('Getting release version'); final iosBuildDir = p.join(Directory.current.path, 'build', 'ios'); - final ipaPath = p.join( - iosBuildDir, - 'ipa', - '${getIpaName()}.ipa', - ); + final String ipaPath; + try { + ipaPath = getIpaPath(); + } catch (error) { + releaseVersionProgress.fail('Could not find ipa file: $error'); + return ExitCode.software.code; + } + final runnerPath = p.join( iosBuildDir, 'archive', diff --git a/packages/shorebird_cli/lib/src/shorebird_artifact_mixin.dart b/packages/shorebird_cli/lib/src/shorebird_artifact_mixin.dart index 9daf0478..a0210b26 100644 --- a/packages/shorebird_cli/lib/src/shorebird_artifact_mixin.dart +++ b/packages/shorebird_cli/lib/src/shorebird_artifact_mixin.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:path/path.dart' as p; -import 'package:propertylistserialization/propertylistserialization.dart'; import 'package:shorebird_cli/src/command.dart'; import 'package:shorebird_cli/src/logger.dart'; @@ -62,21 +61,39 @@ mixin ShorebirdArtifactMixin on ShorebirdCommand { return extractedZipDir; } - /// Finds CFBundleName in the app's Info.plist. The ipa generated by `flutter - /// build ipa` will have this name and an .ipa extension. - String getIpaName() { - final infoPlist = PropertyListSerialization.propertyListWithString( - File( - p.join( - Directory.current.path, - 'ios', - 'Runner', - 'Info.plist', - ), - ).readAsStringSync(), - ) as Map; + /// Returns the path to the .ipa file generated by `flutter build ipa`. Throws + /// an exception if there is not exactly one .ipa file in the build directory, + /// or if there is no build directory. + String getIpaPath() { + final ipaBuildDirectory = Directory( + p.join( + Directory.current.path, + 'build', + 'ios', + 'ipa', + ), + ); - return infoPlist['CFBundleName']! as String; + if (!ipaBuildDirectory.existsSync()) { + throw Exception('No directory found at ${ipaBuildDirectory.path}'); + } + + final ipaFiles = ipaBuildDirectory + .listSync(recursive: true) + .whereType() + .where((f) => p.extension(f.path) == '.ipa'); + + if (ipaFiles.isEmpty) { + throw Exception('No .ipa files found in ${ipaBuildDirectory.path}'); + } + + if (ipaFiles.length > 1) { + throw Exception( + 'More than one .ipa file found in ${ipaBuildDirectory.path}', + ); + } + + return ipaFiles.single.path; } /// Finds the most recently-edited app.dill file in the .dart_tool directory. 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 6fdfb47c..00af646e 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 @@ -67,6 +67,7 @@ void main() { const appDisplayName = 'Test App'; const platformName = 'ios'; const elfAotSnapshotFileName = 'out.aot'; + const ipaPath = 'build/ios/ipa/Runner.ipa'; const infoPlistContent = ''' @@ -143,6 +144,7 @@ flutter: ) ..createSync(recursive: true) ..writeAsStringSync(infoPlistContent); + File(p.join(tempDir.path, ipaPath)).createSync(recursive: true); return tempDir; } @@ -316,6 +318,78 @@ flutter: expect(exitCode, equals(ExitCode.software.code)); }); + test('exits with code 70 if build directory does not exist', () async { + final tempDir = setUpTempDir(); + setUpTempArtifacts(tempDir); + Directory(p.join(tempDir.path, 'build')).deleteSync(recursive: true); + + final exitCode = await IOOverrides.runZoned( + () => runWithOverrides(command.run), + getCurrentDirectory: () => tempDir, + ); + + expect(exitCode, equals(ExitCode.software.code)); + verify( + () => progress.fail( + any( + that: stringContainsInOrder([ + 'Could not find ipa file', + 'No directory found at ${p.join(tempDir.path, 'build')}', + ]), + ), + ), + ).called(1); + }); + + test('exits with code 70 if ipa file does not exist', () async { + final tempDir = setUpTempDir(); + setUpTempArtifacts(tempDir); + File(p.join(tempDir.path, ipaPath)).deleteSync(recursive: true); + + final exitCode = await IOOverrides.runZoned( + () => runWithOverrides(command.run), + getCurrentDirectory: () => tempDir, + ); + + expect(exitCode, equals(ExitCode.software.code)); + verify( + () => progress.fail( + any( + that: stringContainsInOrder([ + 'Could not find ipa file', + 'No .ipa files found in', + 'build/ios/ipa', + ]), + ), + ), + ).called(1); + }); + + test('exits with code 70 if more than one ipa file is found', () async { + final tempDir = setUpTempDir(); + setUpTempArtifacts(tempDir); + File(p.join(tempDir.path, 'build/ios/ipa/Runner2.ipa')) + .createSync(recursive: true); + + final exitCode = await IOOverrides.runZoned( + () => runWithOverrides(command.run), + getCurrentDirectory: () => tempDir, + ); + + expect(exitCode, equals(ExitCode.software.code)); + verify( + () => progress.fail( + any( + that: stringContainsInOrder([ + 'Could not find ipa file', + 'More than one .ipa file found in', + 'build/ios/ipa', + ]), + ), + ), + ).called(1); + }); + test( 'exits with usage code when ' 'both --dry-run and --force are specified', () async { diff --git a/packages/shorebird_cli/test/src/commands/release/release_ios_command_test.dart b/packages/shorebird_cli/test/src/commands/release/release_ios_command_test.dart index 877e874b..94c265ad 100644 --- a/packages/shorebird_cli/test/src/commands/release/release_ios_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/release/release_ios_command_test.dart @@ -62,6 +62,7 @@ void main() { const appDisplayName = 'Test App'; const arch = 'armv7'; const releasePlatform = ReleasePlatform.ios; + const ipaPath = 'build/ios/ipa/Runner.ipa'; const appMetadata = AppMetadata(appId: appId, displayName: appDisplayName); const release = Release( id: 0, @@ -135,6 +136,7 @@ flutter: ) ..createSync(recursive: true) ..writeAsStringSync(infoPlistContent); + File(p.join(tempDir.path, ipaPath)).createSync(recursive: true); return tempDir; } @@ -383,6 +385,75 @@ error: exportArchive: No signing certificate "iOS Distribution" found ); }); + test('exits with code 70 if build directory does not exist', () async { + final tempDir = setUpTempDir(); + Directory(p.join(tempDir.path, 'build')).deleteSync(recursive: true); + + final exitCode = await IOOverrides.runZoned( + () => runWithOverrides(command.run), + getCurrentDirectory: () => tempDir, + ); + + expect(exitCode, equals(ExitCode.software.code)); + verify( + () => progress.fail( + any( + that: stringContainsInOrder([ + 'Could not find ipa file', + 'No directory found at ${p.join(tempDir.path, 'build')}', + ]), + ), + ), + ).called(1); + }); + + test('exits with code 70 if ipa file does not exist', () async { + final tempDir = setUpTempDir(); + File(p.join(tempDir.path, ipaPath)).deleteSync(recursive: true); + + final exitCode = await IOOverrides.runZoned( + () => runWithOverrides(command.run), + getCurrentDirectory: () => tempDir, + ); + + expect(exitCode, equals(ExitCode.software.code)); + verify( + () => progress.fail( + any( + that: stringContainsInOrder([ + 'Could not find ipa file', + 'No .ipa files found in', + 'build/ios/ipa', + ]), + ), + ), + ).called(1); + }); + + test('exits with code 70 if more than one ipa file is found', () async { + final tempDir = setUpTempDir(); + File(p.join(tempDir.path, 'build/ios/ipa/Runner2.ipa')) + .createSync(recursive: true); + + final exitCode = await IOOverrides.runZoned( + () => runWithOverrides(command.run), + getCurrentDirectory: () => tempDir, + ); + + expect(exitCode, equals(ExitCode.software.code)); + verify( + () => progress.fail( + any( + that: stringContainsInOrder([ + 'Could not find ipa file', + 'More than one .ipa file found in', + 'build/ios/ipa', + ]), + ), + ), + ).called(1); + }); + test('throws error when unable to detect flutter revision', () async { const error = 'oops'; when(() => flutterRevisionProcessResult.exitCode).thenReturn(1); @@ -443,7 +514,7 @@ error: exportArchive: No signing certificate "iOS Distribution" found that: stringContainsInOrder( [ 'Your next step is to upload the ipa to App Store Connect.', - 'build/ios/ipa/app_bundle_name.ipa', + 'build/ios/ipa/Runner.ipa', ], ), ), @@ -495,7 +566,7 @@ flavors: that: stringContainsInOrder( [ 'Your next step is to upload the ipa to App Store Connect.', - 'build/ios/ipa/app_bundle_name.ipa', + 'build/ios/ipa/Runner.ipa', ], ), ),