diff --git a/pkgs/c_compiler/lib/src/cbuilder/run_cbuilder.dart b/pkgs/c_compiler/lib/src/cbuilder/run_cbuilder.dart index b5351836f..9a8945909 100644 --- a/pkgs/c_compiler/lib/src/cbuilder/run_cbuilder.dart +++ b/pkgs/c_compiler/lib/src/cbuilder/run_cbuilder.dart @@ -95,7 +95,9 @@ class RunCBuilder { // The sysroot should be discovered automatically after NDK 22. // Workaround: if (dynamicLibrary != null) '-nostartfiles', - '--target=${androidNdkClangTargetFlags[target]!}', + '--target=' + '${androidNdkClangTargetFlags[target]!}' + '${buildConfig.targetAndroidNdkApi!}', ], if (target.os == OS.macOS || target.os == OS.iOS) '--target=${appleClangTargetFlags[target]!}', diff --git a/pkgs/c_compiler/test/cbuilder/cbuilder_cross_android_test.dart b/pkgs/c_compiler/test/cbuilder/cbuilder_cross_android_test.dart index 967b98f63..6ea3520be 100644 --- a/pkgs/c_compiler/test/cbuilder/cbuilder_cross_android_test.dart +++ b/pkgs/c_compiler/test/cbuilder/cbuilder_cross_android_test.dart @@ -33,37 +33,22 @@ void main() { Target.androidX64: 'elf64-x86-64', }; + /// From https://docs.flutter.dev/reference/supported-platforms. + const flutterAndroidNdkVersionLowestSupported = 21; + + /// From https://docs.flutter.dev/reference/supported-platforms. + const flutterAndroidNdkVersionHighestSupported = 30; + for (final linkMode in LinkMode.values) { for (final target in targets) { test('Cbuilder $linkMode library $target', () async { await inTempDir((tempUri) async { - final addCUri = - packageUri.resolve('test/cbuilder/testfiles/add/src/add.c'); - const name = 'add'; - - final buildConfig = BuildConfig( - outDir: tempUri, - packageRoot: tempUri, - target: target, - linkModePreference: linkMode == LinkMode.dynamic - ? LinkModePreference.dynamic - : LinkModePreference.static, + final libUri = await buildLib( + tempUri, + target, + flutterAndroidNdkVersionLowestSupported, + linkMode, ); - final buildOutput = BuildOutput(); - - final cbuilder = CBuilder.library( - name: 'add', - assetName: 'add', - sources: [addCUri.toFilePath()], - ); - await cbuilder.run( - buildConfig: buildConfig, - buildOutput: buildOutput, - logger: logger, - ); - - final libUri = - tempUri.resolve(target.os.libraryFileName(name, linkMode)); if (Platform.isLinux) { final result = await runProcess( executable: Uri.file('readelf'), @@ -91,4 +76,64 @@ void main() { }); } } + + test('Cbuilder API levels binary difference', () async { + const target = Target.androidArm64; + const linkMode = LinkMode.dynamic; + const apiLevel1 = flutterAndroidNdkVersionLowestSupported; + const apiLevel2 = flutterAndroidNdkVersionHighestSupported; + await inTempDir((tempUri) async { + final out1Uri = tempUri.resolve('out1/'); + final out2Uri = tempUri.resolve('out2/'); + final out3Uri = tempUri.resolve('out3/'); + await Directory.fromUri(out1Uri).create(); + await Directory.fromUri(out2Uri).create(); + await Directory.fromUri(out3Uri).create(); + final lib1Uri = await buildLib(out1Uri, target, apiLevel1, linkMode); + final lib2Uri = await buildLib(out2Uri, target, apiLevel2, linkMode); + final lib3Uri = await buildLib(out3Uri, target, apiLevel2, linkMode); + final bytes1 = await File.fromUri(lib1Uri).readAsBytes(); + final bytes2 = await File.fromUri(lib2Uri).readAsBytes(); + final bytes3 = await File.fromUri(lib3Uri).readAsBytes(); + // Different API levels should lead to a different binary. + expect(bytes1, isNot(bytes2)); + // Identical API levels should lead to an identical binary. + expect(bytes2, bytes3); + }); + }); +} + +Future buildLib( + Uri tempUri, + Target target, + int androidNdkApi, + LinkMode linkMode, +) async { + final addCUri = packageUri.resolve('test/cbuilder/testfiles/add/src/add.c'); + const name = 'add'; + + final buildConfig = BuildConfig( + outDir: tempUri, + packageRoot: tempUri, + target: target, + targetAndroidNdkApi: androidNdkApi, + linkModePreference: linkMode == LinkMode.dynamic + ? LinkModePreference.dynamic + : LinkModePreference.static, + ); + final buildOutput = BuildOutput(); + + final cbuilder = CBuilder.library( + name: name, + assetName: name, + sources: [addCUri.toFilePath()], + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); + + final libUri = tempUri.resolve(target.os.libraryFileName(name, linkMode)); + return libUri; } diff --git a/pkgs/c_compiler/test/cbuilder/cbuilder_cross_ios_test.dart b/pkgs/c_compiler/test/cbuilder/cbuilder_cross_ios_test.dart index e4dcf0077..bbcb70d39 100644 --- a/pkgs/c_compiler/test/cbuilder/cbuilder_cross_ios_test.dart +++ b/pkgs/c_compiler/test/cbuilder/cbuilder_cross_ios_test.dart @@ -53,8 +53,8 @@ void main() { final buildOutput = BuildOutput(); final cbuilder = CBuilder.library( - name: 'add', - assetName: 'add', + name: name, + assetName: name, sources: [addCUri.toFilePath()], ); await cbuilder.run( diff --git a/pkgs/c_compiler/test/cbuilder/cbuilder_cross_linux_host_test.dart b/pkgs/c_compiler/test/cbuilder/cbuilder_cross_linux_host_test.dart index 9a4f34a72..014255448 100644 --- a/pkgs/c_compiler/test/cbuilder/cbuilder_cross_linux_host_test.dart +++ b/pkgs/c_compiler/test/cbuilder/cbuilder_cross_linux_host_test.dart @@ -53,8 +53,8 @@ void main() { final buildOutput = BuildOutput(); final cbuilder = CBuilder.library( - name: 'add', - assetName: 'add', + name: name, + assetName: name, sources: [addCUri.toFilePath()], ); await cbuilder.run( diff --git a/pkgs/c_compiler/test/cbuilder/cbuilder_cross_macos_host_test.dart b/pkgs/c_compiler/test/cbuilder/cbuilder_cross_macos_host_test.dart index 06f4ff16f..b68e924d0 100644 --- a/pkgs/c_compiler/test/cbuilder/cbuilder_cross_macos_host_test.dart +++ b/pkgs/c_compiler/test/cbuilder/cbuilder_cross_macos_host_test.dart @@ -53,8 +53,8 @@ void main() { final buildOutput = BuildOutput(); final cbuilder = CBuilder.library( - name: 'add', - assetName: 'add', + name: name, + assetName: name, sources: [addCUri.toFilePath()], ); await cbuilder.run( diff --git a/pkgs/c_compiler/test/cbuilder/cbuilder_cross_windows_host_test.dart b/pkgs/c_compiler/test/cbuilder/cbuilder_cross_windows_host_test.dart index 230c9afbb..f00feeb8d 100644 --- a/pkgs/c_compiler/test/cbuilder/cbuilder_cross_windows_host_test.dart +++ b/pkgs/c_compiler/test/cbuilder/cbuilder_cross_windows_host_test.dart @@ -65,8 +65,8 @@ void main() { final buildOutput = BuildOutput(); final cbuilder = CBuilder.library( - name: 'add', - assetName: 'add', + name: name, + assetName: name, sources: [addCUri.toFilePath()], ); await cbuilder.run( diff --git a/pkgs/native_assets_cli/lib/src/model/build_config.dart b/pkgs/native_assets_cli/lib/src/model/build_config.dart index 5886efd73..8e657cc97 100644 --- a/pkgs/native_assets_cli/lib/src/model/build_config.dart +++ b/pkgs/native_assets_cli/lib/src/model/build_config.dart @@ -39,6 +39,12 @@ class BuildConfig { IOSSdk? get targetIOSSdk => _targetIOSSdk; late final IOSSdk? _targetIOSSdk; + /// When compiling for Android, the API version to target. + /// + /// Required when [target.os] equals [OS.android]. + int? get targetAndroidNdkApi => _targetAndroidNdkApi; + late final int? _targetAndroidNdkApi; + /// Preferred linkMode method for library. LinkModePreference get linkModePreference => _linkModePreference; late final LinkModePreference _linkModePreference; @@ -66,6 +72,7 @@ class BuildConfig { required Uri packageRoot, required Target target, IOSSdk? targetIOSSdk, + int? targetAndroidNdkApi, CCompilerConfig? cCompiler, required LinkModePreference linkModePreference, Map? dependencyMetadata, @@ -75,6 +82,7 @@ class BuildConfig { .._packageRoot = packageRoot .._target = target .._targetIOSSdk = targetIOSSdk + .._targetAndroidNdkApi = targetAndroidNdkApi .._cCompiler = cCompiler ?? CCompilerConfig() .._linkModePreference = linkModePreference .._dependencyMetadata = dependencyMetadata; @@ -94,6 +102,7 @@ class BuildConfig { required Uri packageRoot, required Target target, IOSSdk? targetIOSSdk, + int? targetAndroidNdkApi, CCompilerConfig? cCompiler, required LinkModePreference linkModePreference, Map? dependencyMetadata, @@ -103,6 +112,7 @@ class BuildConfig { packageName, target.toString(), targetIOSSdk.toString(), + targetAndroidNdkApi.toString(), linkModePreference.toString(), cCompiler?.ar.toString(), cCompiler?.cc.toString(), @@ -184,6 +194,7 @@ class BuildConfig { static const packageRootConfigKey = 'package_root'; static const dependencyMetadataConfigKey = 'dependency_metadata'; static const _versionKey = 'version'; + static const targetAndroidNdkApiConfigKey = 'target_android_ndk_api'; List _readFieldsFromConfig() { var targetSet = false; @@ -227,6 +238,9 @@ class BuildConfig { ), ) : null, + (config) => _targetAndroidNdkApi = (targetSet && _target.os == OS.android) + ? config.int(targetAndroidNdkApiConfigKey) + : null, (config) => cCompiler._ar = config.optionalPath(CCompilerConfig.arConfigKeyFull, mustExist: true), (config) { @@ -292,6 +306,8 @@ class BuildConfig { packageRootConfigKey: _packageRoot.toFilePath(), Target.configKey: _target.toString(), if (_targetIOSSdk != null) IOSSdk.configKey: _targetIOSSdk.toString(), + if (_targetAndroidNdkApi != null) + targetAndroidNdkApiConfigKey: _targetAndroidNdkApi!, if (cCompilerYaml.isNotEmpty) CCompilerConfig.configKey: cCompilerYaml, LinkModePreference.configKey: _linkModePreference.toString(), if (_dependencyMetadata != null) @@ -314,6 +330,7 @@ class BuildConfig { if (other._packageRoot != _packageRoot) return false; if (other._target != _target) return false; if (other._targetIOSSdk != _targetIOSSdk) return false; + if (other._targetAndroidNdkApi != _targetAndroidNdkApi) return false; if (other._cCompiler != _cCompiler) return false; if (other._linkModePreference != _linkModePreference) return false; if (!DeepCollectionEquality() @@ -327,6 +344,7 @@ class BuildConfig { _packageRoot, _target, _targetIOSSdk, + _targetAndroidNdkApi, _cCompiler, _linkModePreference, DeepCollectionEquality().hash(_dependencyMetadata), diff --git a/pkgs/native_assets_cli/test/model/asset_test.dart b/pkgs/native_assets_cli/test/model/asset_test.dart index c542c5e3b..a4aa644b3 100644 --- a/pkgs/native_assets_cli/test/model/asset_test.dart +++ b/pkgs/native_assets_cli/test/model/asset_test.dart @@ -139,7 +139,12 @@ native-assets: }); test('AssetPath factory', () async { - expect(() => AssetPath('wrong', null), throwsFormatException); + expect( + () => AssetPath('wrong', null), + throwsA(predicate( + (e) => e is FormatException && e.message.contains('Unknown pathType'), + )), + ); }); test('Asset hashCode copyWith', () async { diff --git a/pkgs/native_assets_cli/test/model/build_config_test.dart b/pkgs/native_assets_cli/test/model/build_config_test.dart index 9b3ecfac3..780a65c5f 100644 --- a/pkgs/native_assets_cli/test/model/build_config_test.dart +++ b/pkgs/native_assets_cli/test/model/build_config_test.dart @@ -63,6 +63,7 @@ void main() async { outDir: outDir2Uri, packageRoot: tempUri, target: Target.androidArm64, + targetAndroidNdkApi: 30, linkModePreference: LinkModePreference.preferStatic, ); @@ -88,6 +89,7 @@ void main() async { outDir: outDirUri, packageRoot: packageRootUri, target: Target.androidArm64, + targetAndroidNdkApi: 30, linkModePreference: LinkModePreference.preferStatic, ); @@ -95,6 +97,7 @@ void main() async { 'out_dir': outDirUri.toFilePath(), 'package_root': packageRootUri.toFilePath(), 'target': 'android_arm64', + 'target_android_ndk_api': 30, 'link_mode_preference': 'prefer-static', 'version': BuildOutput.version.toString(), }); @@ -127,6 +130,7 @@ void main() async { outDir: outDirUri, packageRoot: tempUri, target: Target.androidArm64, + targetAndroidNdkApi: 30, linkModePreference: LinkModePreference.preferStatic, dependencyMetadata: { 'bar': Metadata({ @@ -143,6 +147,7 @@ void main() async { outDir: outDirUri, packageRoot: tempUri, target: Target.androidArm64, + targetAndroidNdkApi: 30, linkModePreference: LinkModePreference.preferStatic, dependencyMetadata: { 'bar': Metadata({ @@ -213,28 +218,67 @@ version: ${BuildConfig.version}'''; test('BuildConfig FormatExceptions', () { expect( () => BuildConfig.fromConfig(Config(fileParsed: {})), - throwsFormatException, + throwsA(predicate( + (e) => + e is FormatException && + e.message.contains( + 'No value was provided for required key: target', + ), + )), ); expect( () => BuildConfig.fromConfig(Config(fileParsed: { + 'version': BuildConfig.version.toString(), 'package_root': packageRootUri.toFilePath(), 'target': 'android_arm64', + 'target_android_ndk_api': 30, 'link_mode_preference': 'prefer-static', })), - throwsFormatException, + throwsA(predicate( + (e) => + e is FormatException && + e.message.contains( + 'No value was provided for required key: out_dir', + ), + )), ); expect( () => BuildConfig.fromConfig(Config(fileParsed: { + 'version': BuildConfig.version.toString(), 'out_dir': outDirUri.toFilePath(), 'package_root': packageRootUri.toFilePath(), 'target': 'android_arm64', + 'target_android_ndk_api': 30, 'link_mode_preference': 'prefer-static', 'dependency_metadata': { 'bar': {'key': 'value'}, 'foo': [], }, })), - throwsFormatException, + throwsA(predicate( + (e) => + e is FormatException && + e.message.contains( + "Unexpected value '[]' for key 'dependency_metadata.foo' in " + 'config file. Expected a Map.', + ), + )), + ); + expect( + () => BuildConfig.fromConfig(Config(fileParsed: { + 'out_dir': outDirUri.toFilePath(), + 'version': BuildConfig.version.toString(), + 'package_root': packageRootUri.toFilePath(), + 'target': 'android_arm64', + 'link_mode_preference': 'prefer-static', + })), + throwsA(predicate( + (e) => + e is FormatException && + e.message.contains( + 'No value was provided for required key: target_android_ndk_api', + ), + )), ); }); @@ -271,6 +315,7 @@ version: ${BuildConfig.version}'''; outDir: outDirUri, packageRoot: tempUri, target: Target.androidArm64, + targetAndroidNdkApi: 30, linkModePreference: LinkModePreference.preferStatic, ); final configFileContents = buildConfig.toYamlString(); @@ -289,6 +334,7 @@ version: ${BuildConfig.version}'''; outDir: outDirUri, packageRoot: tempUri, target: Target.androidArm64, + targetAndroidNdkApi: 30, linkModePreference: LinkModePreference.preferStatic, dependencyMetadata: { 'bar': Metadata({ @@ -337,7 +383,15 @@ version: ${BuildConfig.version}'''; 'target': 'linux_x64', 'version': version, }); - expect(() => BuildConfig.fromConfig(config), throwsFormatException); + expect( + () => BuildConfig.fromConfig(config), + throwsA(predicate( + (e) => + e is FormatException && + e.message.contains(version) && + e.message.contains(BuildConfig.version.toString()), + )), + ); }); } @@ -354,7 +408,7 @@ version: ${BuildConfig.version}'''; ); // Using the checksum for a build folder should be stable. - expect(name1, '96819d83ae789cb65752986a4abb4071'); + expect(name1, '02dce8b58210deaf9f278772e892d01f'); // Build folder different due to metadata. final name2 = BuildConfig.checksum( diff --git a/pkgs/native_assets_cli/test/model/build_output_test.dart b/pkgs/native_assets_cli/test/model/build_output_test.dart index c99a7befa..cf9a8892c 100644 --- a/pkgs/native_assets_cli/test/model/build_output_test.dart +++ b/pkgs/native_assets_cli/test/model/build_output_test.dart @@ -100,7 +100,12 @@ version: ${BuildOutput.version}'''; test('BuildOutput version $version', () { expect( () => BuildOutput.fromYamlString('version: $version'), - throwsFormatException, + throwsA(predicate( + (e) => + e is FormatException && + e.message.contains(version) && + e.message.contains(BuildConfig.version.toString()), + )), ); }); } diff --git a/pkgs/native_assets_cli/test/model/target_test.dart b/pkgs/native_assets_cli/test/model/target_test.dart index ce7467266..1412c8a76 100644 --- a/pkgs/native_assets_cli/test/model/target_test.dart +++ b/pkgs/native_assets_cli/test/model/target_test.dart @@ -27,11 +27,26 @@ void main() { test('Target fromDartPlatform', () async { final current = Target.fromDartPlatform(Platform.version); expect(current.toString(), Abi.current().toString()); - expect(() => Target.fromDartPlatform('bogus'), throwsFormatException); expect( - () => Target.fromDartPlatform( - '3.0.0 (be) (Wed Apr 5 14:19:42 2023 +0000) on "myfancyos_ia32"'), - throwsFormatException); + () => Target.fromDartPlatform('bogus'), + throwsA(predicate( + (e) => + e is FormatException && + e.message.contains('bogus') && + e.message.contains('Unknown version'), + )), + ); + expect( + () => Target.fromDartPlatform( + '3.0.0 (be) (Wed Apr 5 14:19:42 2023 +0000) on "myfancyos_ia32"', + ), + throwsA(predicate( + (e) => + e is FormatException && + e.message.contains('myfancyos_ia32') && + e.message.contains('Unknown ABI'), + )), + ); }); test('Target cross compilation', () async {