-
Notifications
You must be signed in to change notification settings - Fork 15
fix(android): apply 16kb alignment config in native asset builds #90
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
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
7c4c610
fix(android): apply 16kb alignment config in native asset builds
reez 6c06f6f
feat: support aab native library alignment checks
reez 8c2154f
fix: verify android alignment for x86_64
reez 7d85229
fix: verify apk elf alignment in android smoke
reez File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 |
|---|---|---|
| @@ -1,10 +1,17 @@ | ||
| import 'package:hooks/hooks.dart'; | ||
| import 'package:native_toolchain_rust/native_toolchain_rust.dart'; | ||
|
|
||
| void main(List<String> args) async { | ||
| Future<void> main(List<String> args) async { | ||
| await build(args, (input, output) async { | ||
| await const RustBuilder( | ||
| final cargoConfigPath = input.packageRoot | ||
| .resolve('native/.cargo/config.toml') | ||
| .toFilePath(); | ||
|
|
||
| // Native Assets invokes Cargo from the package root, so pass the crate-local | ||
| // config explicitly instead of relying on Cargo's working-directory lookup. | ||
| await RustBuilder( | ||
| assetName: 'uniffi:bdk_dart_ffi', | ||
| extraCargoBuildArgs: ['--config', cargoConfigPath], | ||
| ).run(input: input, output: output); | ||
| }); | ||
| } |
This file contains hidden or 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 hidden or 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,218 @@ | ||
| import 'dart:io'; | ||
| import 'dart:typed_data'; | ||
|
|
||
| const _libraryName = 'libbdk_dart_ffi.so'; | ||
| const _minimumLoadAlignment = 0x4000; | ||
| const _ptLoad = 1; | ||
|
|
||
| Future<void> main(List<String> args) async { | ||
| if (args.isEmpty) { | ||
| _fail( | ||
| 'Usage: dart scripts/check_android_elf_alignment.dart ' | ||
| '<apk-aab-or-so> [...]', | ||
| ); | ||
| } | ||
|
|
||
| for (final path in args) { | ||
| final input = File(path); | ||
| if (!input.existsSync()) { | ||
| _fail('File not found: ${input.path}'); | ||
| } | ||
|
|
||
| if (_isElf(input)) { | ||
| _checkLibrary(input, input.path); | ||
| } else { | ||
| await _checkArchive(input); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Future<void> _checkArchive(File archive) async { | ||
| final tempDir = await Directory.systemTemp.createTemp( | ||
| 'bdk_dart_elf_alignment_', | ||
| ); | ||
|
|
||
| try { | ||
| final entries = await _findNativeLibraryEntries(archive); | ||
| if (entries.isEmpty) { | ||
| _fail('No $_libraryName entries found in ${archive.path}'); | ||
| } | ||
|
|
||
| await _extractLibraries(archive, tempDir, entries); | ||
|
|
||
| for (final entry in entries) { | ||
| final library = _extractedEntry(tempDir, entry); | ||
| if (!library.existsSync()) { | ||
| _fail('Extracted library not found: $entry'); | ||
| } | ||
| _checkLibrary(library, entry); | ||
| } | ||
| } finally { | ||
| await tempDir.delete(recursive: true); | ||
| } | ||
| } | ||
|
|
||
| void _checkLibrary(File library, String label) { | ||
| final loadAlignments = _readLoadAlignments(library); | ||
| if (loadAlignments.isEmpty) { | ||
| _fail('No PT_LOAD segments found in $label'); | ||
| } | ||
|
|
||
| final invalidAlignments = loadAlignments.where( | ||
| (alignment) => | ||
| alignment < _minimumLoadAlignment || | ||
| alignment % _minimumLoadAlignment != 0, | ||
| ); | ||
|
|
||
| if (invalidAlignments.isNotEmpty) { | ||
| _fail( | ||
| '$label has invalid PT_LOAD alignment(s): ' | ||
| '${invalidAlignments.map(_hex).join(', ')}', | ||
| ); | ||
| } | ||
|
|
||
| final minimumAlignment = loadAlignments.reduce((a, b) => a < b ? a : b); | ||
| stdout.writeln('OK $label minLOADalign=${_hex(minimumAlignment)}'); | ||
| } | ||
|
|
||
| Future<List<String>> _findNativeLibraryEntries(File archive) async { | ||
| final result = await Process.run('unzip', ['-Z', '-1', archive.path]); | ||
| if (result.exitCode != 0) { | ||
| _fail( | ||
| 'Failed to list $_libraryName entries in ${archive.path}.\n' | ||
| '${result.stderr}', | ||
| ); | ||
| } | ||
|
|
||
| final entries = | ||
| (result.stdout as String) | ||
| .split('\n') | ||
| .where(_isTargetLibraryEntry) | ||
| .toList() | ||
| ..sort(); | ||
| return entries; | ||
| } | ||
|
|
||
| bool _isTargetLibraryEntry(String entry) { | ||
| final segments = entry.split('/'); | ||
| if (segments.any((segment) => segment.isEmpty || segment == '.')) { | ||
| return false; | ||
| } | ||
| if (segments.any((segment) => segment == '..')) { | ||
| return false; | ||
| } | ||
| if (segments.last != _libraryName) { | ||
| return false; | ||
| } | ||
|
|
||
| final isApkLibrary = segments.length == 3 && segments[0] == 'lib'; | ||
| final isAabLibrary = segments.length == 4 && segments[1] == 'lib'; | ||
| return isApkLibrary || isAabLibrary; | ||
| } | ||
|
|
||
| Future<void> _extractLibraries( | ||
| File archive, | ||
| Directory destination, | ||
| List<String> entries, | ||
| ) async { | ||
| final result = await Process.run('unzip', [ | ||
| '-q', | ||
| archive.path, | ||
| ...entries, | ||
| '-d', | ||
| destination.path, | ||
| ]); | ||
|
|
||
| if (result.exitCode != 0) { | ||
| _fail( | ||
| 'Failed to extract $_libraryName from ${archive.path}.\n' | ||
| '${result.stderr}', | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| File _extractedEntry(Directory root, String entry) { | ||
| final path = entry | ||
| .split('/') | ||
| .fold(root.path, (parent, child) => _join(parent, child)); | ||
| return File(path); | ||
| } | ||
|
|
||
| bool _isElf(File file) { | ||
| final reader = file.openSync(); | ||
| try { | ||
| final magic = reader.readSync(4); | ||
| return magic.length == 4 && | ||
| magic[0] == 0x7f && | ||
| magic[1] == 0x45 && | ||
| magic[2] == 0x4c && | ||
| magic[3] == 0x46; | ||
| } finally { | ||
| reader.closeSync(); | ||
| } | ||
| } | ||
|
|
||
| List<int> _readLoadAlignments(File elfFile) { | ||
| final bytes = elfFile.readAsBytesSync(); | ||
| if (bytes.length < 64 || !_isElf(elfFile)) { | ||
| _fail('${elfFile.path} is not an ELF file'); | ||
| } | ||
|
|
||
| final elfClass = bytes[4]; | ||
| final endian = switch (bytes[5]) { | ||
| 1 => Endian.little, | ||
| 2 => Endian.big, | ||
| _ => throw FormatException('Unsupported ELF endianness in ${elfFile.path}'), | ||
| }; | ||
| final data = ByteData.sublistView(bytes); | ||
|
|
||
| int uint16(int offset) => data.getUint16(offset, endian); | ||
| int uint32(int offset) => data.getUint32(offset, endian); | ||
| int uint64(int offset) => data.getUint64(offset, endian); | ||
|
|
||
| final ( | ||
| programHeaderOffset, | ||
| programHeaderEntrySize, | ||
| programHeaderCount, | ||
| ) = switch (elfClass) { | ||
| 1 => (uint32(28), uint16(42), uint16(44)), | ||
| 2 => (uint64(32), uint16(54), uint16(56)), | ||
| _ => throw FormatException('Unsupported ELF class in ${elfFile.path}'), | ||
| }; | ||
|
|
||
| final alignments = <int>[]; | ||
| for (var index = 0; index < programHeaderCount; index++) { | ||
| final headerOffset = programHeaderOffset + index * programHeaderEntrySize; | ||
| if (headerOffset + programHeaderEntrySize > bytes.length) { | ||
| _fail('${elfFile.path} has a truncated ELF program header table'); | ||
| } | ||
|
|
||
| final type = uint32(headerOffset); | ||
| if (type != _ptLoad) { | ||
| continue; | ||
| } | ||
|
|
||
| final alignment = switch (elfClass) { | ||
| 1 => uint32(headerOffset + 28), | ||
| 2 => uint64(headerOffset + 48), | ||
| _ => throw StateError('unreachable'), | ||
| }; | ||
| alignments.add(alignment); | ||
| } | ||
|
|
||
| return alignments; | ||
| } | ||
|
|
||
| String _hex(int value) => '0x${value.toRadixString(16)}'; | ||
|
|
||
| String _join(String parent, String child) { | ||
| final separator = Platform.pathSeparator; | ||
| return parent.endsWith(separator) | ||
| ? '$parent$child' | ||
| : '$parent$separator$child'; | ||
| } | ||
|
|
||
| Never _fail(String message) { | ||
| stderr.writeln(message); | ||
| exit(1); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
small nit here: This handles .so and APK but not AAB. Not urgent since the hook job checks the .so directly, but if someone wants to sanity check a Play upload bundle down the road, that'd be a nice add.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah I see what your saying, is the 6c06f6f commit I added doing what you were thinking you'd want?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes it does.