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 c77c1d31c..b0384eb38 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 'ipa_differ.dart'; diff --git a/packages/shorebird_cli/lib/src/archive_analysis/ipa_differ.dart b/packages/shorebird_cli/lib/src/archive_analysis/ios_archive_differ.dart similarity index 94% rename from packages/shorebird_cli/lib/src/archive_analysis/ipa_differ.dart rename to packages/shorebird_cli/lib/src/archive_analysis/ios_archive_differ.dart index 97709c7a8..ca879848d 100644 --- a/packages/shorebird_cli/lib/src/archive_analysis/ipa_differ.dart +++ b/packages/shorebird_cli/lib/src/archive_analysis/ios_archive_differ.dart @@ -2,7 +2,7 @@ import 'package:path/path.dart' as p; import 'package:shorebird_cli/src/archive_analysis/archive_differ.dart'; import 'package:shorebird_cli/src/archive_analysis/file_set_diff.dart'; -/// Finds differences between two IPAs. +/// Finds differences between two IPAs or zipped Xcframeworks. /// /// Asset changes will be in the `Assets.car` file (which is a combination of /// the `.xcasset` catalogs in the Xcode project) and the `flutter_assets` @@ -12,7 +12,7 @@ import 'package:shorebird_cli/src/archive_analysis/file_set_diff.dart'; /// Flutter.framework or App.framework files. /// /// Dart changes will appear in the App.framework/App executable. -class IpaDiffer extends ArchiveDiffer { +class IosArchiveDiffer extends ArchiveDiffer { static const binaryFiles = { 'App.framework/App', 'Flutter.framework/Flutter', 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 aec4e3978..15f735384 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,10 @@ class PatchIosCommand extends ShorebirdCommand /// {@macro patch_ios_command} PatchIosCommand({ HashFunction? hashFn, - IpaDiffer? ipaDiffer, + IosArchiveDiffer? archiveDiffer, IpaReader? ipaReader, }) : _hashFn = hashFn ?? ((m) => sha256.convert(m).toString()), - _ipaDiffer = ipaDiffer ?? IpaDiffer(), + _archiveDiffer = archiveDiffer ?? IosArchiveDiffer(), _ipaReader = ipaReader ?? IpaReader() { argParser ..addOption( @@ -71,7 +71,7 @@ class PatchIosCommand extends ShorebirdCommand 'Publish new patches for a specific iOS release to Shorebird.'; final HashFunction _hashFn; - final IpaDiffer _ipaDiffer; + final IosArchiveDiffer _archiveDiffer; final IpaReader _ipaReader; @override @@ -235,7 +235,7 @@ Please re-run the release command for this version or create a new release.'''); await patchDiffChecker.confirmUnpatchableDiffsIfNecessary( localArtifact: File(ipaPath), releaseArtifactUrl: Uri.parse(releaseArtifact.url), - archiveDiffer: _ipaDiffer, + archiveDiffer: _archiveDiffer, force: force, ); 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 22479656c..801f26512 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,10 +1,13 @@ 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'; +import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; import 'package:scoped/scoped.dart'; +import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; import 'package:shorebird_cli/src/command.dart'; import 'package:shorebird_cli/src/config/shorebird_yaml.dart'; @@ -12,6 +15,7 @@ import 'package:shorebird_cli/src/doctor.dart'; import 'package:shorebird_cli/src/formatters/file_size_formatter.dart'; import 'package:shorebird_cli/src/ios.dart'; import 'package:shorebird_cli/src/logger.dart'; +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'; @@ -23,7 +27,9 @@ class PatchIosFrameworkCommand extends ShorebirdCommand with ShorebirdBuildMixin, ShorebirdArtifactMixin { PatchIosFrameworkCommand({ HashFunction? hashFn, - }) : _hashFn = hashFn ?? ((m) => sha256.convert(m).toString()) { + IosArchiveDiffer? archiveDiffer, + }) : _hashFn = hashFn ?? ((m) => sha256.convert(m).toString()), + _archiveDiffer = archiveDiffer ?? IosArchiveDiffer() { argParser ..addOption( 'release-version', @@ -46,6 +52,7 @@ of the iOS app that is using this module.''', } final HashFunction _hashFn; + final IosArchiveDiffer _archiveDiffer; @override String get name => 'ios-framework-alpha'; @@ -158,6 +165,36 @@ 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, + arch: 'xcframework', + platform: ReleasePlatform.ios, + ); + final shouldContinue = + await patchDiffChecker.confirmUnpatchableDiffsIfNecessary( + localArtifact: File(zippedFrameworkPath), + releaseArtifactUrl: Uri.parse(releaseArtifact.url), + archiveDiffer: _archiveDiffer, + force: force, + ); + + if (!shouldContinue) { + return ExitCode.success.code; + } + if (dryRun) { logger ..info('No issues detected.') diff --git a/packages/shorebird_cli/lib/src/commands/release/release_ios_framework_command.dart b/packages/shorebird_cli/lib/src/commands/release/release_ios_framework_command.dart index e45f1cde9..db64cd41e 100644 --- a/packages/shorebird_cli/lib/src/commands/release/release_ios_framework_command.dart +++ b/packages/shorebird_cli/lib/src/commands/release/release_ios_framework_command.dart @@ -1,5 +1,3 @@ -import 'dart:io' hide Platform; - import 'package:mason_logger/mason_logger.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; @@ -9,13 +7,14 @@ import 'package:shorebird_cli/src/config/config.dart'; import 'package:shorebird_cli/src/doctor.dart'; import 'package:shorebird_cli/src/ios.dart'; import 'package:shorebird_cli/src/logger.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_validator.dart'; import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; class ReleaseIosFrameworkCommand extends ShorebirdCommand - with ShorebirdBuildMixin { + with ShorebirdArtifactMixin, ShorebirdBuildMixin { ReleaseIosFrameworkCommand() { argParser ..addOption( @@ -119,16 +118,10 @@ ${summary.join('\n')} ); } - final iosBuildDir = p.join(Directory.current.path, 'build', 'ios'); - final frameworkDirectory = Directory( - p.join(iosBuildDir, 'framework', 'Release'), - ); - final xcframeworkPath = p.join(frameworkDirectory.path, 'App.xcframework'); - await codePushClientWrapper.createIosFrameworkReleaseArtifacts( appId: appId, releaseId: release.id, - appFrameworkPath: xcframeworkPath, + appFrameworkPath: getAppXcframeworkPath(), ); await codePushClientWrapper.updateReleaseStatus( @@ -138,7 +131,8 @@ ${summary.join('\n')} status: ReleaseStatus.active, ); - final relativeFrameworkDirectoryPath = p.relative(frameworkDirectory.path); + final relativeFrameworkDirectoryPath = + p.relative(getAppXcframeworkDirectory().path); logger ..success('\nāœ… Published Release!') ..info(''' diff --git a/packages/shorebird_cli/lib/src/shorebird_artifact_mixin.dart b/packages/shorebird_cli/lib/src/shorebird_artifact_mixin.dart index a0210b264..1370cb42d 100644 --- a/packages/shorebird_cli/lib/src/shorebird_artifact_mixin.dart +++ b/packages/shorebird_cli/lib/src/shorebird_artifact_mixin.dart @@ -96,6 +96,28 @@ mixin ShorebirdArtifactMixin on ShorebirdCommand { return ipaFiles.single.path; } + static const String appXcframeworkName = 'App.xcframework'; + + /// Returns the path to the App.xcframework generated by + /// `shorebird release ios-framework-alpha` or + /// `shorebird patch ios-framework-alpha`. + String getAppXcframeworkPath() { + return p.join(getAppXcframeworkDirectory().path, appXcframeworkName); + } + + /// Returns the [Directory] containing the App.xcframework generated by + /// `shorebird release ios-framework-alpha` or + /// `shorebird patch ios-framework-alpha`. + Directory getAppXcframeworkDirectory() => Directory( + p.join( + Directory.current.path, + 'build', + 'ios', + 'framework', + 'Release', + ), + ); + /// Finds the most recently-edited app.dill file in the .dart_tool directory. // TODO(bryanoltman): This is an enormous hack ā€“ we don't know that this is // the correct file. diff --git a/packages/shorebird_cli/test/fixtures/xcframeworks/README.md b/packages/shorebird_cli/test/fixtures/xcframeworks/README.md new file mode 100644 index 000000000..5d836d062 --- /dev/null +++ b/packages/shorebird_cli/test/fixtures/xcframeworks/README.md @@ -0,0 +1,7 @@ +The xcframework files in this folder were generated by building the stock Flutter counter app with `shorebird release ios-framework-alpha` and zipped with the `ditto` command. + +Files: + +- base.xcframework.zip is meant to represent an xcframework uploaded as part of a release. +- changed_asset.xcframework.zip is meant to represent an xcframework generated with a changed asset file. +- changed_dart.xcframework.zip is meant to represent an xcframework generated with a changed dart file. diff --git a/packages/shorebird_cli/test/fixtures/xcframeworks/base.xcframework.zip b/packages/shorebird_cli/test/fixtures/xcframeworks/base.xcframework.zip new file mode 100644 index 000000000..cc9f33982 Binary files /dev/null and b/packages/shorebird_cli/test/fixtures/xcframeworks/base.xcframework.zip differ diff --git a/packages/shorebird_cli/test/fixtures/xcframeworks/changed_asset.xcframework.zip b/packages/shorebird_cli/test/fixtures/xcframeworks/changed_asset.xcframework.zip new file mode 100644 index 000000000..1e30ebcd2 Binary files /dev/null and b/packages/shorebird_cli/test/fixtures/xcframeworks/changed_asset.xcframework.zip differ diff --git a/packages/shorebird_cli/test/fixtures/xcframeworks/changed_dart.xcframework.zip b/packages/shorebird_cli/test/fixtures/xcframeworks/changed_dart.xcframework.zip new file mode 100644 index 000000000..11f947ad6 Binary files /dev/null and b/packages/shorebird_cli/test/fixtures/xcframeworks/changed_dart.xcframework.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 new file mode 100644 index 000000000..5f3c5898e --- /dev/null +++ b/packages/shorebird_cli/test/src/archive_analysis/ios_archive_differ_test.dart @@ -0,0 +1,168 @@ +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 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 xcframeworkFixturesBasePath = p.join( + 'test', + 'fixtures', + 'xcframeworks', + ); + final baseXcframeworkPath = + p.join(xcframeworkFixturesBasePath, 'base.xcframework.zip'); + final changedAssetXcframeworkPath = + p.join(xcframeworkFixturesBasePath, 'changed_asset.xcframework.zip'); + final changedDartXcframeworkPath = + p.join(xcframeworkFixturesBasePath, 'changed_dart.xcframework.zip'); + + group(IosArchiveDiffer, () { + late IosArchiveDiffer differ; + + setUp(() { + differ = IosArchiveDiffer(); + }); + + group('ipa', () { + group('changedPaths', () { + test('finds no differences between the same ipa', () { + expect(differ.changedFiles(baseIpaPath, baseIpaPath), isEmpty); + }); + + test('finds differences between two different ipas', () { + expect( + differ.changedFiles(baseIpaPath, changedAssetIpaPath).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', + }, + ); + }); + }); + + group('changedFiles', () { + test('detects asset changes', () { + final fileSetDiff = + differ.changedFiles(baseIpaPath, changedAssetIpaPath); + expect(differ.assetsFileSetDiff(fileSetDiff), isNotEmpty); + expect(differ.dartFileSetDiff(fileSetDiff), isNotEmpty); + expect(differ.nativeFileSetDiff(fileSetDiff), isNotEmpty); + }); + + test('detects dart changes', () { + final fileSetDiff = + differ.changedFiles(baseIpaPath, changedDartIpaPath); + expect(differ.assetsFileSetDiff(fileSetDiff), isEmpty); + expect(differ.dartFileSetDiff(fileSetDiff), isNotEmpty); + expect(differ.nativeFileSetDiff(fileSetDiff), isNotEmpty); + }); + + test('detects swift changes', () { + final fileSetDiff = + differ.changedFiles(baseIpaPath, changedSwiftIpaPath); + expect(differ.assetsFileSetDiff(fileSetDiff), isEmpty); + expect(differ.dartFileSetDiff(fileSetDiff), isNotEmpty); + expect(differ.nativeFileSetDiff(fileSetDiff), isNotEmpty); + }); + }); + + group('containsPotentiallyBreakingAssetDiffs', () { + test('returns true if a file in flutter_assets has changed', () { + final fileSetDiff = differ.changedFiles( + baseIpaPath, + changedAssetIpaPath, + ); + expect( + differ.containsPotentiallyBreakingAssetDiffs(fileSetDiff), + isTrue, + ); + }); + + test('returns false if no files in flutter_assets has changed', () { + final fileSetDiff = differ.changedFiles( + baseIpaPath, + changedDartIpaPath, + ); + expect( + differ.containsPotentiallyBreakingAssetDiffs(fileSetDiff), + isFalse, + ); + }); + }); + + group('containsPotentiallyBreakingNativeDiffs', () { + test("always returns false, as we don't check for this yet", () { + final fileSetDiff = differ.changedFiles( + baseIpaPath, + changedSwiftIpaPath, + ); + expect( + differ.containsPotentiallyBreakingNativeDiffs(fileSetDiff), + isFalse, + ); + }); + }); + }); + + group('xcframework', () { + group('changedPaths', () { + test('finds no differences between the same zipped xcframeworks', () { + expect( + differ.changedFiles(baseXcframeworkPath, baseXcframeworkPath), + isEmpty, + ); + }); + + test('finds differences between two differed zipped xcframeworks', () { + expect( + differ + .changedFiles(baseXcframeworkPath, changedAssetXcframeworkPath) + .changedPaths, + { + 'ios-arm64_x86_64-simulator/App.framework/_CodeSignature/CodeResources', + 'ios-arm64_x86_64-simulator/App.framework/App', + 'ios-arm64_x86_64-simulator/App.framework/flutter_assets/assets/asset.json', + 'ios-arm64/App.framework/_CodeSignature/CodeResources', + 'ios-arm64/App.framework/App', + 'ios-arm64/App.framework/flutter_assets/assets/asset.json' + }, + ); + }); + }); + + group('changedFiles', () { + test('detects asset changes', () { + final fileSetDiff = differ.changedFiles( + baseXcframeworkPath, + changedAssetXcframeworkPath, + ); + expect(differ.assetsFileSetDiff(fileSetDiff), isNotEmpty); + expect(differ.dartFileSetDiff(fileSetDiff), isNotEmpty); + expect(differ.nativeFileSetDiff(fileSetDiff), isEmpty); + }); + + test('detects dart changes', () { + final fileSetDiff = differ.changedFiles( + baseXcframeworkPath, + changedDartXcframeworkPath, + ); + expect(differ.assetsFileSetDiff(fileSetDiff), isEmpty); + expect(differ.dartFileSetDiff(fileSetDiff), isNotEmpty); + expect(differ.nativeFileSetDiff(fileSetDiff), isEmpty); + }); + }); + }); + }); +} diff --git a/packages/shorebird_cli/test/src/archive_analysis/ipa_differ_test.dart b/packages/shorebird_cli/test/src/archive_analysis/ipa_differ_test.dart deleted file mode 100644 index 4055a8bf0..000000000 --- a/packages/shorebird_cli/test/src/archive_analysis/ipa_differ_test.dart +++ /dev/null @@ -1,105 +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 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'); - - late IpaDiffer differ; - - setUp(() { - differ = IpaDiffer(); - }); - - group(IpaDiffer, () { - group('changedPaths', () { - test('finds no differences between the same ipa', () { - expect(differ.changedFiles(baseIpaPath, baseIpaPath), isEmpty); - }); - - test('finds differences between two different ipas', () { - expect( - differ.changedFiles(baseIpaPath, changedAssetIpaPath).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', - }, - ); - }); - }); - - group('changedFiles', () { - test('detects asset changes', () { - final fileSetDiff = - differ.changedFiles(baseIpaPath, changedAssetIpaPath); - expect(differ.assetsFileSetDiff(fileSetDiff), isNotEmpty); - expect(differ.dartFileSetDiff(fileSetDiff), isNotEmpty); - expect(differ.nativeFileSetDiff(fileSetDiff), isNotEmpty); - }); - - test('detects dart changes', () { - final fileSetDiff = - differ.changedFiles(baseIpaPath, changedDartIpaPath); - expect(differ.assetsFileSetDiff(fileSetDiff), isEmpty); - expect(differ.dartFileSetDiff(fileSetDiff), isNotEmpty); - expect(differ.nativeFileSetDiff(fileSetDiff), isNotEmpty); - }); - - test('detects swift changes', () { - final fileSetDiff = - differ.changedFiles(baseIpaPath, changedSwiftIpaPath); - expect(differ.assetsFileSetDiff(fileSetDiff), isEmpty); - expect(differ.dartFileSetDiff(fileSetDiff), isNotEmpty); - expect(differ.nativeFileSetDiff(fileSetDiff), isNotEmpty); - }); - }); - - group('containsPotentiallyBreakingAssetDiffs', () { - test('returns true if a file in flutter_assets has changed', () { - final fileSetDiff = differ.changedFiles( - baseIpaPath, - changedAssetIpaPath, - ); - expect( - differ.containsPotentiallyBreakingAssetDiffs(fileSetDiff), - isTrue, - ); - }); - - test('returns false if no files in flutter_assets has changed', () { - final fileSetDiff = differ.changedFiles( - baseIpaPath, - changedDartIpaPath, - ); - expect( - differ.containsPotentiallyBreakingAssetDiffs(fileSetDiff), - isFalse, - ); - }); - }); - - group('containsPotentiallyBreakingNativeDiffs', () { - test("always returns false, as we don't check for this yet", () { - final fileSetDiff = differ.changedFiles( - baseIpaPath, - changedSwiftIpaPath, - ); - expect( - differ.containsPotentiallyBreakingNativeDiffs(fileSetDiff), - isFalse, - ); - }); - }); - }); -} 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 3247aafa4..ae3d4dd1e 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 @@ -39,7 +39,7 @@ class _MockDoctor extends Mock implements Doctor {} class _MockIpa extends Mock implements Ipa {} -class _MockIpaDiffer extends Mock implements IpaDiffer {} +class _MockIpaDiffer extends Mock implements IosArchiveDiffer {} class _MockIpaReader extends Mock implements IpaReader {} @@ -130,7 +130,7 @@ flutter: late File genSnapshotFile; late Doctor doctor; late Ipa ipa; - late IpaDiffer ipaDiffer; + late IosArchiveDiffer archiveDiffer; late IpaReader ipaReader; late Progress progress; late Logger logger; @@ -232,7 +232,7 @@ flutter: ), ); ipa = _MockIpa(); - ipaDiffer = _MockIpaDiffer(); + archiveDiffer = _MockIpaDiffer(); ipaReader = _MockIpaReader(); progress = _MockProgress(); logger = _MockLogger(); @@ -328,13 +328,14 @@ flutter: () => patchDiffChecker.confirmUnpatchableDiffsIfNecessary( localArtifact: any(named: 'localArtifact'), releaseArtifactUrl: any(named: 'releaseArtifactUrl'), - archiveDiffer: ipaDiffer, + archiveDiffer: archiveDiffer, force: any(named: 'force'), ), ).thenAnswer((_) async => true); command = runWithOverrides( - () => PatchIosCommand(ipaDiffer: ipaDiffer, ipaReader: ipaReader), + () => + PatchIosCommand(archiveDiffer: archiveDiffer, ipaReader: ipaReader), )..testArgResults = argResults; }); @@ -774,7 +775,7 @@ Please re-run the release command for this version or create a new release.'''), () => patchDiffChecker.confirmUnpatchableDiffsIfNecessary( localArtifact: any(named: 'localArtifact'), releaseArtifactUrl: any(named: 'releaseArtifactUrl'), - archiveDiffer: ipaDiffer, + archiveDiffer: archiveDiffer, force: any(named: 'force'), ), ).thenAnswer((_) async => false); @@ -791,7 +792,7 @@ Please re-run the release command for this version or create a new release.'''), () => patchDiffChecker.confirmUnpatchableDiffsIfNecessary( localArtifact: any(named: 'localArtifact'), releaseArtifactUrl: Uri.parse(ipaArtifact.url), - archiveDiffer: ipaDiffer, + archiveDiffer: archiveDiffer, force: false, ), ).called(1); 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 dbfca1971..3ddee2ebb 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 @@ -6,12 +6,14 @@ import 'package:mocktail/mocktail.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.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/patch/patch.dart'; import 'package:shorebird_cli/src/config/config.dart'; import 'package:shorebird_cli/src/doctor.dart'; import 'package:shorebird_cli/src/logger.dart'; +import 'package:shorebird_cli/src/patch_diff_checker.dart'; import 'package:shorebird_cli/src/platform.dart'; import 'package:shorebird_cli/src/process.dart'; import 'package:shorebird_cli/src/shorebird_env.dart'; @@ -30,8 +32,12 @@ class _MockCodePushClientWrapper extends Mock class _MockDoctor extends Mock implements Doctor {} +class _MockIosArchiveDiffer extends Mock implements IosArchiveDiffer {} + class _MockLogger extends Mock implements Logger {} +class _MockPatchDiffChecker extends Mock implements PatchDiffChecker {} + class _MockPlatform extends Mock implements Platform {} class _MockProgress extends Mock implements Progress {} @@ -72,6 +78,16 @@ flutter: assets: - shorebird.yaml'''; const appMetadata = AppMetadata(appId: appId, displayName: appDisplayName); + const arch = 'aarch64'; + const xcframeworkArtifact = ReleaseArtifact( + id: 0, + releaseId: 0, + arch: arch, + platform: ReleasePlatform.ios, + hash: '#', + size: 42, + url: 'https://example.com/release.xcframework', + ); const release = Release( id: 0, appId: appId, @@ -87,6 +103,8 @@ flutter: late Directory flutterDirectory; late File genSnapshotFile; late Doctor doctor; + late IosArchiveDiffer archiveDiffer; + late PatchDiffChecker patchDiffChecker; late Platform platform; late Auth auth; late Progress progress; @@ -108,6 +126,7 @@ flutter: codePushClientWrapperRef.overrideWith(() => codePushClientWrapper), doctorRef.overrideWith(() => doctor), loggerRef.overrideWith(() => logger), + patchDiffCheckerRef.overrideWith(() => patchDiffChecker), platformRef.overrideWith(() => platform), processRef.overrideWith(() => shorebirdProcess), shorebirdEnvRef.overrideWith(() => shorebirdEnv), @@ -136,6 +155,18 @@ flutter: File(p.join(dir.path, 'build', elfAotSnapshotFileName)).createSync( recursive: true, ); + Directory( + p.join( + dir.path, + 'build', + 'ios', + 'framework', + 'Release', + 'App.xcframework', + ), + ).createSync( + recursive: true, + ); } Directory setUpTempDir() { @@ -150,13 +181,17 @@ flutter: } setUpAll(() { + registerFallbackValue(File('')); registerFallbackValue(ReleasePlatform.ios); + registerFallbackValue(Uri.parse('https://example.com')); }); setUp(() { argResults = _MockArgResults(); + archiveDiffer = _MockIosArchiveDiffer(); codePushClientWrapper = _MockCodePushClientWrapper(); doctor = _MockDoctor(); + patchDiffChecker = _MockPatchDiffChecker(); platform = _MockPlatform(); shorebirdRoot = Directory.systemTemp.createTempSync(); flutterDirectory = Directory( @@ -224,6 +259,14 @@ flutter: when( () => codePushClientWrapper.getReleases(appId: any(named: 'appId')), ).thenAnswer((_) async => [release]); + when( + () => codePushClientWrapper.getReleaseArtifact( + appId: any(named: 'appId'), + releaseId: any(named: 'releaseId'), + arch: any(named: 'arch'), + platform: any(named: 'platform'), + ), + ).thenAnswer((_) async => xcframeworkArtifact); when( () => codePushClientWrapper.publishPatch( appId: any(named: 'appId'), @@ -246,9 +289,18 @@ flutter: supportedOperatingSystems: any(named: 'supportedOperatingSystems'), ), ).thenAnswer((_) async {}); + when( + () => patchDiffChecker.confirmUnpatchableDiffsIfNecessary( + localArtifact: any(named: 'localArtifact'), + releaseArtifactUrl: any(named: 'releaseArtifactUrl'), + archiveDiffer: archiveDiffer, + force: any(named: 'force'), + ), + ).thenAnswer((_) async => true); - command = runWithOverrides(PatchIosFrameworkCommand.new) - ..testArgResults = argResults; + command = runWithOverrides( + () => PatchIosFrameworkCommand(archiveDiffer: archiveDiffer), + )..testArgResults = argResults; }); test('has a description', () { @@ -560,6 +612,44 @@ Please re-run the release command for this version or create a new release.'''), expect(exitCode, ExitCode.software.code); }); + test('exits if confirmUnpatchableDiffsIfNecessary returns false', () async { + when(() => argResults['force']).thenReturn(false); + when( + () => patchDiffChecker.confirmUnpatchableDiffsIfNecessary( + localArtifact: any(named: 'localArtifact'), + releaseArtifactUrl: any(named: 'releaseArtifactUrl'), + archiveDiffer: archiveDiffer, + force: any(named: 'force'), + ), + ).thenAnswer((_) async => false); + final tempDir = setUpTempDir(); + setUpTempArtifacts(tempDir); + + final exitCode = await IOOverrides.runZoned( + () => runWithOverrides(command.run), + getCurrentDirectory: () => tempDir, + ); + + expect(exitCode, equals(ExitCode.success.code)); + verify( + () => patchDiffChecker.confirmUnpatchableDiffsIfNecessary( + localArtifact: any(named: 'localArtifact'), + releaseArtifactUrl: Uri.parse(xcframeworkArtifact.url), + archiveDiffer: archiveDiffer, + force: false, + ), + ).called(1); + verifyNever( + () => codePushClientWrapper.publishPatch( + appId: any(named: 'appId'), + releaseId: any(named: 'releaseId'), + platform: any(named: 'platform'), + channelName: any(named: 'channelName'), + patchArtifactBundles: any(named: 'patchArtifactBundles'), + ), + ); + }); + test('does not create patch on --dry-run', () async { when(() => argResults['dry-run']).thenReturn(true); final tempDir = setUpTempDir();