-
Notifications
You must be signed in to change notification settings - Fork 147
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(shorebird_cli): alert user of non-patchable changes (#538)
- Loading branch information
1 parent
146f84b
commit f3999fd
Showing
13 changed files
with
584 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export 'aab_differ.dart'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
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 == p.join('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'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import 'dart:io'; | ||
|
||
import 'package:meta/meta.dart'; | ||
|
||
/// {@template mf_entry} | ||
/// A single entry from an .MF file. | ||
/// {@endtemplate} | ||
@immutable | ||
class MfEntry { | ||
/// {@macro mf_entry} | ||
const MfEntry({ | ||
required this.name, | ||
required this.sha256Digest, | ||
}); | ||
|
||
/// Contents of the `Name` field. | ||
final String name; | ||
|
||
/// Contents of the `SHA-256-Digest` field. | ||
final String sha256Digest; | ||
|
||
@override | ||
String toString() => 'MfEntry(name: $name, sha256Digest: $sha256Digest)'; | ||
|
||
@override | ||
bool operator ==(Object other) => | ||
identical(this, other) || | ||
other is MfEntry && | ||
runtimeType == other.runtimeType && | ||
name == other.name && | ||
sha256Digest == other.sha256Digest; | ||
|
||
@override | ||
int get hashCode => Object.hashAll([name, sha256Digest]); | ||
} | ||
|
||
/// Parses a .MF file into a list of [MfEntry]s. | ||
class MfReader { | ||
static final nameRegex = RegExp(r'^Name: (.+)$'); | ||
static final nameContinuedRegex = RegExp(r'^ (.+)$'); | ||
static final shaDigestRegex = RegExp(r'^SHA-256-Digest: (.+)$'); | ||
|
||
/// Parses the content of [mfFile] into a list of [MfEntry]s. | ||
/// | ||
/// [mfFile] should be a JAR manifest file, as described in | ||
/// https://docs.oracle.com/javase/tutorial/deployment/jar/manifestindex.html. | ||
static List<MfEntry> read(File mfFile) => parse(mfFile.readAsStringSync()); | ||
|
||
/// Parses the contents [mfContents] file into a list of [MfEntry]s. | ||
/// | ||
/// [mfContents] should be a JAR manifest file, as described in | ||
/// https://docs.oracle.com/javase/tutorial/deployment/jar/manifestindex.html. | ||
static List<MfEntry> parse(String mfContents) { | ||
final lines = mfContents.split('\n').map((line) => line.trimRight()); | ||
final entries = <MfEntry>[]; | ||
var currentHash = ''; | ||
var currentName = ''; | ||
for (final line in lines) { | ||
if (line.isEmpty && currentName.isNotEmpty && currentHash.isNotEmpty) { | ||
entries.add(MfEntry(name: currentName, sha256Digest: currentHash)); | ||
currentHash = ''; | ||
currentName = ''; | ||
} else if (nameRegex.hasMatch(line)) { | ||
currentName = nameRegex.firstMatch(line)!.group(1)!; | ||
} else if (nameContinuedRegex.hasMatch(line)) { | ||
currentName += nameContinuedRegex.firstMatch(line)!.group(1)!; | ||
} else if (shaDigestRegex.hasMatch(line)) { | ||
currentHash = shaDigestRegex.firstMatch(line)!.group(1)!; | ||
} | ||
} | ||
|
||
if (currentName.isNotEmpty && currentHash.isNotEmpty) { | ||
entries.add(MfEntry(name: currentName, sha256Digest: currentHash)); | ||
} | ||
|
||
return entries; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
The aab files in this folder were generated by building our time_shift app. | ||
|
||
Some of their contents has been removed to reduce the size of the files. Most notably, all libflutter.so and libapp.so files have been removed. | ||
|
||
Files: | ||
- base.aab is meant to represent an aab uploaded as part of a release. | ||
- changed_dart.aab was built from the same code as base.aab with only Dart changes (i.e., no asset or Java/Kotlin changes) | ||
- changed_kotlin.aab was built from the same code as base.aab with only Kotlin changes (i.e., no asset or Dart changes) | ||
- changed_asset.aab was built from the same code as base.aab with only asset changes (i.e., no Dart or Java/Kotlin changes) | ||
- changed_dart_and_asset.aab was built from the same code as base.aab with only Dart and asset changes (i.e., no Java/Kotlin changes) |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import 'package:path/path.dart' as p; | ||
import 'package:shorebird_cli/src/aab/aab.dart'; | ||
import 'package:test/test.dart'; | ||
|
||
void main() { | ||
group(AabDiffer, () { | ||
final aabFixturesBasePath = p.join('test', 'fixtures', 'aabs'); | ||
final baseAabPath = p.join(aabFixturesBasePath, 'base.aab'); | ||
final changedAssetAabPath = | ||
p.join(aabFixturesBasePath, 'changed_asset.aab'); | ||
final changedDartAabPath = p.join(aabFixturesBasePath, 'changed_dart.aab'); | ||
final changedKotlinAabPath = | ||
p.join(aabFixturesBasePath, 'changed_kotlin.aab'); | ||
final changedDartAndAssetAabPath = | ||
p.join(aabFixturesBasePath, 'changed_dart_and_asset.aab'); | ||
|
||
late AabDiffer differ; | ||
|
||
setUp(() { | ||
differ = AabDiffer(); | ||
}); | ||
|
||
group('aabFileDifferences', () { | ||
test('finds no differences between the same aab', () { | ||
expect(differ.aabChangedFiles(baseAabPath, baseAabPath), isEmpty); | ||
}); | ||
|
||
test('finds differences between the two different aabs', () { | ||
expect( | ||
differ.aabChangedFiles(baseAabPath, changedDartAabPath).toSet(), | ||
{ | ||
'BUNDLE-METADATA/com.android.tools.build.libraries/dependencies.pb', | ||
'base/lib/arm64-v8a/libapp.so', | ||
'base/lib/armeabi-v7a/libapp.so', | ||
'base/lib/x86_64/libapp.so', | ||
}, | ||
); | ||
}); | ||
}); | ||
|
||
group('aabContentDifferences', () { | ||
test('detects no differences between the same aab', () { | ||
expect(differ.aabContentDifferences(baseAabPath, baseAabPath), isEmpty); | ||
}); | ||
|
||
test('detects asset changes', () { | ||
expect( | ||
differ.aabContentDifferences(baseAabPath, changedAssetAabPath), | ||
{AabDifferences.assets}, | ||
); | ||
}); | ||
|
||
test('detects kotlin changes', () { | ||
expect( | ||
differ.aabContentDifferences(baseAabPath, changedKotlinAabPath), | ||
{AabDifferences.native}, | ||
); | ||
}); | ||
|
||
test('detects dart changes', () { | ||
expect( | ||
differ.aabContentDifferences(baseAabPath, changedDartAabPath), | ||
{AabDifferences.dart}, | ||
); | ||
}); | ||
|
||
test('detects dart and asset changes', () { | ||
expect( | ||
differ.aabContentDifferences(baseAabPath, changedDartAndAssetAabPath), | ||
{ | ||
AabDifferences.assets, | ||
AabDifferences.dart, | ||
}, | ||
); | ||
}); | ||
}); | ||
}); | ||
} |
Oops, something went wrong.