diff --git a/packages/shorebird_cli/lib/src/archive/archive.dart b/packages/shorebird_cli/lib/src/archive/archive.dart new file mode 100644 index 000000000..17e222e67 --- /dev/null +++ b/packages/shorebird_cli/lib/src/archive/archive.dart @@ -0,0 +1 @@ +export 'directory_archive.dart'; diff --git a/packages/shorebird_cli/lib/src/archive/directory_archive.dart b/packages/shorebird_cli/lib/src/archive/directory_archive.dart new file mode 100644 index 000000000..b9d610df6 --- /dev/null +++ b/packages/shorebird_cli/lib/src/archive/directory_archive.dart @@ -0,0 +1,19 @@ +import 'dart:isolate'; + +import 'package:archive/archive_io.dart'; +import 'package:io/io.dart'; +import 'package:path/path.dart' as p; +import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart'; + +extension DirectoryArchive on Directory { + /// Copies this directory to a temporary directory and zips it. + Future zipToTempFile() async { + final tempDir = await Directory.systemTemp.createTemp(); + final outFile = File(p.join(tempDir.path, '${p.basename(path)}.zip')); + await Isolate.run(() { + copyPathSync(path, tempDir.path); + ZipFileEncoder().zipDirectory(tempDir, filename: outFile.path); + }); + return outFile; + } +} diff --git a/packages/shorebird_cli/lib/src/archive_analysis/archive_analysis.dart b/packages/shorebird_cli/lib/src/archive_analysis/archive_analysis.dart index b0384eb38..b26b7599d 100644 --- a/packages/shorebird_cli/lib/src/archive_analysis/archive_analysis.dart +++ b/packages/shorebird_cli/lib/src/archive_analysis/archive_analysis.dart @@ -1,4 +1,4 @@ export 'android_archive_differ.dart'; export 'file_set_diff.dart'; export 'ios_archive_differ.dart'; -export 'ipa.dart'; +export 'plist.dart'; diff --git a/packages/shorebird_cli/lib/src/archive_analysis/ios_archive_differ.dart b/packages/shorebird_cli/lib/src/archive_analysis/ios_archive_differ.dart index 0fae0772e..ec4e310b3 100644 --- a/packages/shorebird_cli/lib/src/archive_analysis/ios_archive_differ.dart +++ b/packages/shorebird_cli/lib/src/archive_analysis/ios_archive_differ.dart @@ -20,11 +20,13 @@ import 'package:shorebird_cli/src/archive_analysis/file_set_diff.dart'; class IosArchiveDiffer extends ArchiveDiffer { String _hash(List bytes) => sha256.convert(bytes).toString(); - static const binaryFiles = { - 'App.framework/App', - 'Flutter.framework/Flutter', + static final binaryFilePatterns = { + RegExp(r'App.framework/App$'), + RegExp(r'Flutter.framework/Flutter$'), }; - static RegExp appRegex = RegExp(r'^Payload/[\w\-. ]+.app/[\w\- ]+$'); + static RegExp appRegex = RegExp( + r'^Products/Applications/[\w\-. ]+.app/[\w\- ]+$', + ); /// Files that have been added, removed, or that have changed between the /// archives at the two provided paths. This method will also unisgn mach-o @@ -73,8 +75,8 @@ class IosArchiveDiffer extends ArchiveDiffer { .where((file) => file.isFile) .where( (file) => - file.name.endsWith('App.framework/App') || - file.name.endsWith('Flutter.framework/Flutter') || + binaryFilePatterns + .any((pattern) => pattern.hasMatch(file.name)) || appRegex.hasMatch(file.name), ) .toList(); @@ -84,7 +86,7 @@ class IosArchiveDiffer extends ArchiveDiffer { return ZipDecoder() .decodeBuffer(InputFileStream(archivePath)) .files - .where((file) => file.isFile && p.extension(file.name) == '.car') + .where((file) => file.isFile && p.basename(file.name) == 'Assets.car') .toList(); } @@ -102,8 +104,7 @@ class IosArchiveDiffer extends ArchiveDiffer { } final outFile = File(outPath); - final hash = _hash(outFile.readAsBytesSync()); - return hash; + return _hash(outFile.readAsBytesSync()); } /// Uses assetutil to write a json description of a .car file to disk and @@ -148,7 +149,7 @@ class IosArchiveDiffer extends ArchiveDiffer { /// The flutter_assets directory contains the assets listed in the assets /// section of the pubspec.yaml file. /// Assets.car is the compiled asset catalog(s) (.xcassets files). - return p.extension(filePath) == '.car' || + return p.basename(filePath) == 'Assets.car' || p.split(filePath).contains('flutter_assets'); } diff --git a/packages/shorebird_cli/lib/src/archive_analysis/ipa.dart b/packages/shorebird_cli/lib/src/archive_analysis/ipa.dart deleted file mode 100644 index 4f531b13e..000000000 --- a/packages/shorebird_cli/lib/src/archive_analysis/ipa.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:archive/archive_io.dart'; -import 'package:propertylistserialization/propertylistserialization.dart'; - -/// {@template ipa_reader} -/// Wraps the [Ipa] class to make it easier to test. -/// {@endtemplate} -class IpaReader { - /// {@macro ipa_reader} - Ipa read(String path) => Ipa(path: path); -} - -/// {@template ipa} -/// A class that represents an iOS IPA file. -/// {@endtemplate} -class Ipa { - /// {@macro ipa} - Ipa({ - required String path, - }) : _ipaFile = File(path); - - /// This key is a user-visible string for the version of the bundle. The - /// required format is three period-separated integers, such as 10.14.1. The - /// string can only contain numeric characters (0-9) and periods. - /// - /// See https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring - static const releaseVersionKey = 'CFBundleShortVersionString'; - - /// The version of the build that identifies an iteration of the bundle. - /// - /// See https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleversion - static const buildNumberKey = 'CFBundleVersion'; - - final File _ipaFile; - - /// The version number of the IPA, as derived from the app's Info.plist. - String get versionNumber { - final plist = _getPlist(); - if (plist == null) { - throw Exception('Could not find Info.plist'); - } - - final releaseVersion = plist[releaseVersionKey] as String?; - final buildNumber = plist[buildNumberKey] as String?; - if (releaseVersion == null) { - throw Exception('Could not determine release version'); - } - - return buildNumber == null - ? releaseVersion - : '$releaseVersion+$buildNumber'; - } - - Map? _getPlist() { - final plistPathRegex = RegExp(r'Payload/[\w\-. ]+.app/Info.plist'); - final plistFile = ZipDecoder() - .decodeBuffer(InputFileStream(_ipaFile.path)) - .files - .where((file) { - return file.isFile && plistPathRegex.hasMatch(file.name); - }).firstOrNull; - if (plistFile == null) { - return null; - } - - final content = plistFile.content as Uint8List; - - return PropertyListSerialization.propertyListWithData( - ByteData.view(content.buffer), - ) as Map; - } -} diff --git a/packages/shorebird_cli/lib/src/archive_analysis/plist.dart b/packages/shorebird_cli/lib/src/archive_analysis/plist.dart new file mode 100644 index 000000000..884f82cfb --- /dev/null +++ b/packages/shorebird_cli/lib/src/archive_analysis/plist.dart @@ -0,0 +1,47 @@ +import 'dart:io'; + +import 'package:propertylistserialization/propertylistserialization.dart'; + +class Plist { + Plist({required File file}) { + properties = PropertyListSerialization.propertyListWithString( + file.readAsStringSync(), + ) as Map; + } + + /// This key is a user-visible string for the version of the bundle. The + /// required format is three period-separated integers, such as 10.14.1. The + /// string can only contain numeric characters (0-9) and periods. + /// + /// See https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring + static const releaseVersionKey = 'CFBundleShortVersionString'; + + /// The version of the build that identifies an iteration of the bundle. + /// + /// See https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleversion + static const buildNumberKey = 'CFBundleVersion'; + + /// The Info.plist contained in .xcarchives has the following structure: + /// { + /// ApplicationProperties: { + /// CFBundleShortVersionString: "1.0.0", + /// CFBundleVersion: "1", + /// }, + static const applicationPropertiesKey = 'ApplicationProperties'; + + late final Map properties; + + String get versionNumber { + final applicationProperties = + properties[applicationPropertiesKey]! as Map; + final releaseVersion = applicationProperties[releaseVersionKey] as String?; + final buildNumber = applicationProperties[buildNumberKey] as String?; + if (releaseVersion == null) { + throw Exception('Could not determine release version'); + } + + return buildNumber == null + ? releaseVersion + : '$releaseVersion+$buildNumber'; + } +} diff --git a/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart b/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart index 0a9cdf1be..5a877cc53 100644 --- a/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart +++ b/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart @@ -3,10 +3,12 @@ import 'dart:isolate'; import 'package:archive/archive_io.dart'; import 'package:collection/collection.dart'; import 'package:crypto/crypto.dart'; +import 'package:io/io.dart' as io; import 'package:mason_logger/mason_logger.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:scoped/scoped.dart'; +import 'package:shorebird_cli/src/archive/directory_archive.dart'; import 'package:shorebird_cli/src/auth/auth.dart'; import 'package:shorebird_cli/src/logger.dart'; import 'package:shorebird_cli/src/shorebird_build_mixin.dart'; @@ -503,35 +505,51 @@ aar artifact already exists, continuing...''', createArtifactProgress.complete(); } - /// Uploads a release ipa to the Shorebird server. + /// Removes all .dylib files from the given .xcarchive to reduce the size of + /// the uploaded artifact. + Future _thinXcarchive({required String xcarchivePath}) async { + final xcarchiveDirectoryName = p.basename(xcarchivePath); + final tempDir = Directory.systemTemp.createTempSync(); + final thinnedArchiveDirectory = + Directory(p.join(tempDir.path, xcarchiveDirectoryName)); + await io.copyPath(xcarchivePath, thinnedArchiveDirectory.path); + thinnedArchiveDirectory + .listSync(recursive: true) + .whereType() + .where((file) => p.extension(file.path) == '.dylib') + .forEach((file) => file.deleteSync()); + return thinnedArchiveDirectory; + } + + /// Uploads a release .xcarchive and .app to the Shorebird server. Future createIosReleaseArtifacts({ required String appId, required int releaseId, - required String ipaPath, + required String xcarchivePath, required String runnerPath, }) async { final createArtifactProgress = logger.progress('Creating artifacts'); - final ipaFile = File(ipaPath); + final thinnedArchiveDirectory = + await _thinXcarchive(xcarchivePath: xcarchivePath); + final zippedArchive = await thinnedArchiveDirectory.zipToTempFile(); try { await codePushClient.createReleaseArtifact( appId: appId, releaseId: releaseId, - artifactPath: ipaPath, - arch: 'ipa', + artifactPath: zippedArchive.path, + arch: 'xcarchive', platform: ReleasePlatform.ios, - hash: sha256.convert(await ipaFile.readAsBytes()).toString(), + hash: sha256.convert(await zippedArchive.readAsBytes()).toString(), ); } catch (error) { _handleErrorAndExit( error, progress: createArtifactProgress, - message: 'Error uploading ipa: $error', + message: 'Error uploading xcarchive: $error', ); } - final runnerDirectory = Directory(runnerPath); - await Isolate.run(() => ZipFileEncoder().zipDirectory(runnerDirectory)); - final zippedRunner = File('$runnerPath.zip'); + final zippedRunner = await Directory(runnerPath).zipToTempFile(); try { await codePushClient.createReleaseArtifact( appId: appId, 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 ec40eeee1..0191a1d58 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 @@ -30,10 +30,8 @@ class PatchIosCommand extends ShorebirdCommand PatchIosCommand({ HashFunction? hashFn, IosArchiveDiffer? archiveDiffer, - IpaReader? ipaReader, }) : _hashFn = hashFn ?? ((m) => sha256.convert(m).toString()), - _archiveDiffer = archiveDiffer ?? IosArchiveDiffer(), - _ipaReader = ipaReader ?? IpaReader() { + _archiveDiffer = archiveDiffer ?? IosArchiveDiffer() { argParser ..addOption( 'target', @@ -44,6 +42,11 @@ class PatchIosCommand extends ShorebirdCommand 'flavor', help: 'The product flavor to use when building the app.', ) + ..addFlag( + 'codesign', + help: 'Codesign the application bundle.', + defaultsTo: true, + ) ..addFlag( 'force', abbr: 'f', @@ -67,7 +70,6 @@ class PatchIosCommand extends ShorebirdCommand final HashFunction _hashFn; final IosArchiveDiffer _archiveDiffer; - final IpaReader _ipaReader; @override Future run() async { @@ -107,30 +109,30 @@ class PatchIosCommand extends ShorebirdCommand return ExitCode.software.code; } - final detectReleaseVersionProgress = logger.progress( - 'Detecting release version', + final archivePath = p.join( + Directory.current.path, + 'build', + 'ios', + 'archive', + 'Runner.xcarchive', ); - final String ipaPath; - final String releaseVersion; - try { - ipaPath = getIpaPath(); - } catch (error) { - detectReleaseVersionProgress.fail('Could not find ipa file: $error'); + final plistFile = File(p.join(archivePath, 'Info.plist')); + if (!plistFile.existsSync()) { + logger.err('No Info.plist file found at ${plistFile.path}.'); return ExitCode.software.code; } + + final plist = Plist(file: plistFile); + final String releaseVersion; try { - final ipa = _ipaReader.read(ipaPath); - releaseVersion = ipa.versionNumber; - detectReleaseVersionProgress.complete( - 'Detected release version $releaseVersion', - ); + releaseVersion = plist.versionNumber; } catch (error) { - detectReleaseVersionProgress.fail( - 'Failed to determine release version: $error', - ); + logger.err('Failed to determine release version: $error'); return ExitCode.software.code; } + logger.info('Detected release version $releaseVersion'); + final release = await codePushClientWrapper.getRelease( appId: appId, releaseVersion: releaseVersion, @@ -169,7 +171,7 @@ Current Flutter Revision: $originalFlutterRevision return ExitCode.software.code; } finally { flutterVersionProgress = logger.progress( - 'Switching back to original Flutter revision $originalFlutterRevision', + '''Switching back to original Flutter revision $originalFlutterRevision''', ); await shorebirdFlutter.useRevision(revision: originalFlutterRevision); flutterVersionProgress.complete(); @@ -179,13 +181,13 @@ Current Flutter Revision: $originalFlutterRevision final releaseArtifact = await codePushClientWrapper.getReleaseArtifact( appId: appId, releaseId: release.id, - arch: 'ipa', + arch: 'xcarchive', platform: ReleasePlatform.ios, ); try { - await patchDiffChecker.confirmUnpatchableDiffsIfNecessary( - localArtifact: File(ipaPath), + await patchDiffChecker.zipAndConfirmUnpatchableDiffsIfNecessary( + localArtifactDirectory: Directory(archivePath), releaseArtifactUrl: Uri.parse(releaseArtifact.url), archiveDiffer: _archiveDiffer, force: force, @@ -224,8 +226,6 @@ ${summary.join('\n')} ''', ); - // TODO(bryanoltman): check for asset changes - final needsConfirmation = !force && !shorebirdEnv.isRunningOnCI; if (needsConfirmation) { final confirm = logger.confirm('Would you like to continue?'); @@ -260,9 +260,12 @@ ${summary.join('\n')} Future _buildPatch() async { final target = results['target'] as String?; final flavor = results['flavor'] as String?; + final shouldCodesign = results['codesign'] == true; final buildProgress = logger.progress('Building patch'); try { - await buildIpa(flavor: flavor, target: target); + // If buildIpa is called with a different codesign value than the release + // was, we will erroneously report native diffs. + await buildIpa(codesign: shouldCodesign, flavor: flavor, target: target); } on ProcessException catch (error) { buildProgress.fail('Failed to build: ${error.message}'); rethrow; 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 b9c74b75b..a21a33904 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 @@ -1,6 +1,5 @@ import 'dart:io' hide Platform; -import 'package:archive/archive_io.dart'; import 'package:collection/collection.dart'; import 'package:crypto/crypto.dart'; import 'package:mason_logger/mason_logger.dart'; @@ -168,18 +167,6 @@ Please re-run the release command for this version or create a new release.'''); buildProgress.complete(); - const zippedFrameworkFileName = - '${ShorebirdArtifactMixin.appXcframeworkName}.zip'; - final tempDir = Directory.systemTemp.createTempSync(); - final zippedFrameworkPath = p.join( - tempDir.path, - zippedFrameworkFileName, - ); - ZipFileEncoder().zipDirectory( - Directory(getAppXcframeworkPath()), - filename: zippedFrameworkPath, - ); - final releaseArtifact = await codePushClientWrapper.getReleaseArtifact( appId: appId, releaseId: release.id, @@ -188,8 +175,8 @@ Please re-run the release command for this version or create a new release.'''); ); try { - await patchDiffChecker.confirmUnpatchableDiffsIfNecessary( - localArtifact: File(zippedFrameworkPath), + await patchDiffChecker.zipAndConfirmUnpatchableDiffsIfNecessary( + localArtifactDirectory: Directory(getAppXcframeworkPath()), releaseArtifactUrl: Uri.parse(releaseArtifact.url), archiveDiffer: _archiveDiffer, force: force, 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 fa75a91fb..f7f89c785 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 @@ -23,9 +23,7 @@ import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; class ReleaseIosCommand extends ShorebirdCommand with ShorebirdBuildMixin, ShorebirdArtifactMixin { /// {@macro release_ios_command} - ReleaseIosCommand({ - IpaReader? ipaReader, - }) : _ipaReader = ipaReader ?? IpaReader() { + ReleaseIosCommand() { argParser ..addOption( 'target', @@ -36,6 +34,11 @@ class ReleaseIosCommand extends ShorebirdCommand 'flavor', help: 'The product flavor to use when building the app.', ) + ..addFlag( + 'codesign', + help: 'Codesign the application bundle.', + defaultsTo: true, + ) ..addFlag( 'force', abbr: 'f', @@ -44,8 +47,6 @@ class ReleaseIosCommand extends ShorebirdCommand ); } - final IpaReader _ipaReader; - @override String get description => ''' Builds and submits your iOS app to Shorebird. @@ -71,6 +72,17 @@ make smaller updates to your app. showiOSStatusWarning(); + final codesign = results['codesign'] == true; + if (!codesign) { + logger + ..info( + '''Building for device with codesigning disabled. You will have to manually codesign before deploying to device.''', + ) + ..warn( + '''shorebird preview will not work for releases created with "--no-codesign". However, you can still preview your app by signing the generated .xcarchive in Xcode.''', + ); + } + const releasePlatform = ReleasePlatform.ios; final flavor = results['flavor'] as String?; final target = results['target'] as String?; @@ -80,49 +92,55 @@ make smaller updates to your app. final buildProgress = logger.progress('Building release'); try { - await buildIpa(flavor: flavor, target: target); + await buildIpa(codesign: codesign, flavor: flavor, target: target); } on ProcessException catch (error) { - buildProgress.fail('Failed to build IPA: ${error.message}'); + buildProgress.fail('Failed to build: ${error.message}'); return ExitCode.software.code; } on BuildException catch (error) { - buildProgress.fail('Failed to build IPA'); + buildProgress.fail('Failed to build'); logger.err(error.message); return ExitCode.software.code; } buildProgress.complete(); - final releaseVersionProgress = logger.progress('Getting release version'); - final iosBuildDir = p.join(Directory.current.path, 'build', 'ios'); + // Ensure the ipa was built final String ipaPath; try { ipaPath = getIpaPath(); } catch (error) { - releaseVersionProgress.fail('Could not find ipa file: $error'); + logger.err('Could not find ipa file: $error'); return ExitCode.software.code; } - final runnerPath = p.join( + final iosBuildDir = p.join(Directory.current.path, 'build', 'ios'); + + final archivePath = p.join( iosBuildDir, 'archive', 'Runner.xcarchive', + ); + final runnerPath = p.join( + archivePath, 'Products', 'Applications', 'Runner.app', ); - String releaseVersion; + final plistFile = File(p.join(archivePath, 'Info.plist')); + if (!plistFile.existsSync()) { + logger.err('No Info.plist file found at ${plistFile.path}.'); + return ExitCode.software.code; + } + + final plist = Plist(file: plistFile); + final String releaseVersion; try { - final ipa = _ipaReader.read(ipaPath); - releaseVersion = ipa.versionNumber; + releaseVersion = plist.versionNumber; } catch (error) { - releaseVersionProgress.fail( - 'Failed to determine release version: $error', - ); + logger.err('Failed to determine release version: $error'); return ExitCode.software.code; } - releaseVersionProgress.complete(); - final existingRelease = await codePushClientWrapper.maybeGetRelease( appId: appId, releaseVersion: releaseVersion, @@ -177,12 +195,10 @@ ${summary.join('\n')} ); } - final relativeIpaPath = p.relative(ipaPath); - await codePushClientWrapper.createIosReleaseArtifacts( appId: app.appId, releaseId: release.id, - ipaPath: ipaPath, + xcarchivePath: archivePath, runnerPath: runnerPath, ); @@ -193,9 +209,11 @@ ${summary.join('\n')} status: ReleaseStatus.active, ); - logger - ..success('\n✅ Published Release!') - ..info(''' + logger.success('\n✅ Published Release!'); + + if (codesign) { + final relativeIpaPath = p.relative(ipaPath); + logger.info(''' Your next step is to upload the ipa to App Store Connect. ${lightCyan.wrap(relativeIpaPath)} @@ -205,6 +223,17 @@ To upload to the App Store either: 2. Run ${lightCyan.wrap('xcrun altool --upload-app --type ios -f $relativeIpaPath --apiKey your_api_key --apiIssuer your_issuer_id')}. See "man altool" for details about how to authenticate with the App Store Connect API key. '''); + } else { + logger.info(''' + +Your next step is to submit the archive at ${lightCyan.wrap(archivePath)} to the App Store using Xcode. + +You can open the archive in Xcode by running: + ${lightCyan.wrap('open $archivePath')} + +${styleBold.wrap('Make sure to uncheck "Manage Version and Build Number", or else shorebird will not work.')} +'''); + } return ExitCode.success.code; } diff --git a/packages/shorebird_cli/lib/src/patch_diff_checker.dart b/packages/shorebird_cli/lib/src/patch_diff_checker.dart index 25802c550..33b4ee4fd 100644 --- a/packages/shorebird_cli/lib/src/patch_diff_checker.dart +++ b/packages/shorebird_cli/lib/src/patch_diff_checker.dart @@ -4,6 +4,7 @@ import 'package:http/http.dart' as http; import 'package:mason_logger/mason_logger.dart'; import 'package:path/path.dart' as p; import 'package:scoped/scoped.dart'; +import 'package:shorebird_cli/src/archive/archive.dart'; import 'package:shorebird_cli/src/archive_analysis/archive_differ.dart'; import 'package:shorebird_cli/src/http_client/http_client.dart'; import 'package:shorebird_cli/src/logger.dart'; @@ -35,6 +36,26 @@ class PatchDiffChecker { final http.Client _httpClient; + /// Zips the contents of [localArtifactDirectory] to a temporary file and + /// forwards to [confirmUnpatchableDiffsIfNecessary]. + Future zipAndConfirmUnpatchableDiffsIfNecessary({ + required Directory localArtifactDirectory, + required Uri releaseArtifactUrl, + required ArchiveDiffer archiveDiffer, + required bool force, + }) async { + final zipProgress = logger.progress('Compressing archive'); + final zippedFile = await localArtifactDirectory.zipToTempFile(); + zipProgress.complete(); + + return confirmUnpatchableDiffsIfNecessary( + localArtifact: zippedFile, + releaseArtifactUrl: releaseArtifactUrl, + archiveDiffer: archiveDiffer, + force: force, + ); + } + /// Downloads the release artifact at [releaseArtifactUrl] and checks for /// differences that could cause issues when applying the patch represented by /// [localArtifact]. diff --git a/packages/shorebird_cli/lib/src/shorebird_build_mixin.dart b/packages/shorebird_cli/lib/src/shorebird_build_mixin.dart index 5fab16b2f..0eb4d4e8f 100644 --- a/packages/shorebird_cli/lib/src/shorebird_build_mixin.dart +++ b/packages/shorebird_cli/lib/src/shorebird_build_mixin.dart @@ -182,10 +182,12 @@ mixin ShorebirdBuildMixin on ShorebirdCommand { }); } + /// Calls `flutter build ipa`. If [codesign] is false, this will only build + /// an .xcarchive and _not_ an .ipa. Future buildIpa({ + required bool codesign, String? flavor, String? target, - bool codesign = true, }) async { return _runShorebirdBuildCommand(() async { const executable = 'flutter'; diff --git a/packages/shorebird_cli/pubspec.yaml b/packages/shorebird_cli/pubspec.yaml index 221cfd42a..26bd3654d 100644 --- a/packages/shorebird_cli/pubspec.yaml +++ b/packages/shorebird_cli/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: googleapis_auth: ^1.4.0 http: ^1.0.0 intl: ^0.18.0 + io: ^1.0.4 json_annotation: ^4.8.0 mason_logger: ^0.2.8 meta: ^1.9.0 diff --git a/packages/shorebird_cli/test/fixtures/ipas/README.md b/packages/shorebird_cli/test/fixtures/ipas/README.md deleted file mode 100644 index 0055a8ef1..000000000 --- a/packages/shorebird_cli/test/fixtures/ipas/README.md +++ /dev/null @@ -1,12 +0,0 @@ -The ipa files in this folder were generated by building the stock Flutter counter app with `shorebird build ipa`. - -Some of their contents has been removed to reduce the size of the files. All .dylib files have been removed. - -Files: - -- base.ipa is meant to represent an ipa uploaded as part of a release. App.framework/App and Flutter.framework/Flutter have been removed to save space. -- no_version.ipa an .ipa with the version info removed from the Info.plist, along with several other files to save space. -- no_plist.ipa an .ipa with the Info.plist (and many other parts) removed. -- app_file_space.ipa is an .ipa with a space in the .app file name. -- asset_changes.ipa is built from the same codebase as base.ipa with a change made to one of the assets. App.framework/App and Flutter.framework/Flutter have been removed to save space. -- dart_changes.ipa is built from the same codebase as base.ipa with a change made to the dart code. App.framework/App and Flutter.framework/Flutter have been removed to save space. diff --git a/packages/shorebird_cli/test/fixtures/ipas/app_file_space.ipa b/packages/shorebird_cli/test/fixtures/ipas/app_file_space.ipa deleted file mode 100644 index 0b9062e5b..000000000 Binary files a/packages/shorebird_cli/test/fixtures/ipas/app_file_space.ipa and /dev/null differ diff --git a/packages/shorebird_cli/test/fixtures/ipas/no_plist.ipa b/packages/shorebird_cli/test/fixtures/ipas/no_plist.ipa deleted file mode 100644 index 7e92a1859..000000000 Binary files a/packages/shorebird_cli/test/fixtures/ipas/no_plist.ipa and /dev/null differ diff --git a/packages/shorebird_cli/test/fixtures/ipas/no_version.ipa b/packages/shorebird_cli/test/fixtures/ipas/no_version.ipa deleted file mode 100644 index 11db636eb..000000000 Binary files a/packages/shorebird_cli/test/fixtures/ipas/no_version.ipa and /dev/null differ diff --git a/packages/shorebird_cli/test/fixtures/xcarchives/README.md b/packages/shorebird_cli/test/fixtures/xcarchives/README.md new file mode 100644 index 000000000..358d14d1d --- /dev/null +++ b/packages/shorebird_cli/test/fixtures/xcarchives/README.md @@ -0,0 +1,8 @@ +The xcarchives in this folder were generated by building the stock Flutter counter app with `shorebird release ios-alpha --no-codesign`. .dylib files have been removed to reduce size. + +Files: + +- Runner.base.xcarchive is meant to represent an xcarchive uploaded as part of a release. +- Runner.changed_asset.xcarchive was made with same code as Runner.base.xcarchive, delta a change to a json asset file. +- Runner.changed_dart.xcarchive was made with same code as Runner.base.xcarchive, delta a change to lib/main.dart. +- Runner.changed_swift.xcarchive was made with same code as Runner.base.xcarchive, delta a change to ios/Runner/AppDelegate.swift. diff --git a/packages/shorebird_cli/test/fixtures/ipas/dart_changes.ipa b/packages/shorebird_cli/test/fixtures/xcarchives/base.xcarchive.zip similarity index 53% rename from packages/shorebird_cli/test/fixtures/ipas/dart_changes.ipa rename to packages/shorebird_cli/test/fixtures/xcarchives/base.xcarchive.zip index 27c63c95d..816bd4a02 100644 Binary files a/packages/shorebird_cli/test/fixtures/ipas/dart_changes.ipa and b/packages/shorebird_cli/test/fixtures/xcarchives/base.xcarchive.zip differ diff --git a/packages/shorebird_cli/test/fixtures/ipas/swift_changes.ipa b/packages/shorebird_cli/test/fixtures/xcarchives/changed_asset.xcarchive.zip similarity index 53% rename from packages/shorebird_cli/test/fixtures/ipas/swift_changes.ipa rename to packages/shorebird_cli/test/fixtures/xcarchives/changed_asset.xcarchive.zip index f559a114d..7c9c129b0 100644 Binary files a/packages/shorebird_cli/test/fixtures/ipas/swift_changes.ipa and b/packages/shorebird_cli/test/fixtures/xcarchives/changed_asset.xcarchive.zip differ diff --git a/packages/shorebird_cli/test/fixtures/ipas/asset_changes.ipa b/packages/shorebird_cli/test/fixtures/xcarchives/changed_dart.xcarchive.zip similarity index 53% rename from packages/shorebird_cli/test/fixtures/ipas/asset_changes.ipa rename to packages/shorebird_cli/test/fixtures/xcarchives/changed_dart.xcarchive.zip index b03a9ae74..d474974c8 100644 Binary files a/packages/shorebird_cli/test/fixtures/ipas/asset_changes.ipa and b/packages/shorebird_cli/test/fixtures/xcarchives/changed_dart.xcarchive.zip differ diff --git a/packages/shorebird_cli/test/fixtures/ipas/base.ipa b/packages/shorebird_cli/test/fixtures/xcarchives/changed_swift.xcarchive.zip similarity index 53% rename from packages/shorebird_cli/test/fixtures/ipas/base.ipa rename to packages/shorebird_cli/test/fixtures/xcarchives/changed_swift.xcarchive.zip index caede42f0..63dce563d 100644 Binary files a/packages/shorebird_cli/test/fixtures/ipas/base.ipa and b/packages/shorebird_cli/test/fixtures/xcarchives/changed_swift.xcarchive.zip differ diff --git a/packages/shorebird_cli/test/src/archive_analysis/ios_archive_differ_test.dart b/packages/shorebird_cli/test/src/archive_analysis/ios_archive_differ_test.dart index 3577ff502..d8889b511 100644 --- a/packages/shorebird_cli/test/src/archive_analysis/ios_archive_differ_test.dart +++ b/packages/shorebird_cli/test/src/archive_analysis/ios_archive_differ_test.dart @@ -4,11 +4,23 @@ import 'package:shorebird_cli/src/platform.dart'; import 'package:test/test.dart'; void main() { - final ipaFixturesBasePath = p.join('test', 'fixtures', 'ipas'); - final baseIpaPath = p.join(ipaFixturesBasePath, 'base.ipa'); - final changedAssetIpaPath = p.join(ipaFixturesBasePath, 'asset_changes.ipa'); - final changedDartIpaPath = p.join(ipaFixturesBasePath, 'dart_changes.ipa'); - final changedSwiftIpaPath = p.join(ipaFixturesBasePath, 'swift_changes.ipa'); + final xcarchiveFixturesBasePath = p.join('test', 'fixtures', 'xcarchives'); + final baseIpaPath = p.join( + xcarchiveFixturesBasePath, + 'base.xcarchive.zip', + ); + final changedAssetIpaPath = p.join( + xcarchiveFixturesBasePath, + 'changed_asset.xcarchive.zip', + ); + final changedDartIpaPath = p.join( + xcarchiveFixturesBasePath, + 'changed_dart.xcarchive.zip', + ); + final changedSwiftIpaPath = p.join( + xcarchiveFixturesBasePath, + 'changed_swift.xcarchive.zip', + ); final xcframeworkFixturesBasePath = p.join( 'test', @@ -34,29 +46,33 @@ void main() { group('appRegex', () { test('identifies Runner.app/Runner as an app file', () { expect( - IosArchiveDiffer.appRegex.hasMatch('Payload/Runner.app/Runner'), + IosArchiveDiffer.appRegex.hasMatch( + 'Products/Applications/Runner.app/Runner', + ), isTrue, ); }); test('does not identify Runner.app/Assets.car as an app file', () { expect( - IosArchiveDiffer.appRegex.hasMatch('Payload/Runner.app/Assets.car'), + IosArchiveDiffer.appRegex.hasMatch( + 'Products/Applications/Runner.app/Assets.car', + ), isFalse, ); }); }); - group('ipa', () { + group('xcarchive', () { group('changedPaths', () { - test('finds no differences between the same ipa', () { + test('finds no differences between the same xcarchive', () { expect( differ.changedFiles(baseIpaPath, baseIpaPath), isEmpty, ); }); - test('finds differences between two different ipas', () { + test('finds differences between two different xcarchives', () { final fileSetDiff = differ.changedFiles( baseIpaPath, changedAssetIpaPath, @@ -65,27 +81,19 @@ void main() { expect( fileSetDiff.changedPaths, { - 'Payload/Runner.app/_CodeSignature/CodeResources', - 'Payload/Runner.app/Frameworks/App.framework/_CodeSignature/CodeResources', - 'Payload/Runner.app/Frameworks/App.framework/flutter_assets/assets/asset.json', - 'Symbols/4C4C4411-5555-3144-A13A-E47369D8ACD5.symbols', - 'Symbols/BC970605-0A53-3457-8736-D7A870AB6E71.symbols', - 'Symbols/0CBBC9EF-0745-3074-81B7-765F5B4515FD.symbols', + 'Products/Applications/Runner.app/Frameworks/App.framework/_CodeSignature/CodeResources', + 'Products/Applications/Runner.app/Frameworks/App.framework/flutter_assets/assets/asset.json', + 'Info.plist', }, ); } else { expect( fileSetDiff.changedPaths, { - 'Payload/Runner.app/_CodeSignature/CodeResources', - 'Payload/Runner.app/Runner', - 'Payload/Runner.app/Frameworks/Flutter.framework/Flutter', - 'Payload/Runner.app/Frameworks/App.framework/_CodeSignature/CodeResources', - 'Payload/Runner.app/Frameworks/App.framework/App', - 'Payload/Runner.app/Frameworks/App.framework/flutter_assets/assets/asset.json', - 'Symbols/4C4C4411-5555-3144-A13A-E47369D8ACD5.symbols', - 'Symbols/BC970605-0A53-3457-8736-D7A870AB6E71.symbols', - 'Symbols/0CBBC9EF-0745-3074-81B7-765F5B4515FD.symbols', + 'Products/Applications/Runner.app/Frameworks/App.framework/_CodeSignature/CodeResources', + 'Products/Applications/Runner.app/Frameworks/App.framework/App', + 'Products/Applications/Runner.app/Frameworks/App.framework/flutter_assets/assets/asset.json', + 'Info.plist', }, ); } @@ -101,10 +109,7 @@ void main() { differ.dartFileSetDiff(fileSetDiff), platform.isMacOS ? isEmpty : isNotEmpty, ); - expect( - differ.nativeFileSetDiff(fileSetDiff), - platform.isMacOS ? isEmpty : isNotEmpty, - ); + expect(differ.nativeFileSetDiff(fileSetDiff), isEmpty); }); test('detects dart changes', () { @@ -112,20 +117,14 @@ void main() { differ.changedFiles(baseIpaPath, changedDartIpaPath); expect(differ.assetsFileSetDiff(fileSetDiff), isEmpty); expect(differ.dartFileSetDiff(fileSetDiff), isNotEmpty); - expect( - differ.nativeFileSetDiff(fileSetDiff), - platform.isMacOS ? isEmpty : isNotEmpty, - ); + expect(differ.nativeFileSetDiff(fileSetDiff), isEmpty); }); test('detects swift changes', () { final fileSetDiff = differ.changedFiles(baseIpaPath, changedSwiftIpaPath); expect(differ.assetsFileSetDiff(fileSetDiff), isEmpty); - expect( - differ.dartFileSetDiff(fileSetDiff), - platform.isMacOS ? isEmpty : isNotEmpty, - ); + expect(differ.dartFileSetDiff(fileSetDiff), isEmpty); expect(differ.nativeFileSetDiff(fileSetDiff), isNotEmpty); }); }); @@ -173,7 +172,7 @@ void main() { ); expect( differ.containsPotentiallyBreakingNativeDiffs(fileSetDiff), - platform.isMacOS ? isFalse : isTrue, + isFalse, ); }); }); diff --git a/packages/shorebird_cli/test/src/archive_analysis/ipa_test.dart b/packages/shorebird_cli/test/src/archive_analysis/ipa_test.dart deleted file mode 100644 index 527c76c58..000000000 --- a/packages/shorebird_cli/test/src/archive_analysis/ipa_test.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:path/path.dart' as p; -import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart'; -import 'package:test/test.dart'; - -void main() { - final ipaFixturesBasePath = p.join('test', 'fixtures', 'ipas'); - final baseIpaBath = p.join(ipaFixturesBasePath, 'base.ipa'); - final noVersionIpaPath = p.join(ipaFixturesBasePath, 'no_version.ipa'); - final noPlistIpaPath = p.join(ipaFixturesBasePath, 'no_plist.ipa'); - final spaceInAppFileNameIpaPath = - p.join(ipaFixturesBasePath, 'app_file_space.ipa'); - - group(IpaReader, () { - test('creates Ipa', () { - final ipa = IpaReader().read(baseIpaBath); - expect(ipa.versionNumber, '1.0.0+1'); - }); - }); - - group(Ipa, () { - test('reads app version from ipa', () { - final ipa = Ipa(path: baseIpaBath); - expect(ipa.versionNumber, '1.0.0+1'); - }); - - test('reads app version from ipa with space in app file name', () { - final ipa = Ipa(path: spaceInAppFileNameIpaPath); - expect(ipa.versionNumber, '1.0.0+1'); - }); - - test('throws exception if no Info.plist is found', () { - final ipa = Ipa(path: noPlistIpaPath); - expect(() => ipa.versionNumber, throwsException); - }); - - test('throws exception if no version is found in Info.plist', () { - final ipa = Ipa(path: noVersionIpaPath); - expect(() => ipa.versionNumber, throwsException); - }); - }); -} diff --git a/packages/shorebird_cli/test/src/code_push_client_wrapper_test.dart b/packages/shorebird_cli/test/src/code_push_client_wrapper_test.dart index 4a752ca07..2100d6dde 100644 --- a/packages/shorebird_cli/test/src/code_push_client_wrapper_test.dart +++ b/packages/shorebird_cli/test/src/code_push_client_wrapper_test.dart @@ -1483,12 +1483,14 @@ Please bump your version number and try again.''', }); group('createIosReleaseArtifacts', () { - final ipaPath = p.join('path', 'to', 'app.ipa'); + final xcarchivePath = p.join('path', 'to', 'app.xcarchive'); final runnerPath = p.join('path', 'to', 'runner.app'); Directory setUpTempDir({String? flavor}) { final tempDir = Directory.systemTemp.createTempSync(); - File(p.join(tempDir.path, ipaPath)).createSync(recursive: true); + Directory(p.join(tempDir.path, xcarchivePath)) + .createSync(recursive: true); + Directory(p.join(tempDir.path, runnerPath)).createSync(recursive: true); return tempDir; } @@ -1505,12 +1507,14 @@ Please bump your version number and try again.''', ).thenAnswer((_) async => {}); }); - test('exits with code 70 when ipa artifact creation fails', () async { + test('exits with code 70 when xcarchive artifact creation fails', + () async { const error = 'something went wrong'; when( () => codePushClient.createReleaseArtifact( appId: any(named: 'appId'), - artifactPath: any(named: 'artifactPath', that: endsWith('.ipa')), + artifactPath: + any(named: 'artifactPath', that: endsWith('.xcarchive.zip')), releaseId: any(named: 'releaseId'), arch: any(named: 'arch'), platform: any(named: 'platform'), @@ -1525,7 +1529,7 @@ Please bump your version number and try again.''', () async => codePushClientWrapper.createIosReleaseArtifacts( appId: app.appId, releaseId: releaseId, - ipaPath: p.join(tempDir.path, ipaPath), + xcarchivePath: p.join(tempDir.path, xcarchivePath), runnerPath: p.join(tempDir.path, runnerPath), ), ), @@ -1537,13 +1541,14 @@ Please bump your version number and try again.''', verify(() => progress.fail(any(that: contains(error)))).called(1); }); - test('exits with code 70 when uploading ipa that already exists', + test('exits with code 70 when uploading xcarchive that already exists', () async { const error = 'something went wrong'; when( () => codePushClient.createReleaseArtifact( appId: any(named: 'appId'), - artifactPath: any(named: 'artifactPath', that: endsWith('.ipa')), + artifactPath: + any(named: 'artifactPath', that: endsWith('.xcarchive.zip')), releaseId: any(named: 'releaseId'), arch: any(named: 'arch'), platform: any(named: 'platform'), @@ -1558,7 +1563,7 @@ Please bump your version number and try again.''', () async => codePushClientWrapper.createIosReleaseArtifacts( appId: app.appId, releaseId: releaseId, - ipaPath: p.join(tempDir.path, ipaPath), + xcarchivePath: p.join(tempDir.path, xcarchivePath), runnerPath: p.join(tempDir.path, runnerPath), ), ), @@ -1594,7 +1599,7 @@ Please bump your version number and try again.''', () async => codePushClientWrapper.createIosReleaseArtifacts( appId: app.appId, releaseId: releaseId, - ipaPath: p.join(tempDir.path, ipaPath), + xcarchivePath: p.join(tempDir.path, xcarchivePath), runnerPath: p.join(tempDir.path, runnerPath), ), ), @@ -1624,7 +1629,7 @@ Please bump your version number and try again.''', () async => codePushClientWrapper.createIosReleaseArtifacts( appId: app.appId, releaseId: releaseId, - ipaPath: p.join(tempDir.path, ipaPath), + xcarchivePath: p.join(tempDir.path, xcarchivePath), runnerPath: p.join(tempDir.path, runnerPath), ), getCurrentDirectory: () => tempDir, diff --git a/packages/shorebird_cli/test/src/commands/init_command_test.dart b/packages/shorebird_cli/test/src/commands/init_command_test.dart index 84ec1cd89..30ef2450c 100644 --- a/packages/shorebird_cli/test/src/commands/init_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/init_command_test.dart @@ -189,7 +189,8 @@ Please make sure you are running "shorebird init" from the root of your Flutter final exitCode = await runWithOverrides(command.run); verify( () => logger.err( - 'A "shorebird.yaml" file already exists and seems up-to-date.'), + 'A "shorebird.yaml" file already exists and seems up-to-date.', + ), ).called(1); verify( () => logger.info( 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 92b35da87..50a31477c 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 @@ -37,12 +37,8 @@ class _MockCodePushClientWrapper extends Mock class _MockDoctor extends Mock implements Doctor {} -class _MockIpa extends Mock implements Ipa {} - class _MockIpaDiffer extends Mock implements IosArchiveDiffer {} -class _MockIpaReader extends Mock implements IpaReader {} - class _MockLogger extends Mock implements Logger {} class _MockPatchDiffChecker extends Mock implements PatchDiffChecker {} @@ -85,10 +81,40 @@ void main() { - CFBundleName - app_bundle_name + ApplicationProperties + + ApplicationPath + Applications/Runner.app + Architectures + + arm64 + + CFBundleIdentifier + com.shorebird.timeShift + CFBundleShortVersionString + 1.2.3 + CFBundleVersion + 1 + + ArchiveVersion + 2 + Name + Runner + SchemeName + Runner '''; + const emptyPlistContent = ''' + + + + + ApplicationProperties + + + +' +'''; const pubspecYamlContent = ''' name: example version: $version @@ -126,9 +152,7 @@ flutter: late Directory shorebirdRoot; late File genSnapshotFile; late Doctor doctor; - late Ipa ipa; late IosArchiveDiffer archiveDiffer; - late IpaReader ipaReader; late Progress progress; late Logger logger; late PatchDiffChecker patchDiffChecker; @@ -171,7 +195,14 @@ flutter: p.join(tempDir.path, 'shorebird.yaml'), ).writeAsStringSync('app_id: $appId'); File( - p.join(tempDir.path, 'ios', 'Runner', 'Info.plist'), + p.join( + tempDir.path, + 'build', + 'ios', + 'archive', + 'Runner.xcarchive', + 'Info.plist', + ), ) ..createSync(recursive: true) ..writeAsStringSync(infoPlistContent); @@ -199,6 +230,7 @@ flutter: } setUpAll(() { + registerFallbackValue(Directory('')); registerFallbackValue(File('')); registerFallbackValue(FileSetDiff.empty()); registerFallbackValue(ReleasePlatform.ios); @@ -227,9 +259,7 @@ flutter: 'gen_snapshot_arm64', ), ); - ipa = _MockIpa(); archiveDiffer = _MockIpaDiffer(); - ipaReader = _MockIpaReader(); progress = _MockProgress(); logger = _MockLogger(); platform = _MockPlatform(); @@ -277,8 +307,6 @@ flutter: patchArtifactBundles: any(named: 'patchArtifactBundles'), ), ).thenAnswer((_) async {}); - when(() => ipa.versionNumber).thenReturn(version); - when(() => ipaReader.read(any())).thenReturn(ipa); when(() => doctor.iosCommandValidators).thenReturn([flutterValidator]); when(flutterValidator.validate).thenAnswer((_) async => []); when(() => logger.confirm(any())).thenReturn(true); @@ -333,8 +361,8 @@ flutter: ), ).thenAnswer((_) async {}); when( - () => patchDiffChecker.confirmUnpatchableDiffsIfNecessary( - localArtifact: any(named: 'localArtifact'), + () => patchDiffChecker.zipAndConfirmUnpatchableDiffsIfNecessary( + localArtifactDirectory: any(named: 'localArtifactDirectory'), releaseArtifactUrl: any(named: 'releaseArtifactUrl'), archiveDiffer: archiveDiffer, force: any(named: 'force'), @@ -342,8 +370,7 @@ flutter: ).thenAnswer((_) async => {}); command = runWithOverrides( - () => - PatchIosCommand(archiveDiffer: archiveDiffer, ipaReader: ipaReader), + () => PatchIosCommand(archiveDiffer: archiveDiffer), )..testArgResults = argResults; }); @@ -451,63 +478,7 @@ error: exportArchive: No signing certificate "iOS Distribution" found 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', - ]), - ), - ), + () => logger.err(any(that: contains('No Info.plist file found'))), ).called(1); }); @@ -659,10 +630,21 @@ Please re-run the release command for this version or create a new release.'''), test('exits with code 70 when release version cannot be determiend', () async { - when(() => ipa.versionNumber).thenThrow(Exception('oops')); - final tempDir = setUpTempDir(); setUpTempArtifacts(tempDir); + File( + p.join( + tempDir.path, + 'build', + 'ios', + 'archive', + 'Runner.xcarchive', + 'Info.plist', + ), + ) + ..createSync(recursive: true) + ..writeAsStringSync(emptyPlistContent); + final exitCode = await IOOverrides.runZoned( () => runWithOverrides(command.run), getCurrentDirectory: () => tempDir, @@ -670,7 +652,7 @@ Please re-run the release command for this version or create a new release.'''), expect(exitCode, equals(ExitCode.software.code)); verify( - () => progress.fail( + () => logger.err( any(that: contains('Failed to determine release version')), ), ).called(1); @@ -679,14 +661,14 @@ Please re-run the release command for this version or create a new release.'''), test('prints release version when detected', () async { final tempDir = setUpTempDir(); setUpTempArtifacts(tempDir); + final exitCode = await IOOverrides.runZoned( () => runWithOverrides(command.run), getCurrentDirectory: () => tempDir, ); expect(exitCode, equals(ExitCode.success.code)); - verify(() => progress.complete('Detected release version 1.2.3+1')) - .called(1); + verify(() => logger.info('Detected release version 1.2.3+1')).called(1); }); test('aborts when user opts out', () async { @@ -717,62 +699,13 @@ Please re-run the release command for this version or create a new release.'''), expect(exitCode, ExitCode.software.code); }); - test('exits with code 70 when ipa not found', () async { - final tempDir = setUpTempDir(); - setUpTempArtifacts(tempDir); - File(p.join(tempDir.path, ipaPath)).deleteSync(); - - final exitCode = await IOOverrides.runZoned( - () => runWithOverrides(command.run), - getCurrentDirectory: () => tempDir, - ); - - expect(exitCode, equals(ExitCode.software.code)); - verify( - () => progress.fail(any(that: contains('Could not find ipa file'))), - ).called(1); - }); - - test('exits with code 70 when local release version cannot be determiend', - () async { - when(() => ipa.versionNumber).thenThrow(Exception('oops')); - - final tempDir = setUpTempDir(); - setUpTempArtifacts(tempDir); - final exitCode = await IOOverrides.runZoned( - () => runWithOverrides(command.run), - getCurrentDirectory: () => tempDir, - ); - - expect(exitCode, equals(ExitCode.software.code)); - verify( - () => progress.fail( - any(that: contains('Failed to determine release version')), - ), - ).called(1); - }); - - test('prints local release version when detected', () async { - final tempDir = setUpTempDir(); - setUpTempArtifacts(tempDir); - final exitCode = await IOOverrides.runZoned( - () => runWithOverrides(command.run), - getCurrentDirectory: () => tempDir, - ); - - expect(exitCode, equals(ExitCode.success.code)); - verify( - () => progress.complete('Detected release version 1.2.3+1'), - ).called(1); - }); - test( - '''exits with code 0 if confirmUnpatchableDiffsIfNecessary throws UserCancelledException''', + '''exits with code 0 if zipAndConfirmUnpatchableDiffsIfNecessary throws UserCancelledException''', () async { when(() => argResults['force']).thenReturn(false); when( - () => patchDiffChecker.confirmUnpatchableDiffsIfNecessary( - localArtifact: any(named: 'localArtifact'), + () => patchDiffChecker.zipAndConfirmUnpatchableDiffsIfNecessary( + localArtifactDirectory: any(named: 'localArtifactDirectory'), releaseArtifactUrl: any(named: 'releaseArtifactUrl'), archiveDiffer: archiveDiffer, force: any(named: 'force'), @@ -788,8 +721,8 @@ Please re-run the release command for this version or create a new release.'''), expect(exitCode, equals(ExitCode.success.code)); verify( - () => patchDiffChecker.confirmUnpatchableDiffsIfNecessary( - localArtifact: any(named: 'localArtifact'), + () => patchDiffChecker.zipAndConfirmUnpatchableDiffsIfNecessary( + localArtifactDirectory: any(named: 'localArtifactDirectory'), releaseArtifactUrl: Uri.parse(ipaArtifact.url), archiveDiffer: archiveDiffer, force: false, @@ -807,12 +740,12 @@ Please re-run the release command for this version or create a new release.'''), }); test( - '''exits with code 70 if confirmUnpatchableDiffsIfNecessary throws UnpatchableChangeException''', + '''exits with code 70 if zipAndConfirmUnpatchableDiffsIfNecessary throws UnpatchableChangeException''', () async { when(() => argResults['force']).thenReturn(false); when( - () => patchDiffChecker.confirmUnpatchableDiffsIfNecessary( - localArtifact: any(named: 'localArtifact'), + () => patchDiffChecker.zipAndConfirmUnpatchableDiffsIfNecessary( + localArtifactDirectory: any(named: 'localArtifactDirectory'), releaseArtifactUrl: any(named: 'releaseArtifactUrl'), archiveDiffer: archiveDiffer, force: any(named: 'force'), @@ -828,8 +761,8 @@ Please re-run the release command for this version or create a new release.'''), expect(exitCode, equals(ExitCode.software.code)); verify( - () => patchDiffChecker.confirmUnpatchableDiffsIfNecessary( - localArtifact: any(named: 'localArtifact'), + () => patchDiffChecker.zipAndConfirmUnpatchableDiffsIfNecessary( + localArtifactDirectory: any(named: 'localArtifactDirectory'), releaseArtifactUrl: Uri.parse(ipaArtifact.url), archiveDiffer: archiveDiffer, force: false, @@ -933,6 +866,33 @@ Please re-run the release command for this version or create a new release.'''), ).called(1); }); + test('forwards codesign to flutter build', () async { + when(() => argResults['codesign']).thenReturn(false); + final tempDir = setUpTempDir(); + setUpTempArtifacts(tempDir); + await IOOverrides.runZoned( + () => runWithOverrides(command.run), + getCurrentDirectory: () => tempDir, + ); + + verify( + () => shorebirdProcess.run( + 'flutter', + any( + that: containsAllInOrder( + [ + 'build', + 'ipa', + '--release', + '--no-codesign', + ], + ), + ), + runInShell: true, + ), + ).called(1); + }); + test( 'succeeds when patch is successful ' 'with flavors and target', () async { diff --git a/packages/shorebird_cli/test/src/commands/patch/patch_ios_framework_command_test.dart b/packages/shorebird_cli/test/src/commands/patch/patch_ios_framework_command_test.dart index 0de0266c8..f56670876 100644 --- a/packages/shorebird_cli/test/src/commands/patch/patch_ios_framework_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/patch/patch_ios_framework_command_test.dart @@ -179,6 +179,7 @@ flutter: } setUpAll(() { + registerFallbackValue(Directory('')); registerFallbackValue(File('')); registerFallbackValue(ReleasePlatform.ios); registerFallbackValue(Uri.parse('https://example.com')); @@ -300,8 +301,8 @@ flutter: ), ).thenAnswer((_) async {}); when( - () => patchDiffChecker.confirmUnpatchableDiffsIfNecessary( - localArtifact: any(named: 'localArtifact'), + () => patchDiffChecker.zipAndConfirmUnpatchableDiffsIfNecessary( + localArtifactDirectory: any(named: 'localArtifactDirectory'), releaseArtifactUrl: any(named: 'releaseArtifactUrl'), archiveDiffer: archiveDiffer, force: any(named: 'force'), @@ -633,12 +634,12 @@ Please re-run the release command for this version or create a new release.'''), }); test( - '''exits with code 0 if confirmUnpatchableDiffsIfNecessary throws UserCancelledException''', + '''exits with code 0 if zipAndConfirmUnpatchableDiffsIfNecessary throws UserCancelledException''', () async { when(() => argResults['force']).thenReturn(false); when( - () => patchDiffChecker.confirmUnpatchableDiffsIfNecessary( - localArtifact: any(named: 'localArtifact'), + () => patchDiffChecker.zipAndConfirmUnpatchableDiffsIfNecessary( + localArtifactDirectory: any(named: 'localArtifactDirectory'), releaseArtifactUrl: any(named: 'releaseArtifactUrl'), archiveDiffer: archiveDiffer, force: any(named: 'force'), @@ -654,8 +655,8 @@ Please re-run the release command for this version or create a new release.'''), expect(exitCode, equals(ExitCode.success.code)); verify( - () => patchDiffChecker.confirmUnpatchableDiffsIfNecessary( - localArtifact: any(named: 'localArtifact'), + () => patchDiffChecker.zipAndConfirmUnpatchableDiffsIfNecessary( + localArtifactDirectory: any(named: 'localArtifactDirectory'), releaseArtifactUrl: Uri.parse(xcframeworkArtifact.url), archiveDiffer: archiveDiffer, force: false, @@ -673,12 +674,12 @@ Please re-run the release command for this version or create a new release.'''), }); test( - '''exits with code 70 if confirmUnpatchableDiffsIfNecessary throws UnpatchableChangeException''', + '''exits with code 70 if zipAndConfirmUnpatchableDiffsIfNecessary throws UnpatchableChangeException''', () async { when(() => argResults['force']).thenReturn(false); when( - () => patchDiffChecker.confirmUnpatchableDiffsIfNecessary( - localArtifact: any(named: 'localArtifact'), + () => patchDiffChecker.zipAndConfirmUnpatchableDiffsIfNecessary( + localArtifactDirectory: any(named: 'localArtifactDirectory'), releaseArtifactUrl: any(named: 'releaseArtifactUrl'), archiveDiffer: archiveDiffer, force: any(named: 'force'), @@ -694,8 +695,8 @@ Please re-run the release command for this version or create a new release.'''), expect(exitCode, equals(ExitCode.software.code)); verify( - () => patchDiffChecker.confirmUnpatchableDiffsIfNecessary( - localArtifact: any(named: 'localArtifact'), + () => patchDiffChecker.zipAndConfirmUnpatchableDiffsIfNecessary( + localArtifactDirectory: any(named: 'localArtifactDirectory'), releaseArtifactUrl: Uri.parse(xcframeworkArtifact.url), archiveDiffer: archiveDiffer, force: false, diff --git a/packages/shorebird_cli/test/src/commands/release/release_android_command_test.dart b/packages/shorebird_cli/test/src/commands/release/release_android_command_test.dart index b3c6279d8..8b56a6cf2 100644 --- a/packages/shorebird_cli/test/src/commands/release/release_android_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/release/release_android_command_test.dart @@ -357,12 +357,20 @@ void main() { test('succeeds when release is successful', () async { final exitCode = await runWithOverrides(command.run); verify(() => logger.success('\n✅ Published Release!')).called(1); + final aabPath = p.join( + 'build', + 'app', + 'outputs', + 'bundle', + 'release', + 'app-release.aab', + ); // Verify info message does not include apk instructions. verify( () => logger.info(''' Your next step is to upload the app bundle to the Play Store: -${lightCyan.wrap('build/app/outputs/bundle/release/app-release.aab')} +${lightCyan.wrap(aabPath)} For information on uploading to the Play Store, see: ${link(uri: Uri.parse('https://support.google.com/googleplay/android-developer/answer/9859152?hl=en'))} @@ -393,14 +401,30 @@ ${link(uri: Uri.parse('https://support.google.com/googleplay/android-developer/a final exitCode = await runWithOverrides(command.run); verify(() => logger.success('\n✅ Published Release!')).called(1); // Verify info message does include apk instructions. + final aabPath = p.join( + 'build', + 'app', + 'outputs', + 'bundle', + 'release', + 'app-release.aab', + ); + final apkPath = p.join( + 'build', + 'app', + 'outputs', + 'apk', + 'release', + 'app-release.apk', + ); verify( () => logger.info(''' Your next step is to upload the app bundle to the Play Store: -${lightCyan.wrap('build/app/outputs/bundle/release/app-release.aab')} +${lightCyan.wrap(aabPath)} Or distribute the apk: -${lightCyan.wrap('build/app/outputs/apk/release/app-release.apk')} +${lightCyan.wrap(apkPath)} For information on uploading to the Play Store, see: ${link(uri: Uri.parse('https://support.google.com/googleplay/android-developer/answer/9859152?hl=en'))} 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 876daf9b9..0763c005b 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 @@ -7,7 +7,6 @@ import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; import 'package:propertylistserialization/propertylistserialization.dart'; import 'package:scoped/scoped.dart'; -import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart'; import 'package:shorebird_cli/src/auth/auth.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; import 'package:shorebird_cli/src/commands/commands.dart'; @@ -31,10 +30,6 @@ class _MockCodePushClientWrapper extends Mock class _MockDoctor extends Mock implements Doctor {} -class _MockIpa extends Mock implements Ipa {} - -class _MockIpaReader extends Mock implements IpaReader {} - class _MockLogger extends Mock implements Logger {} class _MockPlatform extends Mock implements Platform {} @@ -82,10 +77,40 @@ void main() { - CFBundleName - app_bundle_name + ApplicationProperties + + ApplicationPath + Applications/Runner.app + Architectures + + arm64 + + CFBundleIdentifier + com.shorebird.timeShift + CFBundleShortVersionString + 1.2.3 + CFBundleVersion + 1 + + ArchiveVersion + 2 + Name + Runner + SchemeName + Runner '''; + const emptyPlistContent = ''' + + + + + ApplicationProperties + + + +' +'''; const pubspecYamlContent = ''' name: example version: $version @@ -102,8 +127,6 @@ flutter: late Doctor doctor; late Platform platform; late Auth auth; - late IpaReader ipaReader; - late Ipa ipa; late Progress progress; late Logger logger; late ShorebirdProcessResult flutterBuildProcessResult; @@ -139,7 +162,14 @@ flutter: p.join(tempDir.path, 'shorebird.yaml'), ).writeAsStringSync('app_id: $appId'); File( - p.join(tempDir.path, 'ios', 'Runner', 'Info.plist'), + p.join( + tempDir.path, + 'build', + 'ios', + 'archive', + 'Runner.xcarchive', + 'Info.plist', + ), ) ..createSync(recursive: true) ..writeAsStringSync(infoPlistContent); @@ -161,8 +191,6 @@ flutter: platform = _MockPlatform(); shorebirdRoot = Directory.systemTemp.createTempSync(); auth = _MockAuth(); - ipa = _MockIpa(); - ipaReader = _MockIpaReader(); progress = _MockProgress(); logger = _MockLogger(); flutterBuildProcessResult = _MockProcessResult(); @@ -193,10 +221,9 @@ flutter: ).thenAnswer((_) async => flutterBuildProcessResult); when(() => argResults.rest).thenReturn([]); when(() => argResults['arch']).thenReturn(arch); + when(() => argResults['codesign']).thenReturn(true); when(() => argResults['platform']).thenReturn(releasePlatform); when(() => auth.isAuthenticated).thenReturn(true); - when(() => ipaReader.read(any())).thenReturn(ipa); - when(() => ipa.versionNumber).thenReturn(version); when(() => logger.progress(any())).thenReturn(progress); when(() => logger.confirm(any())).thenReturn(true); when( @@ -235,7 +262,7 @@ flutter: () => codePushClientWrapper.createIosReleaseArtifacts( appId: any(named: 'appId'), releaseId: any(named: 'releaseId'), - ipaPath: any(named: 'ipaPath'), + xcarchivePath: any(named: 'xcarchivePath'), runnerPath: any(named: 'runnerPath'), ), ).thenAnswer((_) async => release); @@ -258,7 +285,7 @@ flutter: ), ).thenAnswer((_) async {}); - command = runWithOverrides(() => ReleaseIosCommand(ipaReader: ipaReader)) + command = runWithOverrides(ReleaseIosCommand.new) ..testArgResults = argResults; }); @@ -290,6 +317,84 @@ flutter: ).called(1); }); + group('when codesign is disabled', () { + setUp(() { + when(() => argResults['codesign']).thenReturn(false); + }); + + test('prints instructions to manually codesign', () async { + final tempDir = setUpTempDir(); + await IOOverrides.runZoned( + () => runWithOverrides(command.run), + getCurrentDirectory: () => tempDir, + ); + + verify( + () => logger.info( + '''Building for device with codesigning disabled. You will have to manually codesign before deploying to device.''', + ), + ).called(1); + }); + + test('builds without codesigning', () async { + final tempDir = setUpTempDir(); + await IOOverrides.runZoned( + () => runWithOverrides(command.run), + getCurrentDirectory: () => tempDir, + ); + + verify( + () => shorebirdProcess.run( + 'flutter', + any( + that: containsAllInOrder( + [ + 'build', + 'ipa', + '--release', + '--no-codesign', + ], + ), + ), + runInShell: true, + ), + ).called(1); + }); + + test('prints archive upload instructions on success', () async { + final tempDir = setUpTempDir(); + final result = await IOOverrides.runZoned( + () => runWithOverrides(command.run), + getCurrentDirectory: () => tempDir, + ); + + expect(result, equals(ExitCode.success.code)); + final archivePath = p.join( + tempDir.path, + 'build', + 'ios', + 'archive', + 'Runner.xcarchive', + ); + verify( + () => logger.info( + any( + that: stringContainsInOrder( + [ + 'Your next step is to submit the archive', + archivePath, + 'to the App Store using Xcode.', + 'You can open the archive in Xcode by running', + 'open $archivePath', + '''Make sure to uncheck "Manage Version and Build Number", or else shorebird will not work.''', + ], + ), + ), + ), + ).called(1); + }); + }); + test('exits with code 70 when build fails with non-zero exit code', () async { when(() => flutterBuildProcessResult.exitCode).thenReturn(1); @@ -346,9 +451,19 @@ error: exportArchive: No signing certificate "iOS Distribution" found test('exits with code 70 when release version cannot be determiend', () async { - when(() => ipa.versionNumber).thenThrow(Exception('oops')); - final tempDir = setUpTempDir(); + File( + p.join( + tempDir.path, + 'build', + 'ios', + 'archive', + 'Runner.xcarchive', + 'Info.plist', + ), + ) + ..createSync(recursive: true) + ..writeAsStringSync(emptyPlistContent); final exitCode = await IOOverrides.runZoned( () => runWithOverrides(command.run), getCurrentDirectory: () => tempDir, @@ -356,7 +471,7 @@ error: exportArchive: No signing certificate "iOS Distribution" found expect(exitCode, equals(ExitCode.software.code)); verify( - () => progress.fail( + () => logger.err( any(that: contains('Failed to determine release version')), ), ).called(1); @@ -377,12 +492,37 @@ error: exportArchive: No signing certificate "iOS Distribution" found () => codePushClientWrapper.createIosReleaseArtifacts( appId: appId, releaseId: release.id, - ipaPath: any(named: 'ipaPath', that: endsWith('.ipa')), + xcarchivePath: + any(named: 'xcarchivePath', that: endsWith('.xcarchive')), runnerPath: any(named: 'runnerPath', that: endsWith('Runner.app')), ), ); }); + test('exits with code 70 if Info.plist does not exist', () async { + final tempDir = setUpTempDir(); + final infoPlistFile = File( + p.join( + tempDir.path, + 'build', + 'ios', + 'archive', + 'Runner.xcarchive', + 'Info.plist', + ), + )..deleteSync(recursive: true); + + final exitCode = await IOOverrides.runZoned( + () => runWithOverrides(command.run), + getCurrentDirectory: () => tempDir, + ); + + expect(exitCode, equals(ExitCode.software.code)); + verify( + () => logger.err('No Info.plist file found at ${infoPlistFile.path}.'), + ).called(1); + }); + test('exits with code 70 if build directory does not exist', () async { final tempDir = setUpTempDir(); Directory(p.join(tempDir.path, 'build')).deleteSync(recursive: true); @@ -394,12 +534,31 @@ error: exportArchive: No signing certificate "iOS Distribution" found expect(exitCode, equals(ExitCode.software.code)); verify( - () => progress.fail( + () => logger.err(any(that: contains('No directory found'))), + ).called(1); + }); + + test('exits with code 70 if ipa build directory does not exist', () async { + final tempDir = setUpTempDir(); + final ipaDirectory = + Directory(p.join(tempDir.path, 'build', 'ios', 'ipa')) + ..deleteSync(recursive: true); + + final exitCode = await IOOverrides.runZoned( + () => runWithOverrides(command.run), + getCurrentDirectory: () => tempDir, + ); + + expect(exitCode, equals(ExitCode.software.code)); + verify( + () => logger.err( any( - that: stringContainsInOrder([ - 'Could not find ipa file', - 'No directory found at ${p.join(tempDir.path, 'build')}', - ]), + that: stringContainsInOrder( + [ + 'Could not find ipa file', + 'No directory found at ${ipaDirectory.path}', + ], + ), ), ), ).called(1); @@ -416,7 +575,7 @@ error: exportArchive: No signing certificate "iOS Distribution" found expect(exitCode, equals(ExitCode.software.code)); verify( - () => progress.fail( + () => logger.err( any( that: stringContainsInOrder([ 'Could not find ipa file', @@ -440,7 +599,7 @@ error: exportArchive: No signing certificate "iOS Distribution" found expect(exitCode, equals(ExitCode.software.code)); verify( - () => progress.fail( + () => logger.err( any( that: stringContainsInOrder([ 'Could not find ipa file', @@ -504,7 +663,8 @@ error: exportArchive: No signing certificate "iOS Distribution" found () => codePushClientWrapper.createIosReleaseArtifacts( appId: appId, releaseId: release.id, - ipaPath: any(named: 'ipaPath', that: endsWith('.ipa')), + xcarchivePath: + any(named: 'xcarchivePath', that: endsWith('.xcarchive')), runnerPath: any(named: 'runnerPath', that: endsWith('Runner.app')), ), ).called(1); @@ -575,7 +735,8 @@ flavors: () => codePushClientWrapper.createIosReleaseArtifacts( appId: appId, releaseId: release.id, - ipaPath: any(named: 'ipaPath', that: endsWith('.ipa')), + xcarchivePath: + any(named: 'xcarchivePath', that: endsWith('.xcarchive')), runnerPath: any(named: 'runnerPath', that: endsWith('Runner.app')), ), ).called(1); @@ -610,7 +771,8 @@ flavors: () => codePushClientWrapper.createIosReleaseArtifacts( appId: appId, releaseId: release.id, - ipaPath: any(named: 'ipaPath', that: endsWith('.ipa')), + xcarchivePath: + any(named: 'xcarchivePath', that: endsWith('.xcarchive')), runnerPath: any(named: 'runnerPath', that: endsWith('Runner.app')), ), ).called(1); diff --git a/packages/shorebird_cli/test/src/commands/release/release_ios_framework_command_test.dart b/packages/shorebird_cli/test/src/commands/release/release_ios_framework_command_test.dart index bc8500945..e6c973d46 100644 --- a/packages/shorebird_cli/test/src/commands/release/release_ios_framework_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/release/release_ios_framework_command_test.dart @@ -123,6 +123,7 @@ flutter: } setUpAll(() { + registerFallbackValue(Directory('')); registerFallbackValue(ReleasePlatform.ios); registerFallbackValue(ReleaseStatus.draft); registerFallbackValue(_FakeRelease()); @@ -309,7 +310,8 @@ flutter: () => codePushClientWrapper.createIosReleaseArtifacts( appId: appId, releaseId: release.id, - ipaPath: any(named: 'ipaPath', that: endsWith('.ipa')), + xcarchivePath: + any(named: 'xcarchivePath', that: endsWith('.xcarchive')), runnerPath: any(named: 'runnerPath', that: endsWith('Runner.app')), ), ); diff --git a/packages/shorebird_cli/test/src/patch_diff_checker_test.dart b/packages/shorebird_cli/test/src/patch_diff_checker_test.dart index be564f949..987059444 100644 --- a/packages/shorebird_cli/test/src/patch_diff_checker_test.dart +++ b/packages/shorebird_cli/test/src/patch_diff_checker_test.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:http/http.dart' as http; import 'package:mason_logger/mason_logger.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:path/path.dart' as p; import 'package:scoped/scoped.dart'; import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart'; import 'package:shorebird_cli/src/archive_analysis/archive_differ.dart'; @@ -92,6 +93,27 @@ void main() { .thenReturn(nativeDiffPrettyString); }); + group('zipAndConfirmUnpatchableDiffsIfNecessary', () { + test('zips directory and forwards to confirmUnpatchableDiffsIfNecessary', + () async { + final tempDir = Directory.systemTemp.createTempSync(); + final localArtifactDirectory = Directory( + p.join(tempDir.path, 'artifact'), + )..createSync(); + + await runWithOverrides( + () => patchDiffChecker.zipAndConfirmUnpatchableDiffsIfNecessary( + localArtifactDirectory: localArtifactDirectory, + releaseArtifactUrl: releaseArtifactUrl, + archiveDiffer: archiveDiffer, + force: false, + ), + ); + + verify(() => archiveDiffer.changedFiles(any(), any())).called(1); + }); + }); + group('confirmUnpatchableDiffsIfNecessary', () { test('throws Exception when release artifact fails to download', () async {