Skip to content

Commit

Permalink
feat(shorebird_cli): support asset diffing in iOS frameworks (#1046)
Browse files Browse the repository at this point in the history
  • Loading branch information
bryanoltman authored Aug 7, 2023
1 parent 1ec6af9 commit 4e9b202
Show file tree
Hide file tree
Showing 14 changed files with 347 additions and 133 deletions.
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
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';
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';
Expand All @@ -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',
Expand All @@ -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';
Expand Down Expand Up @@ -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.')
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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('''
Expand Down
22 changes: 22 additions & 0 deletions packages/shorebird_cli/lib/src/shorebird_artifact_mixin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions packages/shorebird_cli/test/fixtures/xcframeworks/README.md
Original file line number Diff line number Diff line change
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
});
}
Loading

0 comments on commit 4e9b202

Please sign in to comment.