1import 'dart:convert'; 2import 'dart:io'; 3 4import 'package:path/path.dart' as p; 5import 'package:collection/collection.dart' show MapEquality; 6 7// This script verifies that the release binaries only export the expected 8// symbols. 9// 10// Android binaries (libflutter.so) should only export one symbol "JNI_OnLoad" 11// of type "T". 12// 13// iOS binaries (Flutter.framework/Flutter) should only export Objective-C 14// Symbols from the Flutter namespace. These are either of type 15// "(__DATA,__common)" or "(__DATA,__objc_data)". 16 17/// Takes the path to the out directory as the first argument, and the path to 18/// the buildtools directory as the second argument. 19/// 20/// If the second argument is not specified, it is assumed that it is the parent 21/// of the out directory (for backwards compatibility). 22void main(List<String> arguments) { 23 assert(arguments.length == 2 || arguments.length == 1); 24 final String outPath = arguments.first; 25 final String buildToolsPath = arguments.length == 1 26 ? p.join(p.dirname(outPath), 'buildtools') 27 : arguments[1]; 28 29 String platform; 30 if (Platform.isLinux) { 31 platform = 'linux-x64'; 32 } else if (Platform.isMacOS) { 33 platform = 'mac-x64'; 34 } else { 35 throw UnimplementedError('Script only support running on Linux or MacOS.'); 36 } 37 final String nmPath = p.join(buildToolsPath, platform, 'clang', 'bin', 'llvm-nm'); 38 assert(new Directory(outPath).existsSync()); 39 40 final Iterable<String> releaseBuilds = Directory(outPath).listSync() 41 .where((FileSystemEntity entity) => entity is Directory) 42 .map<String>((FileSystemEntity dir) => p.basename(dir.path)) 43 .where((String s) => s.contains('_release')); 44 45 final Iterable<String> iosReleaseBuilds = releaseBuilds 46 .where((String s) => s.startsWith('ios_')); 47 final Iterable<String> androidReleaseBuilds = releaseBuilds 48 .where((String s) => s.startsWith('android_')); 49 50 int failures = 0; 51 failures += _checkIos(outPath, nmPath, iosReleaseBuilds); 52 failures += _checkAndroid(outPath, nmPath, androidReleaseBuilds); 53 print('Failing checks: $failures'); 54 exit(failures); 55} 56 57int _checkIos(String outPath, String nmPath, Iterable<String> builds) { 58 int failures = 0; 59 for (String build in builds) { 60 final String libFlutter = p.join(outPath, build, 'Flutter.framework', 'Flutter'); 61 if (!new File(libFlutter).existsSync()) { 62 print('SKIPPING: $libFlutter does not exist.'); 63 continue; 64 } 65 final ProcessResult nmResult = Process.runSync(nmPath, <String>['-gUm', libFlutter]); 66 if (nmResult.exitCode != 0) { 67 print('ERROR: failed to execute "nm -gUm $libFlutter":\n${nmResult.stderr}'); 68 failures++; 69 continue; 70 } 71 final Iterable<NmEntry> unexpectedEntries = NmEntry.parse(nmResult.stdout).where((NmEntry entry) { 72 return !(((entry.type == '(__DATA,__common)' || entry.type == '(__DATA,__const)') && entry.name.startsWith('_Flutter')) 73 || (entry.type == '(__DATA,__objc_data)' 74 && (entry.name.startsWith('_OBJC_METACLASS_\$_Flutter') || entry.name.startsWith('_OBJC_CLASS_\$_Flutter')))); 75 }); 76 if (unexpectedEntries.isNotEmpty) { 77 print('ERROR: $libFlutter exports unexpected symbols:'); 78 print(unexpectedEntries.fold<String>('', (String previous, NmEntry entry) { 79 return '${previous == '' ? '' : '$previous\n'} ${entry.type} ${entry.name}'; 80 })); 81 failures++; 82 } else { 83 print('OK: $libFlutter'); 84 } 85 } 86 return failures; 87} 88 89int _checkAndroid(String outPath, String nmPath, Iterable<String> builds) { 90 int failures = 0; 91 for (String build in builds) { 92 final String libFlutter = p.join(outPath, build, 'libflutter.so'); 93 if (!new File(libFlutter).existsSync()) { 94 print('SKIPPING: $libFlutter does not exist.'); 95 continue; 96 } 97 final ProcessResult nmResult = Process.runSync(nmPath, <String>['-gU', libFlutter]); 98 if (nmResult.exitCode != 0) { 99 print('ERROR: failed to execute "nm -gU $libFlutter":\n${nmResult.stderr}'); 100 failures++; 101 continue; 102 } 103 final Iterable<NmEntry> entries = NmEntry.parse(nmResult.stdout); 104 final Map<String, String> entryMap = Map<String, String>.fromIterable( 105 entries, 106 key: (dynamic entry) => entry.name, 107 value: (dynamic entry) => entry.type); 108 final Map<String, String> expectedSymbols = <String, String>{ 109 'JNI_OnLoad': 'T', 110 '_binary_icudtl_dat_size': 'A', 111 '_binary_icudtl_dat_start': 'D', 112 }; 113 if (!const MapEquality<String, String>().equals(entryMap, expectedSymbols)) { 114 print('ERROR: $libFlutter exports the wrong symbols'); 115 print(' Expected $expectedSymbols'); 116 print(' Library has $entryMap.'); 117 failures++; 118 } else { 119 print('OK: $libFlutter'); 120 } 121 } 122 return failures; 123} 124 125class NmEntry { 126 NmEntry._(this.address, this.type, this.name); 127 128 final String address; 129 final String type; 130 final String name; 131 132 static Iterable<NmEntry> parse(String stdout) { 133 return LineSplitter.split(stdout).map((String line) { 134 final List<String> parts = line.split(' '); 135 return NmEntry._(parts[0], parts[1], parts.last); 136 }); 137 } 138} 139