feat(shorebird_cli): add support for native and dart diffing to iOS (#1065)

This commit is contained in:
Bryan Oltman
2023-08-09 15:35:12 -04:00
committed by GitHub
parent 14782d15ca
commit ad2aab62f3
3 changed files with 274 additions and 146 deletions
@@ -76,12 +76,12 @@ abstract class ArchiveDiffer {
/// archives at the two provided paths.
FileSetDiff changedFiles(String oldArchivePath, String newArchivePath) =>
FileSetDiff.fromPathHashes(
oldPathHashes: _fileHashes(File(oldArchivePath)),
newPathHashes: _fileHashes(File(newArchivePath)),
oldPathHashes: fileHashes(File(oldArchivePath)),
newPathHashes: fileHashes(File(newArchivePath)),
);
PathHashes _fileHashes(File aar) {
final zipDirectory = ZipDirectory.read(InputFileStream(aar.path));
PathHashes fileHashes(File archive) {
final zipDirectory = ZipDirectory.read(InputFileStream(archive.path));
return {
for (final file in zipDirectory.fileHeaders)
// Zip files contain an (optional) crc32 checksum for a file. IPAs and
@@ -1,3 +1,7 @@
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:crypto/crypto.dart';
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';
@@ -13,27 +17,85 @@ import 'package:shorebird_cli/src/archive_analysis/file_set_diff.dart';
///
/// Dart changes will appear in the App.framework/App executable.
class IosArchiveDiffer extends ArchiveDiffer {
String _hash(List<int> bytes) => sha256.convert(bytes).toString();
static const binaryFiles = {
'App.framework/App',
'Flutter.framework/Flutter',
};
static RegExp appRegex = RegExp(r'^Payload/[\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
/// binaries in the archives before computing the diff.
@override
FileSetDiff changedFiles(String oldArchivePath, String newArchivePath) {
final oldPathHashes = fileHashes(File(oldArchivePath));
final newPathHashes = fileHashes(File(newArchivePath));
_updateToUnsignedHashes(
archivePath: oldArchivePath,
pathHashes: oldPathHashes,
);
_updateToUnsignedHashes(
archivePath: newArchivePath,
pathHashes: newPathHashes,
);
return FileSetDiff.fromPathHashes(
oldPathHashes: oldPathHashes,
newPathHashes: newPathHashes,
);
}
void _updateToUnsignedHashes({
required String archivePath,
required PathHashes pathHashes,
}) {
for (final file in _filesToUnsign(archivePath)) {
pathHashes[file.name] = _unsignedFileHash(file);
}
}
List<ArchiveFile> _filesToUnsign(String archivePath) {
return ZipDecoder()
.decodeBuffer(InputFileStream(archivePath))
.files
.where((file) => file.isFile)
.where(
(file) =>
file.name.endsWith('App.framework/App') ||
file.name.endsWith('Flutter.framework/Flutter') ||
appRegex.hasMatch(file.name),
)
.toList();
}
String _unsignedFileHash(ArchiveFile file) {
final tempDir = Directory.systemTemp.createTempSync();
final outPath = p.join(tempDir.path, file.name);
final outputStream = OutputFileStream(outPath);
file.writeContent(outputStream);
outputStream.close();
if (Platform.isMacOS) {
// coverage:ignore-start
Process.runSync('codesign', ['--remove-signature', outPath]);
// coverage:ignore-end
}
final outFile = File(outPath);
final hash = _hash(outFile.readAsBytesSync());
return hash;
}
@override
bool containsPotentiallyBreakingAssetDiffs(FileSetDiff fileSetDiff) =>
assetsFileSetDiff(fileSetDiff).isNotEmpty;
@override
bool containsPotentiallyBreakingNativeDiffs(FileSetDiff fileSetDiff) {
// Because the mach-o binaries are signed (we believe with in expiration
// date), they will always have different hashes, even if the code used to
// generate them is identical.
//
// TODO(bryanoltman): support mach-o binary diffing.
// We can do this using the `codesign --remove-signature` command, but this
// is slow and requires a temporary directory to store the unsigned binary.
return false;
}
bool containsPotentiallyBreakingNativeDiffs(FileSetDiff fileSetDiff) =>
nativeFileSetDiff(fileSetDiff).isNotEmpty;
@override
bool isAssetFilePath(String filePath) {
@@ -1,5 +1,6 @@
import 'package:path/path.dart' as p;
import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart';
import 'package:shorebird_cli/src/platform.dart';
import 'package:test/test.dart';
void main() {
@@ -21,148 +22,213 @@ void main() {
final changedDartXcframeworkPath =
p.join(xcframeworkFixturesBasePath, 'changed_dart.xcframework.zip');
group(IosArchiveDiffer, () {
late IosArchiveDiffer differ;
group(
IosArchiveDiffer,
() {
late IosArchiveDiffer differ;
setUp(() {
differ = IosArchiveDiffer();
});
setUp(() {
differ = IosArchiveDiffer();
});
group('ipa', () {
group('changedPaths', () {
test('finds no differences between the same ipa', () {
expect(differ.changedFiles(baseIpaPath, baseIpaPath), isEmpty);
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', () {
final fileSetDiff = differ.changedFiles(
baseIpaPath,
changedAssetIpaPath,
);
if (platform.isMacOS) {
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'
},
);
} 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'
},
);
}
});
});
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),
platform.isMacOS ? isEmpty : isNotEmpty,
);
expect(
differ.nativeFileSetDiff(fileSetDiff),
platform.isMacOS ? isEmpty : 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),
platform.isMacOS ? isEmpty : isNotEmpty,
);
});
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.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('returns true if Swift files have been changed', () {
final fileSetDiff = differ.changedFiles(
baseIpaPath,
changedSwiftIpaPath,
);
expect(
differ.containsPotentiallyBreakingNativeDiffs(fileSetDiff),
isTrue,
);
});
test('returns false if Swift files have not been changed', () {
final fileSetDiff = differ.changedFiles(
baseIpaPath,
changedAssetIpaPath,
);
expect(
differ.containsPotentiallyBreakingNativeDiffs(fileSetDiff),
platform.isMacOS ? isFalse : isTrue,
);
});
});
});
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);
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',
() {
final fileSetDiff = differ.changedFiles(
baseXcframeworkPath,
changedAssetXcframeworkPath,
);
if (platform.isMacOS) {
expect(
fileSetDiff.changedPaths,
{
'ios-arm64_x86_64-simulator/App.framework/_CodeSignature/CodeResources',
'ios-arm64_x86_64-simulator/App.framework/flutter_assets/assets/asset.json',
'ios-arm64/App.framework/_CodeSignature/CodeResources',
'ios-arm64/App.framework/flutter_assets/assets/asset.json'
},
);
} else {
expect(
fileSetDiff.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',
},
);
}
});
});
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);
});
group('changedFiles', () {
test('detects asset changes', () {
final fileSetDiff = differ.changedFiles(
baseXcframeworkPath,
changedAssetXcframeworkPath,
);
expect(differ.assetsFileSetDiff(fileSetDiff), isNotEmpty);
expect(
differ.dartFileSetDiff(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), isNotEmpty);
expect(differ.nativeFileSetDiff(fileSetDiff), isNotEmpty);
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);
});
});
});
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);
});
});
});
});
},
);
}