feat(shorebird_cli): add aar differ (#584)
This commit is contained in:
@@ -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.''',
|
||||
|
||||
@@ -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.
Binary file not shown.
+16
-16
@@ -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
-1
@@ -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?'))),
|
||||
|
||||
Reference in New Issue
Block a user