feat(shorebird_cli): add aar differ (#584)

This commit is contained in:
Bryan Oltman
2023-06-02 15:07:24 -04:00
committed by GitHub
parent e58c9a263f
commit 60b439c6e1
18 changed files with 250 additions and 132 deletions
@@ -1 +0,0 @@
export 'aab_differ.dart';
@@ -1,99 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:path/path.dart' as p;
import 'package:shorebird_cli/src/aab/mf_reader.dart';
/// Types of code changes that we care about.
enum AabDifferences {
dart,
native,
assets,
}
/// Finds differences between two AABs.
///
/// Types of changes we care about:
/// - Dart code changes
/// - libapp.so will be different
/// - Java/Kotlin code changes
/// - .dex files will be different
/// - Assets
/// - **/assets/** will be different
/// - AssetManifest.json will have changed if assets have been added or
/// removed
///
/// Changes we don't care about:
/// - Anything in META-INF
/// - BUNDLE-METADATA/com.android.tools.build.libraries/dependencies.pb
/// - This seems to change with every build, regardless of whether any code
/// or assets were changed.
///
/// See https://developer.android.com/guide/app-bundle/app-bundle-format for
/// reference.
class AabDiffer {
/// Returns a set of file paths whose hashes differ between the AABs at the
/// provided paths.
Set<String> aabChangedFiles(String aabPath1, String aabPath2) {
final mfContents1 = _metaInfMfContent(File(aabPath1));
final mfContents2 = _metaInfMfContent(File(aabPath2));
final mfEntries1 = MfReader.parse(mfContents1).toSet();
final mfEntries2 = MfReader.parse(mfContents2).toSet();
return mfEntries1.difference(mfEntries2).map((entry) => entry.name).toSet();
}
/// Returns a set of difference types detected between the aabs at [aabPath1]
/// and [aabPath2].
Set<AabDifferences> aabContentDifferences(String aabPath1, String aabPath2) {
final fileDifferences = aabChangedFiles(aabPath1, aabPath2);
final differences = <AabDifferences>{};
if (_hasAssetChanges(fileDifferences)) {
differences.add(AabDifferences.assets);
}
if (_hasDartChanges(fileDifferences)) {
differences.add(AabDifferences.dart);
}
if (_hasNativeChanges(fileDifferences)) {
differences.add(AabDifferences.native);
}
return differences;
}
/// Reads the contents of META-INF/MANIFEST.MF from an AAB.
///
/// This file contains a list of file paths and their SHA-256 hashes.
String _metaInfMfContent(File aab) {
final inputStream = InputFileStream(aab.path);
final archive = ZipDecoder().decodeBuffer(inputStream);
return utf8.decode(
archive.files
.firstWhere((file) => file.name == 'META-INF/MANIFEST.MF')
.content as List<int>,
);
}
/// Whether any changed files correspond to a change in assets.
bool _hasAssetChanges(Set<String> paths) {
const assetDirNames = ['assets', 'res'];
const assetFileNames = ['AssetManifest.json'];
return paths.any(
(path) =>
p.split(path).any((component) => assetDirNames.contains(component)) ||
assetFileNames.contains(p.basename(path)),
);
}
/// Whether any changed files correspond to a change in Dart code.
bool _hasDartChanges(Set<String> paths) {
const dartFileNames = ['libapp.so', 'libflutter.so'];
return paths.any((path) => dartFileNames.contains(p.basename(path)));
}
/// Whether changed files correspond to a change in Java or Kotlin code.
bool _hasNativeChanges(Set<String> path) {
return path.any((path) => p.extension(path) == '.dex');
}
}
@@ -0,0 +1,52 @@
import 'dart:convert';
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:shorebird_cli/src/archive_analysis/android_archive_differ.dart';
import 'package:shorebird_cli/src/archive_analysis/mf_reader.dart';
/// Finds differences between two AABs.
///
/// Types of changes we care about:
/// - Dart code changes
/// - libapp.so will be different
/// - Java/Kotlin code changes
/// - .dex files will be different
/// - Assets
/// - **/assets/** will be different
/// - AssetManifest.json will have changed if assets have been added or
/// removed
///
/// Changes we don't care about:
/// - Anything in META-INF
/// - BUNDLE-METADATA/com.android.tools.build.libraries/dependencies.pb
/// - This seems to change with every build, regardless of whether any code
/// or assets were changed.
///
/// See https://developer.android.com/guide/app-bundle/app-bundle-format for
/// reference.
class AabDiffer extends AndroidArchiveDiffer {
/// Returns a set of file paths whose hashes differ between the AABs at the
/// provided paths.
@override
Set<String> changedFiles(String aabPath1, String aabPath2) {
final mfContents1 = _metaInfMfContent(File(aabPath1));
final mfContents2 = _metaInfMfContent(File(aabPath2));
final mfEntries1 = MfReader.parse(mfContents1).toSet();
final mfEntries2 = MfReader.parse(mfContents2).toSet();
return mfEntries1.difference(mfEntries2).map((entry) => entry.name).toSet();
}
/// Reads the contents of META-INF/MANIFEST.MF from an AAB.
///
/// This file contains a list of file paths and their SHA-256 hashes.
String _metaInfMfContent(File aab) {
final inputStream = InputFileStream(aab.path);
final archive = ZipDecoder().decodeBuffer(inputStream);
return utf8.decode(
archive.files
.firstWhere((file) => file.name == 'META-INF/MANIFEST.MF')
.content as List<int>,
);
}
}
@@ -0,0 +1,37 @@
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:crypto/crypto.dart';
import 'package:shorebird_cli/src/archive_analysis/android_archive_differ.dart';
/// Finds differences between two AABs.
///
/// Types of changes we care about:
/// - Dart code changes
/// - libapp.so will be different
/// - Assets
/// - **/assets/** will be different
/// - AssetManifest.json will have changed if assets have been added or
/// removed
///
/// See
/// https://developer.android.com/studio/projects/android-library.html#aar-contents
/// for reference. Note that .aars produced by Flutter modules do not contain
/// .jar files, so only asset and dart changes are possible.
class AarDiffer extends AndroidArchiveDiffer {
String _hash(List<int> bytes) => sha256.convert(bytes).toString();
@override
Set<String> changedFiles(String archivePath1, String archivePath2) =>
_fileHashes(File(archivePath1))
.difference(_fileHashes(File(archivePath2)))
.map((pair) => pair.$1)
.toSet();
Set<(String, String)> _fileHashes(File aar) => ZipDecoder()
.decodeBuffer(InputFileStream(aar.path))
.files
.where((file) => file.isFile)
.map((file) => (file.name, _hash(file.content as List<int>)))
.toSet();
}
@@ -0,0 +1,49 @@
import 'package:path/path.dart' as p;
import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart';
abstract class AndroidArchiveDiffer {
/// Files that have changed between the archives at the two provided paths.
Set<String> changedFiles(String archivePath1, String archivePath2);
/// Whether any changed files correspond to a change in assets.
bool hasAssetChanges(Set<String> paths) {
const assetDirNames = ['assets', 'res'];
const assetFileNames = ['AssetManifest.json'];
return paths.any(
(path) =>
p.split(path).any((component) => assetDirNames.contains(component)) ||
assetFileNames.contains(p.basename(path)),
);
}
/// Whether any changed files correspond to a change in Dart code.
bool hasDartChanges(Set<String> paths) {
const dartFileNames = ['libapp.so', 'libflutter.so'];
return paths.any((path) => dartFileNames.contains(p.basename(path)));
}
/// Whether changed files correspond to a change in Java or Kotlin code.
bool hasNativeChanges(Set<String> path) {
return path.any((path) => p.extension(path) == '.dex');
}
/// The types of differences between the archives at the two provided paths.
Set<ArchiveDifferences> contentDifferences(
String archivePath1,
String archivePath2,
) {
final changedFilePaths = changedFiles(archivePath1, archivePath2);
final differences = <ArchiveDifferences>{};
if (hasDartChanges(changedFilePaths)) {
differences.add(ArchiveDifferences.dart);
}
if (hasAssetChanges(changedFilePaths)) {
differences.add(ArchiveDifferences.assets);
}
if (hasNativeChanges(changedFilePaths)) {
differences.add(ArchiveDifferences.native);
}
return differences;
}
}
@@ -0,0 +1,3 @@
export 'aab_differ.dart';
export 'aar_differ.dart';
export 'archive_differences.dart';
@@ -0,0 +1,6 @@
/// Types of code changes that we care about.
enum ArchiveDifferences {
dart,
native,
assets,
}
@@ -5,7 +5,7 @@ import 'package:crypto/crypto.dart';
import 'package:http/http.dart' as http;
import 'package:mason_logger/mason_logger.dart';
import 'package:path/path.dart' as p;
import 'package:shorebird_cli/src/aab/aab.dart';
import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart';
import 'package:shorebird_cli/src/command.dart';
import 'package:shorebird_cli/src/config/shorebird_yaml.dart';
import 'package:shorebird_cli/src/formatters/formatters.dart';
@@ -328,15 +328,15 @@ https://github.com/shorebirdtech/shorebird/issues/472
downloadReleaseArtifactProgress.complete();
final contentDiffs = releaseAabPath == null
? <AabDifferences>{}
: _aabDiffer.aabContentDifferences(
? <ArchiveDifferences>{}
: _aabDiffer.contentDifferences(
releaseAabPath,
bundlePath,
);
logger.detail('aab content differences: $contentDiffs');
if (contentDiffs.contains(AabDifferences.native)) {
if (contentDiffs.contains(ArchiveDifferences.native)) {
logger
..err(
'''The Android App Bundle appears to contain Kotlin or Java changes, which cannot be applied via a patch.''',
@@ -352,7 +352,7 @@ If you believe you're seeing this in error, please reach out to us for support a
return ExitCode.software.code;
}
if (contentDiffs.contains(AabDifferences.assets)) {
if (contentDiffs.contains(ArchiveDifferences.assets)) {
logger.info(
yellow.wrap(
'''⚠️ The Android App Bundle contains asset changes, which will not be included in the patch.''',
+10
View File
@@ -0,0 +1,10 @@
The aab files in this folder were generated by building a flutter module with
`flutter build aar --no-debug --no-profile`.
Some of their contents has been removed to reduce the size of the files. Most notably, x86_64 and armeabi-v7a libapp.so files have been removed.
Files:
- base.aar is meant to represent an aar uploaded as part of a release.
- changed_dart.aar was built from the same code as base.aar with only Dart changes (i.e., no asset changes)
- changed_asset.aar was built from the same code as base.aar with only asset changes (i.e., no Dart changes)
- changed_dart_and_asset.aar was built from the same code as base.aar with both Dart and asset changes
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,5 @@
import 'package:path/path.dart' as p;
import 'package:shorebird_cli/src/aab/aab.dart';
import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart';
import 'package:test/test.dart';
void main() {
@@ -20,14 +20,14 @@ void main() {
differ = AabDiffer();
});
group('aabFileDifferences', () {
group('changedFiles', () {
test('finds no differences between the same aab', () {
expect(differ.aabChangedFiles(baseAabPath, baseAabPath), isEmpty);
expect(differ.changedFiles(baseAabPath, baseAabPath), isEmpty);
});
test('finds differences between the two different aabs', () {
test('finds differences between two different aabs', () {
expect(
differ.aabChangedFiles(baseAabPath, changedDartAabPath).toSet(),
differ.changedFiles(baseAabPath, changedDartAabPath).toSet(),
{
'BUNDLE-METADATA/com.android.tools.build.libraries/dependencies.pb',
'base/lib/arm64-v8a/libapp.so',
@@ -38,38 +38,38 @@ void main() {
});
});
group('aabContentDifferences', () {
group('contentDifferences', () {
test('detects no differences between the same aab', () {
expect(differ.aabContentDifferences(baseAabPath, baseAabPath), isEmpty);
expect(differ.contentDifferences(baseAabPath, baseAabPath), isEmpty);
});
test('detects asset changes', () {
expect(
differ.aabContentDifferences(baseAabPath, changedAssetAabPath),
{AabDifferences.assets},
differ.contentDifferences(baseAabPath, changedAssetAabPath),
{ArchiveDifferences.assets},
);
});
test('detects kotlin changes', () {
expect(
differ.aabContentDifferences(baseAabPath, changedKotlinAabPath),
{AabDifferences.native},
differ.contentDifferences(baseAabPath, changedKotlinAabPath),
{ArchiveDifferences.native},
);
});
test('detects dart changes', () {
expect(
differ.aabContentDifferences(baseAabPath, changedDartAabPath),
{AabDifferences.dart},
differ.contentDifferences(baseAabPath, changedDartAabPath),
{ArchiveDifferences.dart},
);
});
test('detects dart and asset changes', () {
expect(
differ.aabContentDifferences(baseAabPath, changedDartAndAssetAabPath),
differ.contentDifferences(baseAabPath, changedDartAndAssetAabPath),
{
AabDifferences.assets,
AabDifferences.dart,
ArchiveDifferences.assets,
ArchiveDifferences.dart,
},
);
});
@@ -0,0 +1,61 @@
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 aarFixturesBasePath = p.join('test', 'fixtures', 'aars');
final baseAarPath = p.join(aarFixturesBasePath, 'base.aar');
final changedAssetAarPath = p.join(aarFixturesBasePath, 'changed_asset.aar');
final changedDartAarPath = p.join(aarFixturesBasePath, 'changed_dart.aar');
final changedDartAndAssetAarPath =
p.join(aarFixturesBasePath, 'changed_dart_and_asset.aar');
late AarDiffer differ;
setUp(() {
differ = AarDiffer();
});
group('changedFiles', () {
test('finds no differences between the same aar', () {
expect(differ.changedFiles(baseAarPath, baseAarPath), isEmpty);
});
test('finds differences between two different aars', () {
expect(
differ.changedFiles(baseAarPath, changedDartAarPath).toSet(),
{'jni/arm64-v8a/libapp.so'},
);
});
});
group('contentDifferences', () {
test('detects no differences between the same aar', () {
expect(differ.contentDifferences(baseAarPath, baseAarPath), isEmpty);
});
test('detects asset changes', () {
expect(
differ.contentDifferences(baseAarPath, changedAssetAarPath),
{ArchiveDifferences.assets},
);
});
test('detects dart changes', () {
expect(
differ.contentDifferences(baseAarPath, changedDartAarPath),
{ArchiveDifferences.dart},
);
});
test('detects dart and asset changes', () {
expect(
differ.contentDifferences(baseAarPath, changedDartAndAssetAarPath),
{
ArchiveDifferences.assets,
ArchiveDifferences.dart,
},
);
});
});
}
@@ -1,7 +1,7 @@
import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:shorebird_cli/src/aab/mf_reader.dart';
import 'package:shorebird_cli/src/archive_analysis/mf_reader.dart';
import 'package:test/test.dart';
void main() {
@@ -6,7 +6,7 @@ import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
import 'package:path/path.dart' as p;
import 'package:platform/platform.dart';
import 'package:shorebird_cli/src/aab/aab.dart';
import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart';
import 'package:shorebird_cli/src/auth/auth.dart';
import 'package:shorebird_cli/src/cache.dart' show Cache;
import 'package:shorebird_cli/src/commands/patch/patch_android_command.dart';
@@ -250,7 +250,7 @@ flutter:
: releaseVersionNameProcessResult;
});
when(() => aabDiffer.aabContentDifferences(any(), any())).thenReturn({});
when(() => aabDiffer.contentDifferences(any(), any())).thenReturn({});
when(() => argResults.rest).thenReturn([]);
when(() => argResults['arch']).thenReturn(arch);
when(() => argResults['channel']).thenReturn(channelName);
@@ -661,8 +661,8 @@ Please create a release using "shorebird release" and try again.
});
test('throws error when Java/Kotlin code changes are detected', () async {
when(() => aabDiffer.aabContentDifferences(any(), any())).thenReturn(
{AabDifferences.native},
when(() => aabDiffer.contentDifferences(any(), any())).thenReturn(
{ArchiveDifferences.native},
);
final tempDir = setUpTempDir();
@@ -681,8 +681,8 @@ Please create a release using "shorebird release" and try again.
});
test('prompts user to continue when asset changes are detected', () async {
when(() => aabDiffer.aabContentDifferences(any(), any())).thenReturn(
{AabDifferences.assets},
when(() => aabDiffer.contentDifferences(any(), any())).thenReturn(
{ArchiveDifferences.assets},
);
final tempDir = setUpTempDir();
@@ -708,8 +708,8 @@ Please create a release using "shorebird release" and try again.
test(
'''does not warn user of asset or code changes if only dart changes are detected''',
() async {
when(() => aabDiffer.aabContentDifferences(any(), any())).thenReturn(
{AabDifferences.dart},
when(() => aabDiffer.contentDifferences(any(), any())).thenReturn(
{ArchiveDifferences.dart},
);
final tempDir = setUpTempDir();
@@ -740,8 +740,8 @@ Please create a release using "shorebird release" and try again.
test(
'''exits if user decides to not proceed after being warned of non-dart changes''',
() async {
when(() => aabDiffer.aabContentDifferences(any(), any())).thenReturn(
{AabDifferences.assets},
when(() => aabDiffer.contentDifferences(any(), any())).thenReturn(
{ArchiveDifferences.assets},
);
when(
() => logger.confirm(any(that: contains('Continue anyways?'))),