Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(shorebird_cli): support asset diffing in iOS frameworks #1046

Merged
merged 5 commits into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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