Skip to content

Commit 686af14

Browse files
authored
fix(android): apply 16kb alignment config in native asset builds
1 parent 17f73b2 commit 686af14

5 files changed

Lines changed: 495 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,88 @@ jobs:
125125
working-directory: bdk_demo
126126
run: flutter build apk --debug --target-platform android-arm64
127127

128+
- name: Verify APK ELF alignment
129+
run: dart scripts/check_android_elf_alignment.dart bdk_demo/build/app/outputs/flutter-apk/app-debug.apk
130+
131+
android-alignment:
132+
name: android-alignment (${{ matrix.rust_target }})
133+
runs-on: ubuntu-latest
134+
strategy:
135+
fail-fast: false
136+
matrix:
137+
include:
138+
- rust_target: aarch64-linux-android
139+
dart_arch: arm64
140+
- rust_target: x86_64-linux-android
141+
dart_arch: x64
142+
143+
steps:
144+
- name: Checkout
145+
uses: actions/checkout@v4
146+
147+
- name: Setup Rust
148+
uses: dtolnay/rust-toolchain@stable
149+
150+
- name: Setup Android SDK
151+
uses: android-actions/setup-android@v3
152+
153+
- name: Setup Dart
154+
uses: dart-lang/setup-dart@v1
155+
with:
156+
sdk: "3.10.0"
157+
158+
- name: Pub get
159+
run: dart pub get
160+
161+
- name: Install Android NDK
162+
run: sdkmanager "ndk;27.1.12297006"
163+
164+
- name: Build Android native library with NDK 27
165+
run: |
166+
rustup toolchain install 1.85.1 --profile minimal --target "${{ matrix.rust_target }}"
167+
NDK="$ANDROID_HOME/ndk/27.1.12297006"
168+
TOOLCHAIN="$NDK/toolchains/llvm/prebuilt/linux-x86_64"
169+
export ANDROID_NDK_HOME="$NDK"
170+
export ANDROID_NDK_ROOT="$NDK"
171+
172+
case "${{ matrix.rust_target }}" in
173+
aarch64-linux-android)
174+
export AR_aarch64_linux_android="$TOOLCHAIN/bin/llvm-ar"
175+
export CC_aarch64_linux_android="$TOOLCHAIN/bin/aarch64-linux-android35-clang"
176+
export CXX_aarch64_linux_android="$TOOLCHAIN/bin/aarch64-linux-android35-clang++"
177+
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$TOOLCHAIN/bin/aarch64-linux-android35-clang"
178+
export BINDGEN_EXTRA_CLANG_ARGS_aarch64_linux_android="--sysroot=$TOOLCHAIN/sysroot -I$TOOLCHAIN/sysroot/usr/include/aarch64-linux-android"
179+
;;
180+
x86_64-linux-android)
181+
export AR_x86_64_linux_android="$TOOLCHAIN/bin/llvm-ar"
182+
export CC_x86_64_linux_android="$TOOLCHAIN/bin/x86_64-linux-android35-clang"
183+
export CXX_x86_64_linux_android="$TOOLCHAIN/bin/x86_64-linux-android35-clang++"
184+
export CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="$TOOLCHAIN/bin/x86_64-linux-android35-clang"
185+
export BINDGEN_EXTRA_CLANG_ARGS_x86_64_linux_android="--sysroot=$TOOLCHAIN/sysroot -I$TOOLCHAIN/sysroot/usr/include/x86_64-linux-android"
186+
;;
187+
*)
188+
echo "Unsupported Android target: ${{ matrix.rust_target }}"
189+
exit 1
190+
;;
191+
esac
192+
193+
rustup run 1.85.1 cargo build \
194+
--release \
195+
--manifest-path native/Cargo.toml \
196+
--package bdk_dart_ffi \
197+
--target "${{ matrix.rust_target }}" \
198+
--target-dir /tmp/bdk-dart-android-target \
199+
--config native/.cargo/config.toml
200+
201+
- name: Verify NDK 27 native library alignment
202+
run: dart scripts/check_android_elf_alignment.dart /tmp/bdk-dart-android-target/${{ matrix.rust_target }}/release/libbdk_dart_ffi.so
203+
204+
- name: Verify NDK 27 hook alignment
205+
run: |
206+
export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/27.1.12297006"
207+
export ANDROID_NDK_ROOT="$ANDROID_NDK_HOME"
208+
dart scripts/check_android_hook_alignment.dart --arch "${{ matrix.dart_arch }}"
209+
128210
ios-smoke:
129211
runs-on: macos-latest
130212
steps:

hook/build.dart

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import 'package:hooks/hooks.dart';
22
import 'package:native_toolchain_rust/native_toolchain_rust.dart';
33

4-
void main(List<String> args) async {
4+
Future<void> main(List<String> args) async {
55
await build(args, (input, output) async {
6-
await const RustBuilder(
6+
final cargoConfigPath = input.packageRoot
7+
.resolve('native/.cargo/config.toml')
8+
.toFilePath();
9+
10+
// Native Assets invokes Cargo from the package root, so pass the crate-local
11+
// config explicitly instead of relying on Cargo's working-directory lookup.
12+
await RustBuilder(
713
assetName: 'uniffi:bdk_dart_ffi',
14+
extraCargoBuildArgs: ['--config', cargoConfigPath],
815
).run(input: input, output: output);
916
});
1017
}

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ dependencies:
2121
hooks: ^1.0.0
2222
native_toolchain_rust: ^1.0.0
2323
dev_dependencies:
24+
code_assets: ^1.0.0
2425
test: ^1.26.2
2526

2627
hooks:
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import 'dart:io';
2+
import 'dart:typed_data';
3+
4+
const _libraryName = 'libbdk_dart_ffi.so';
5+
const _minimumLoadAlignment = 0x4000;
6+
const _ptLoad = 1;
7+
8+
Future<void> main(List<String> args) async {
9+
if (args.isEmpty) {
10+
_fail(
11+
'Usage: dart scripts/check_android_elf_alignment.dart '
12+
'<apk-aab-or-so> [...]',
13+
);
14+
}
15+
16+
for (final path in args) {
17+
final input = File(path);
18+
if (!input.existsSync()) {
19+
_fail('File not found: ${input.path}');
20+
}
21+
22+
if (_isElf(input)) {
23+
_checkLibrary(input, input.path);
24+
} else {
25+
await _checkArchive(input);
26+
}
27+
}
28+
}
29+
30+
Future<void> _checkArchive(File archive) async {
31+
final tempDir = await Directory.systemTemp.createTemp(
32+
'bdk_dart_elf_alignment_',
33+
);
34+
35+
try {
36+
final entries = await _findNativeLibraryEntries(archive);
37+
if (entries.isEmpty) {
38+
_fail('No $_libraryName entries found in ${archive.path}');
39+
}
40+
41+
await _extractLibraries(archive, tempDir, entries);
42+
43+
for (final entry in entries) {
44+
final library = _extractedEntry(tempDir, entry);
45+
if (!library.existsSync()) {
46+
_fail('Extracted library not found: $entry');
47+
}
48+
_checkLibrary(library, entry);
49+
}
50+
} finally {
51+
await tempDir.delete(recursive: true);
52+
}
53+
}
54+
55+
void _checkLibrary(File library, String label) {
56+
final loadAlignments = _readLoadAlignments(library);
57+
if (loadAlignments.isEmpty) {
58+
_fail('No PT_LOAD segments found in $label');
59+
}
60+
61+
final invalidAlignments = loadAlignments.where(
62+
(alignment) =>
63+
alignment < _minimumLoadAlignment ||
64+
alignment % _minimumLoadAlignment != 0,
65+
);
66+
67+
if (invalidAlignments.isNotEmpty) {
68+
_fail(
69+
'$label has invalid PT_LOAD alignment(s): '
70+
'${invalidAlignments.map(_hex).join(', ')}',
71+
);
72+
}
73+
74+
final minimumAlignment = loadAlignments.reduce((a, b) => a < b ? a : b);
75+
stdout.writeln('OK $label minLOADalign=${_hex(minimumAlignment)}');
76+
}
77+
78+
Future<List<String>> _findNativeLibraryEntries(File archive) async {
79+
final result = await Process.run('unzip', ['-Z', '-1', archive.path]);
80+
if (result.exitCode != 0) {
81+
_fail(
82+
'Failed to list $_libraryName entries in ${archive.path}.\n'
83+
'${result.stderr}',
84+
);
85+
}
86+
87+
final entries =
88+
(result.stdout as String)
89+
.split('\n')
90+
.where(_isTargetLibraryEntry)
91+
.toList()
92+
..sort();
93+
return entries;
94+
}
95+
96+
bool _isTargetLibraryEntry(String entry) {
97+
final segments = entry.split('/');
98+
if (segments.any((segment) => segment.isEmpty || segment == '.')) {
99+
return false;
100+
}
101+
if (segments.any((segment) => segment == '..')) {
102+
return false;
103+
}
104+
if (segments.last != _libraryName) {
105+
return false;
106+
}
107+
108+
final isApkLibrary = segments.length == 3 && segments[0] == 'lib';
109+
final isAabLibrary = segments.length == 4 && segments[1] == 'lib';
110+
return isApkLibrary || isAabLibrary;
111+
}
112+
113+
Future<void> _extractLibraries(
114+
File archive,
115+
Directory destination,
116+
List<String> entries,
117+
) async {
118+
final result = await Process.run('unzip', [
119+
'-q',
120+
archive.path,
121+
...entries,
122+
'-d',
123+
destination.path,
124+
]);
125+
126+
if (result.exitCode != 0) {
127+
_fail(
128+
'Failed to extract $_libraryName from ${archive.path}.\n'
129+
'${result.stderr}',
130+
);
131+
}
132+
}
133+
134+
File _extractedEntry(Directory root, String entry) {
135+
final path = entry
136+
.split('/')
137+
.fold(root.path, (parent, child) => _join(parent, child));
138+
return File(path);
139+
}
140+
141+
bool _isElf(File file) {
142+
final reader = file.openSync();
143+
try {
144+
final magic = reader.readSync(4);
145+
return magic.length == 4 &&
146+
magic[0] == 0x7f &&
147+
magic[1] == 0x45 &&
148+
magic[2] == 0x4c &&
149+
magic[3] == 0x46;
150+
} finally {
151+
reader.closeSync();
152+
}
153+
}
154+
155+
List<int> _readLoadAlignments(File elfFile) {
156+
final bytes = elfFile.readAsBytesSync();
157+
if (bytes.length < 64 || !_isElf(elfFile)) {
158+
_fail('${elfFile.path} is not an ELF file');
159+
}
160+
161+
final elfClass = bytes[4];
162+
final endian = switch (bytes[5]) {
163+
1 => Endian.little,
164+
2 => Endian.big,
165+
_ => throw FormatException('Unsupported ELF endianness in ${elfFile.path}'),
166+
};
167+
final data = ByteData.sublistView(bytes);
168+
169+
int uint16(int offset) => data.getUint16(offset, endian);
170+
int uint32(int offset) => data.getUint32(offset, endian);
171+
int uint64(int offset) => data.getUint64(offset, endian);
172+
173+
final (
174+
programHeaderOffset,
175+
programHeaderEntrySize,
176+
programHeaderCount,
177+
) = switch (elfClass) {
178+
1 => (uint32(28), uint16(42), uint16(44)),
179+
2 => (uint64(32), uint16(54), uint16(56)),
180+
_ => throw FormatException('Unsupported ELF class in ${elfFile.path}'),
181+
};
182+
183+
final alignments = <int>[];
184+
for (var index = 0; index < programHeaderCount; index++) {
185+
final headerOffset = programHeaderOffset + index * programHeaderEntrySize;
186+
if (headerOffset + programHeaderEntrySize > bytes.length) {
187+
_fail('${elfFile.path} has a truncated ELF program header table');
188+
}
189+
190+
final type = uint32(headerOffset);
191+
if (type != _ptLoad) {
192+
continue;
193+
}
194+
195+
final alignment = switch (elfClass) {
196+
1 => uint32(headerOffset + 28),
197+
2 => uint64(headerOffset + 48),
198+
_ => throw StateError('unreachable'),
199+
};
200+
alignments.add(alignment);
201+
}
202+
203+
return alignments;
204+
}
205+
206+
String _hex(int value) => '0x${value.toRadixString(16)}';
207+
208+
String _join(String parent, String child) {
209+
final separator = Platform.pathSeparator;
210+
return parent.endsWith(separator)
211+
? '$parent$child'
212+
: '$parent$separator$child';
213+
}
214+
215+
Never _fail(String message) {
216+
stderr.writeln(message);
217+
exit(1);
218+
}

0 commit comments

Comments
 (0)