Unverified Commit 6ad75553 authored by Daco Harkes's avatar Daco Harkes Committed by GitHub

Native assets support for Android (#135148)

Support for FFI calls with `@Native external` functions through Native assets on Android. This enables bundling native code without any build-system boilerplate code.

For more info see:

* https://github.com/flutter/flutter/issues/129757

### Implementation details for Android.

Mainly follows the design of the previous PRs.

For Android, we detect the compilers inside the NDK inside SDK.

And bundling of the assets is done by the flutter.groovy file.

The `minSdkVersion` is propagated from the flutter.groovy file as well.

The NDK is not part of `flutter doctor`, and users can omit it if no native assets have to be build.
However, if any native assets must be built, flutter throws a tool exit if the NDK is not installed.

Add 2 app is not part of this PR yet, instead `flutter build aar` will tool exit if there are any native assets.
parent da7e5e34
......@@ -2404,6 +2404,16 @@ targets:
["devicelab", "android", "linux"]
task_name: list_text_layout_impeller_perf__e2e_summary
- name: Linux_android native_assets_android
recipe: devicelab/devicelab_drone
presubmit: false
bringup: true
timeout: 60
properties:
tags: >
["devicelab", "android", "linux"]
task_name: native_assets_android
- name: Linux_android new_gallery__crane_perf
recipe: devicelab/devicelab_drone
presubmit: false
......@@ -3797,6 +3807,16 @@ targets:
["devicelab", "android", "mac"]
task_name: microbenchmarks
- name: Mac_android native_assets_android
recipe: devicelab/devicelab_drone
presubmit: false
bringup: true
timeout: 60
properties:
tags: >
["devicelab", "android", "mac"]
task_name: native_assets_android
- name: Mac_android run_debug_test_android
recipe: devicelab/devicelab_drone
presubmit: false
......@@ -5525,6 +5545,16 @@ targets:
["devicelab", "android", "windows"]
task_name: hot_mode_dev_cycle_win__benchmark
- name: Windows_android native_assets_android
recipe: devicelab/devicelab_drone
presubmit: false
bringup: true
timeout: 60
properties:
tags: >
["devicelab", "android", "windows"]
task_name: native_assets_android
- name: Windows_android windows_chrome_dev_mode
recipe: devicelab/devicelab_drone
presubmit: false
......
......@@ -202,6 +202,7 @@
/dev/devicelab/bin/tasks/large_image_changer_perf_ios.dart @zanderso @flutter/engine
/dev/devicelab/bin/tasks/microbenchmarks_ios.dart @vashworth @flutter/engine
/dev/devicelab/bin/tasks/microbenchmarks_ios_xcode_debug.dart @vashworth @flutter/engine
/dev/devicelab/bin/tasks/native_assets_android.dart @dacoharkes @flutter/android
/dev/devicelab/bin/tasks/native_assets_ios.dart @dacoharkes @flutter/ios
/dev/devicelab/bin/tasks/native_platform_view_ui_tests_ios.dart @hellohuanlin @flutter/ios
/dev/devicelab/bin/tasks/new_gallery_ios__transition_perf.dart @zanderso @flutter/engine
......
......@@ -43,6 +43,32 @@ Future<void> main() async {
);
});
section('Create package with native assets');
await flutter(
'config',
options: <String>['--enable-native-assets'],
);
const String ffiPackageName = 'ffi_package';
await createFfiPackage(ffiPackageName, tempDir);
section('Add FFI package');
final File pubspec = File(path.join(projectDir.path, 'pubspec.yaml'));
String content = await pubspec.readAsString();
content = content.replaceFirst(
'dependencies:$platformLineSep',
'dependencies:$platformLineSep $ffiPackageName:$platformLineSep path: ..${Platform.pathSeparator}$ffiPackageName$platformLineSep',
);
await pubspec.writeAsString(content, flush: true);
await inDirectory(projectDir, () async {
await flutter(
'packages',
options: <String>['get'],
);
});
section('Add read-only asset');
final File readonlyTxtAssetFile = await File(path.join(
......@@ -63,8 +89,6 @@ Future<void> main() async {
]);
}
final File pubspec = File(path.join(projectDir.path, 'pubspec.yaml'));
String content = await pubspec.readAsString();
content = content.replaceFirst(
'$platformLineSep # assets:$platformLineSep',
'$platformLineSep assets:$platformLineSep - assets/read-only.txt$platformLineSep',
......@@ -73,7 +97,6 @@ Future<void> main() async {
section('Add plugins');
content = await pubspec.readAsString();
content = content.replaceFirst(
'${platformLineSep}dependencies:$platformLineSep',
'${platformLineSep}dependencies:$platformLineSep device_info: 2.0.3$platformLineSep package_info: 2.0.2$platformLineSep',
......@@ -86,6 +109,45 @@ Future<void> main() async {
);
});
// TODO(dacoharkes): Implement Add2app. https://github.com/flutter/flutter/issues/129757
section('Check native assets error');
await inDirectory(Directory(path.join(projectDir.path, '.android')),
() async {
final StringBuffer stderr = StringBuffer();
final int exitCode = await exec(
gradlewExecutable,
<String>['flutter:assembleDebug'],
environment: <String, String>{'JAVA_HOME': javaHome},
canFail: true,
stderr: stderr,
);
const String errorString =
'Native assets are not yet supported in Android add2app.';
if (!stderr.toString().contains(errorString) || exitCode == 0) {
throw TaskResult.failure(
'''
Expected to find `$errorString` in stderr and nonZero exit code.
$stderr
exitCode: $exitCode
''');
}
});
section('Remove FFI package');
content = content.replaceFirst(
' $ffiPackageName:$platformLineSep path: ..${Platform.pathSeparator}$ffiPackageName$platformLineSep',
'',
);
await pubspec.writeAsString(content, flush: true);
await inDirectory(projectDir, () async {
await flutter(
'packages',
options: <String>['get'],
);
});
section('Build Flutter module library archive');
await inDirectory(Directory(path.join(projectDir.path, '.android')), () async {
......
......@@ -67,7 +67,7 @@ Future<void> main() async {
);
const String ffiPackageName = 'ffi_package';
await _createFfiPackage(ffiPackageName, tempDir);
await createFfiPackage(ffiPackageName, tempDir);
section('Add FFI package');
......@@ -730,30 +730,3 @@ class $dartPluginClass {
// Remove the native plugin code.
await Directory(path.join(pluginDir, 'ios')).delete(recursive: true);
}
Future<void> _createFfiPackage(String name, Directory parent) async {
await inDirectory(parent, () async {
await flutter(
'create',
options: <String>[
'--no-pub',
'--org',
'io.flutter.devicelab',
'--template=package_ffi',
name,
],
);
await _pinDependencies(
File(path.join(parent.path, name, 'pubspec.yaml')),
);
await _pinDependencies(
File(path.join(parent.path, name, 'example', 'pubspec.yaml')),
);
});
}
Future<void> _pinDependencies(File pubspecFile) async {
final String oldPubspec = await pubspecFile.readAsString();
final String newPubspec = oldPubspec.replaceAll(': ^', ': ');
await pubspecFile.writeAsString(newPubspec);
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_devicelab/framework/devices.dart';
import 'package:flutter_devicelab/framework/framework.dart';
import 'package:flutter_devicelab/tasks/native_assets_test.dart';
Future<void> main() async {
await task(() async {
deviceOperatingSystem = DeviceOperatingSystem.android;
return createNativeAssetsTest()();
});
}
......@@ -339,6 +339,8 @@ Future<int> exec(
Map<String, String>? environment,
bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
String? workingDirectory,
StringBuffer? output, // if not null, the stdout will be written here
StringBuffer? stderr, // if not null, the stderr will be written here
}) async {
return _execute(
executable,
......@@ -346,6 +348,8 @@ Future<int> exec(
environment: environment,
canFail : canFail,
workingDirectory: workingDirectory,
output: output,
stderr: stderr,
);
}
......@@ -898,3 +902,30 @@ Future<T> retry<T>(
await Future<void>.delayed(delayDuration);
}
}
Future<void> createFfiPackage(String name, Directory parent) async {
await inDirectory(parent, () async {
await flutter(
'create',
options: <String>[
'--no-pub',
'--org',
'io.flutter.devicelab',
'--template=package_ffi',
name,
],
);
await _pinDependencies(
File(path.join(parent.path, name, 'pubspec.yaml')),
);
await _pinDependencies(
File(path.join(parent.path, name, 'example', 'pubspec.yaml')),
);
});
}
Future<void> _pinDependencies(File pubspecFile) async {
final String oldPubspec = await pubspecFile.readAsString();
final String newPubspec = oldPubspec.replaceAll(': ^', ': ');
await pubspecFile.writeAsString(newPubspec);
}
......@@ -1032,6 +1032,15 @@ class FlutterPlugin implements Plugin<Project> {
}
}
}
// Build an AAR when this property is defined.
boolean isBuildingAar = project.hasProperty('is-plugin')
// In add to app scenarios, a Gradle project contains a `:flutter` and `:app` project.
// `:flutter` is used as a subproject when these tasks exists and the build isn't building an AAR.
Task packageAssets = project.tasks.findByPath(":flutter:package${variant.name.capitalize()}Assets")
Task cleanPackageAssets = project.tasks.findByPath(":flutter:cleanPackage${variant.name.capitalize()}Assets")
boolean isUsedAsSubproject = packageAssets && cleanPackageAssets && !isBuildingAar
boolean isAndroidLibraryValue = isBuildingAar || isUsedAsSubproject
String variantBuildMode = buildModeFor(variant.buildType)
String taskName = toCamelCase(["compile", FLUTTER_BUILD_PREFIX, variant.name])
// Be careful when configuring task below, Groovy has bizarre
......@@ -1044,6 +1053,7 @@ class FlutterPlugin implements Plugin<Project> {
flutterRoot this.flutterRoot
flutterExecutable this.flutterExecutable
buildMode variantBuildMode
minSdkVersion variant.mergedFlavor.minSdkVersion.apiLevel
localEngine this.localEngine
localEngineHost this.localEngineHost
localEngineSrcPath this.localEngineSrcPath
......@@ -1068,6 +1078,7 @@ class FlutterPlugin implements Plugin<Project> {
codeSizeDirectory codeSizeDirectoryValue
deferredComponents deferredComponentsValue
validateDeferredComponents validateDeferredComponentsValue
isAndroidLibrary isAndroidLibraryValue
doLast {
project.exec {
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
......@@ -1097,13 +1108,6 @@ class FlutterPlugin implements Plugin<Project> {
addApiDependencies(project, variant.name, project.files {
packFlutterAppAotTask
})
// We build an AAR when this property is defined.
boolean isBuildingAar = project.hasProperty('is-plugin')
// In add to app scenarios, a Gradle project contains a `:flutter` and `:app` project.
// We know that `:flutter` is used as a subproject when these tasks exists and we aren't building an AAR.
Task packageAssets = project.tasks.findByPath(":flutter:package${variant.name.capitalize()}Assets")
Task cleanPackageAssets = project.tasks.findByPath(":flutter:cleanPackage${variant.name.capitalize()}Assets")
boolean isUsedAsSubproject = packageAssets && cleanPackageAssets && !isBuildingAar
Task copyFlutterAssetsTask = project.tasks.create(
name: "copyFlutterAssets${variant.name.capitalize()}",
type: Copy,
......@@ -1194,6 +1198,9 @@ class FlutterPlugin implements Plugin<Project> {
}
}
}
// Copy the native assets created by build.dart and placed here by flutter assemble.
def nativeAssetsDir = "${project.buildDir}/../native_assets/android/jniLibs/lib/"
project.android.sourceSets.main.jniLibs.srcDir nativeAssetsDir
}
configurePlugins()
detectLowCompileSdkVersionOrNdkVersion()
......@@ -1219,7 +1226,7 @@ class FlutterPlugin implements Plugin<Project> {
// | ----------------- | ----------------------------- |
// | Build Variant | Flutter Equivalent Variant |
// | ----------------- | ----------------------------- |
// | freeRelease | release |
// | freeRelease | release |
// | freeDebug | debug |
// | freeDevelop | debug |
// | profile | profile |
......@@ -1277,6 +1284,8 @@ abstract class BaseFlutterTask extends DefaultTask {
File flutterExecutable
@Input
String buildMode
@Input
int minSdkVersion
@Optional @Input
String localEngine
@Optional @Input
......@@ -1325,6 +1334,8 @@ abstract class BaseFlutterTask extends DefaultTask {
Boolean deferredComponents
@Optional @Input
Boolean validateDeferredComponents
@Optional @Input
Boolean isAndroidLibrary
@OutputFiles
FileCollection getDependenciesFiles() {
......@@ -1414,6 +1425,11 @@ abstract class BaseFlutterTask extends DefaultTask {
if (extraFrontEndOptions != null) {
args "--ExtraFrontEndOptions=${extraFrontEndOptions}"
}
args "-dAndroidArchs=${targetPlatformValues.join(' ')}"
args "-dMinSdkVersion=${minSdkVersion}"
if (isAndroidLibrary != null) {
args "-dIsAndroidLibrary=${isAndroidLibrary ? "true" : "false"}"
}
args ruleNames
}
}
......
......@@ -3,7 +3,9 @@
// found in the LICENSE file.
import '../base/common.dart';
import '../base/config.dart';
import '../base/file_system.dart';
import '../base/platform.dart';
import '../base/process.dart';
import '../base/version.dart';
import '../convert.dart';
......@@ -15,6 +17,13 @@ import 'java.dart';
const String kAndroidSdkRoot = 'ANDROID_SDK_ROOT';
const String kAndroidHome = 'ANDROID_HOME';
// No official environment variable for the NDK root is documented:
// https://developer.android.com/tools/variables#envar
// The follow three seem to be most commonly used.
const String kAndroidNdkHome = 'ANDROID_NDK_HOME';
const String kAndroidNdkPath = 'ANDROID_NDK_PATH';
const String kAndroidNdkRoot = 'ANDROID_NDK_ROOT';
final RegExp _numberedAndroidPlatformRe = RegExp(r'^android-([0-9]+)$');
final RegExp _sdkVersionRe = RegExp(r'^ro.build.version.sdk=([0-9]+)$');
......@@ -34,8 +43,9 @@ final RegExp _sdkVersionRe = RegExp(r'^ro.build.version.sdk=([0-9]+)$');
class AndroidSdk {
AndroidSdk(this.directory, {
Java? java,
FileSystem? fileSystem,
}): _java = java {
reinitialize();
reinitialize(fileSystem: fileSystem);
}
/// The Android SDK root directory.
......@@ -320,11 +330,121 @@ class AndroidSdk {
String? getAvdManagerPath() => getCmdlineToolsPath(globals.platform.isWindows ? 'avdmanager.bat' : 'avdmanager');
/// From https://developer.android.com/ndk/guides/other_build_systems.
static const Map<String, String> _llvmHostDirectoryName = <String, String>{
'macos': 'darwin-x86_64',
'linux': 'linux-x86_64',
'windows': 'windows-x86_64',
};
/// Locates the binary path for an NDK binary.
///
/// The order of resolution is as follows:
///
/// 1. If [globals.config] defines an `'android-ndk'` use that.
/// 2. If the environment variable `ANDROID_NDK_HOME` is defined, use that.
/// 3. If the environment variable `ANDROID_NDK_PATH` is defined, use that.
/// 4. If the environment variable `ANDROID_NDK_ROOT` is defined, use that.
/// 5. Look for the default install location inside the Android SDK:
/// [directory]/ndk/\<version\>/. If multiple versions exist, use the
/// newest.
String? getNdkBinaryPath(
String binaryName, {
Platform? platform,
Config? config,
}) {
platform ??= globals.platform;
config ??= globals.config;
Directory? findAndroidNdkHomeDir() {
String? androidNdkHomeDir;
if (config!.containsKey('android-ndk')) {
androidNdkHomeDir = config.getValue('android-ndk') as String?;
} else if (platform!.environment.containsKey(kAndroidNdkHome)) {
androidNdkHomeDir = platform.environment[kAndroidNdkHome];
} else if (platform.environment.containsKey(kAndroidNdkPath)) {
androidNdkHomeDir = platform.environment[kAndroidNdkPath];
} else if (platform.environment.containsKey(kAndroidNdkRoot)) {
androidNdkHomeDir = platform.environment[kAndroidNdkRoot];
}
if (androidNdkHomeDir != null) {
return directory.fileSystem.directory(androidNdkHomeDir);
}
// Look for the default install location of the NDK inside the Android
// SDK when installed through `sdkmanager` or Android studio.
final Directory ndk = directory.childDirectory('ndk');
if (!ndk.existsSync()) {
return null;
}
final List<Version> ndkVersions = ndk
.listSync()
.map((FileSystemEntity entity) {
try {
return Version.parse(entity.basename);
} on Exception {
return null;
}
})
.whereType<Version>()
.toList()
// Use latest NDK first.
..sort((Version a, Version b) => -a.compareTo(b));
if (ndkVersions.isEmpty) {
return null;
}
return ndk.childDirectory(ndkVersions.first.toString());
}
final Directory? androidNdkHomeDir = findAndroidNdkHomeDir();
if (androidNdkHomeDir == null) {
return null;
}
final File executable = androidNdkHomeDir
.childDirectory('toolchains')
.childDirectory('llvm')
.childDirectory('prebuilt')
.childDirectory(_llvmHostDirectoryName[platform.operatingSystem]!)
.childDirectory('bin')
.childFile(binaryName);
if (executable.existsSync()) {
// LLVM missing in this NDK version.
return executable.path;
}
return null;
}
String? getNdkClangPath({Platform? platform, Config? config}) {
platform ??= globals.platform;
return getNdkBinaryPath(
platform.isWindows ? 'clang.exe' : 'clang',
platform: platform,
config: config,
);
}
String? getNdkArPath({Platform? platform, Config? config}) {
platform ??= globals.platform;
return getNdkBinaryPath(
platform.isWindows ? 'llvm-ar.exe' : 'llvm-ar',
platform: platform,
config: config,
);
}
String? getNdkLdPath({Platform? platform, Config? config}) {
platform ??= globals.platform;
return getNdkBinaryPath(
platform.isWindows ? 'ld.lld.exe' : 'ld.lld',
platform: platform,
config: config,
);
}
/// Sets up various paths used internally.
///
/// This method should be called in a case where the tooling may have updated
/// SDK artifacts, such as after running a gradle build.
void reinitialize() {
void reinitialize({FileSystem? fileSystem}) {
List<Version> buildTools = <Version>[]; // 19.1.0, 22.0.1, ...
final Directory buildToolsDir = directory.childDirectory('build-tools');
......@@ -387,7 +507,7 @@ class AndroidSdk {
sdkLevel: platformVersion,
platformName: platformName,
buildToolsVersion: buildToolsVersion,
fileSystem: globals.fs,
fileSystem: fileSystem ?? globals.fs,
);
}).whereType<AndroidSdkVersion>().toList();
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:native_assets_builder/native_assets_builder.dart'
show BuildResult, DryRunResult;
import 'package:native_assets_cli/native_assets_cli.dart' hide BuildMode;
import 'package:native_assets_cli/native_assets_cli.dart' as native_assets_cli;
import '../base/common.dart';
import '../base/file_system.dart';
import '../build_info.dart';
import '../globals.dart' as globals;
import '../native_assets.dart';
import 'android_sdk.dart';
/// Dry run the native builds.
///
/// This does not build native assets, it only simulates what the final paths
/// of all assets will be so that this can be embedded in the kernel file.
Future<Uri?> dryRunNativeAssetsAndroid({
required NativeAssetsBuildRunner buildRunner,
required Uri projectUri,
bool flutterTester = false,
required FileSystem fileSystem,
}) async {
if (!await nativeBuildRequired(buildRunner)) {
return null;
}
final Uri buildUri_ = nativeAssetsBuildUri(projectUri, OS.android);
final Iterable<Asset> nativeAssetPaths =
await dryRunNativeAssetsAndroidInternal(
fileSystem,
projectUri,
buildRunner,
);
final Uri nativeAssetsUri = await writeNativeAssetsYaml(
nativeAssetPaths,
buildUri_,
fileSystem,
);
return nativeAssetsUri;
}
Future<Iterable<Asset>> dryRunNativeAssetsAndroidInternal(
FileSystem fileSystem,
Uri projectUri,
NativeAssetsBuildRunner buildRunner,
) async {
const OS targetOS = OS.android;
globals.logger.printTrace('Dry running native assets for $targetOS.');
final DryRunResult dryRunResult = await buildRunner.dryRun(
linkModePreference: LinkModePreference.dynamic,
targetOS: targetOS,
workingDirectory: projectUri,
includeParentEnvironment: true,
);
ensureNativeAssetsBuildSucceed(dryRunResult);
final List<Asset> nativeAssets = dryRunResult.assets;
ensureNoLinkModeStatic(nativeAssets);
globals.logger.printTrace('Dry running native assets for $targetOS done.');
final Map<Asset, Asset> assetTargetLocations =
_assetTargetLocations(nativeAssets);
final Iterable<Asset> nativeAssetPaths = assetTargetLocations.values;
return nativeAssetPaths;
}
/// Builds native assets.
Future<(Uri? nativeAssetsYaml, List<Uri> dependencies)>
buildNativeAssetsAndroid({
required NativeAssetsBuildRunner buildRunner,
required Iterable<AndroidArch> androidArchs,
required Uri projectUri,
required BuildMode buildMode,
String? codesignIdentity,
Uri? yamlParentDirectory,
required FileSystem fileSystem,
required int targetAndroidNdkApi,
bool isAndroidLibrary = false,
}) async {
const OS targetOS = OS.android;
final Uri buildUri_ = nativeAssetsBuildUri(projectUri, targetOS);
if (!await nativeBuildRequired(buildRunner)) {
final Uri nativeAssetsYaml = await writeNativeAssetsYaml(
<Asset>[],
yamlParentDirectory ?? buildUri_,
fileSystem,
);
return (nativeAssetsYaml, <Uri>[]);
}
final List<Target> targets = androidArchs.map(_getNativeTarget).toList();
final native_assets_cli.BuildMode buildModeCli =
nativeAssetsBuildMode(buildMode);
globals.logger
.printTrace('Building native assets for $targets $buildModeCli.');
final List<Asset> nativeAssets = <Asset>[];
final Set<Uri> dependencies = <Uri>{};
for (final Target target in targets) {
final BuildResult result = await buildRunner.build(
linkModePreference: LinkModePreference.dynamic,
target: target,
buildMode: buildModeCli,
workingDirectory: projectUri,
includeParentEnvironment: true,
cCompilerConfig: await buildRunner.ndkCCompilerConfig,
targetAndroidNdkApi: targetAndroidNdkApi,
);
ensureNativeAssetsBuildSucceed(result);
nativeAssets.addAll(result.assets);
dependencies.addAll(result.dependencies);
}
ensureNoLinkModeStatic(nativeAssets);
globals.logger.printTrace('Building native assets for $targets done.');
if (isAndroidLibrary && nativeAssets.isNotEmpty) {
throwToolExit('Native assets are not yet supported in Android add2app.');
}
final Map<Asset, Asset> assetTargetLocations =
_assetTargetLocations(nativeAssets);
await _copyNativeAssetsAndroid(buildUri_, assetTargetLocations, fileSystem);
final Uri nativeAssetsUri = await writeNativeAssetsYaml(
assetTargetLocations.values,
yamlParentDirectory ?? buildUri_,
fileSystem);
return (nativeAssetsUri, dependencies.toList());
}
Future<void> _copyNativeAssetsAndroid(
Uri buildUri,
Map<Asset, Asset> assetTargetLocations,
FileSystem fileSystem,
) async {
if (assetTargetLocations.isNotEmpty) {
globals.logger
.printTrace('Copying native assets to ${buildUri.toFilePath()}.');
final List<String> jniArchDirs = <String>[
for (final AndroidArch androidArch in AndroidArch.values)
androidArch.archName,
];
for (final String jniArchDir in jniArchDirs) {
final Uri archUri = buildUri.resolve('jniLibs/lib/$jniArchDir/');
await fileSystem.directory(archUri).create(recursive: true);
}
for (final MapEntry<Asset, Asset> assetMapping
in assetTargetLocations.entries) {
final Uri source = (assetMapping.key.path as AssetAbsolutePath).uri;
final Uri target = (assetMapping.value.path as AssetAbsolutePath).uri;
final AndroidArch androidArch =
_getAndroidArch(assetMapping.value.target);
final String jniArchDir = androidArch.archName;
final Uri archUri = buildUri.resolve('jniLibs/lib/$jniArchDir/');
final Uri targetUri = archUri.resolveUri(target);
final String targetFullPath = targetUri.toFilePath();
await fileSystem.file(source).copy(targetFullPath);
}
globals.logger.printTrace('Copying native assets done.');
}
}
/// Get the [Target] for [androidArch].
Target _getNativeTarget(AndroidArch androidArch) {
switch (androidArch) {
case AndroidArch.armeabi_v7a:
return Target.androidArm;
case AndroidArch.arm64_v8a:
return Target.androidArm64;
case AndroidArch.x86:
return Target.androidIA32;
case AndroidArch.x86_64:
return Target.androidX64;
}
}
/// Get the [AndroidArch] for [target].
AndroidArch _getAndroidArch(Target target) {
switch (target) {
case Target.androidArm:
return AndroidArch.armeabi_v7a;
case Target.androidArm64:
return AndroidArch.arm64_v8a;
case Target.androidIA32:
return AndroidArch.x86;
case Target.androidX64:
return AndroidArch.x86_64;
case Target.androidRiscv64:
throwToolExit('Android RISC-V not yet supported.');
default:
throwToolExit('Invalid target: $target.');
}
}
Map<Asset, Asset> _assetTargetLocations(List<Asset> nativeAssets) {
return <Asset, Asset>{
for (final Asset asset in nativeAssets)
asset: _targetLocationAndroid(asset),
};
}
/// Converts the `path` of [asset] as output from a `build.dart` invocation to
/// the path used inside the Flutter app bundle.
Asset _targetLocationAndroid(Asset asset) {
final AssetPath path = asset.path;
switch (path) {
case AssetSystemPath _:
case AssetInExecutable _:
case AssetInProcess _:
return asset;
case AssetAbsolutePath _:
final String fileName = path.uri.pathSegments.last;
return asset.copyWith(path: AssetAbsolutePath(Uri(path: fileName)));
}
throw Exception(
'Unsupported asset path type ${path.runtimeType} in asset $asset',
);
}
/// Looks the NDK clang compiler tools.
///
/// Tool-exits if the NDK cannot be found.
///
/// Should only be invoked if a native assets build is performed. If the native
/// assets feature is disabled, or none of the packages have native assets, a
/// missing NDK is okay.
@override
Future<CCompilerConfig> cCompilerConfigAndroid() async {
final AndroidSdk? androidSdk = AndroidSdk.locateAndroidSdk();
if (androidSdk == null) {
throwToolExit('Android SDK could not be found.');
}
final CCompilerConfig result = CCompilerConfig(
cc: _toOptionalFileUri(androidSdk.getNdkClangPath()),
ar: _toOptionalFileUri(androidSdk.getNdkArPath()),
ld: _toOptionalFileUri(androidSdk.getNdkLdPath()),
);
if (result.cc == null || result.ar == null || result.ld == null) {
throwToolExit('Android NDK Clang could not be found.');
}
return result;
}
Uri? _toOptionalFileUri(String? string) {
if (string == null) {
return null;
}
return Uri.file(string);
}
......@@ -925,6 +925,31 @@ const String kIosArchs = 'IosArchs';
/// Supported values are x86_64 and arm64.
const String kDarwinArchs = 'DarwinArchs';
/// The define to control what Android architectures are built for.
///
/// This is expected to be a space-delimited list of architectures.
const String kAndroidArchs = 'AndroidArchs';
/// If the current build is `flutter build aar`.
///
/// This is expected to be a boolean.
///
/// If not provided, defaults to false.
const String kIsAndroidLibrary = 'IsAndroidLibrary';
/// The define to control what min Android SDK version is built for.
///
/// This is expected to be int.
///
/// If not provided, defaults to `minSdkVersion` from gradle_utils.dart.
///
/// This is passed in by flutter.groovy's invocation of `flutter assemble`.
///
/// For more info, see:
/// https://developer.android.com/ndk/guides/sdk-versions#minsdkversion
/// https://developer.android.com/ndk/guides/other_build_systems#overview
const String kMinSdkVersion = 'MinSdkVersion';
/// Path to the SDK root to be used as the isysroot.
const String kSdkRoot = 'SdkRoot';
......
......@@ -10,6 +10,7 @@ import 'package:native_assets_builder/native_assets_builder.dart' as native_asse
import 'package:native_assets_cli/native_assets_cli.dart';
import 'package:package_config/package_config_types.dart';
import 'android/native_assets.dart';
import 'base/common.dart';
import 'base/file_system.dart';
import 'base/logger.dart';
......@@ -61,6 +62,9 @@ abstract class NativeAssetsBuildRunner {
/// The C compiler config to use for compilation.
Future<CCompilerConfig> get cCompilerConfig;
/// The NDK compiler to use to use for compilation for Android.
Future<CCompilerConfig> get ndkCCompilerConfig;
}
/// Uses `package:native_assets_builder` for its implementation.
......@@ -174,9 +178,15 @@ class NativeAssetsBuildRunnerImpl implements NativeAssetsBuildRunner {
if (globals.platform.isWindows) {
return cCompilerConfigWindows();
}
throwToolExit(
'Native assets feature not yet implemented for Android.',
);
if (globals.platform.isAndroid) {
throwToolExit('Should use ndkCCompilerConfig for Android.');
}
throwToolExit('Unknown target OS.');
}();
@override
late final Future<CCompilerConfig> ndkCCompilerConfig = () {
return cCompilerConfigAndroid();
}();
}
......@@ -230,6 +240,9 @@ Future<bool> nativeBuildRequired(NativeAssetsBuildRunner buildRunner) async {
}
final List<Package> packagesWithNativeAssets = await buildRunner.packagesWithNativeAssets();
if (packagesWithNativeAssets.isEmpty) {
globals.logger.printTrace(
'No packages with native assets. Skipping native assets compilation.',
);
return false;
}
......@@ -258,6 +271,9 @@ Future<void> ensureNoNativeAssetsOrOsIsSupported(
}
final List<Package> packagesWithNativeAssets = await buildRunner.packagesWithNativeAssets();
if (packagesWithNativeAssets.isEmpty) {
globals.logger.printTrace(
'No packages with native assets. Skipping native assets compilation.',
);
return;
}
final String packageNames = packagesWithNativeAssets.map((Package p) => p.name).join(' ');
......@@ -374,6 +390,11 @@ Future<Uri?> dryRunNativeAssets({
case build_info.TargetPlatform.android_x64:
case build_info.TargetPlatform.android_x86:
case build_info.TargetPlatform.android:
nativeAssetsYaml = await dryRunNativeAssetsAndroid(
projectUri: projectUri,
fileSystem: fileSystem,
buildRunner: buildRunner,
);
case build_info.TargetPlatform.fuchsia_arm64:
case build_info.TargetPlatform.fuchsia_x64:
case build_info.TargetPlatform.web_javascript:
......@@ -433,7 +454,17 @@ Future<Uri?> dryRunNativeAssetsMultipeOSes({
fileSystem,
projectUri,
buildRunner,
)
),
if (targetPlatforms.contains(build_info.TargetPlatform.android) ||
targetPlatforms.contains(build_info.TargetPlatform.android_arm) ||
targetPlatforms.contains(build_info.TargetPlatform.android_arm64) ||
targetPlatforms.contains(build_info.TargetPlatform.android_x64) ||
targetPlatforms.contains(build_info.TargetPlatform.android_x86))
...await dryRunNativeAssetsAndroidInternal(
fileSystem,
projectUri,
buildRunner,
),
];
final Uri nativeAssetsUri = await writeNativeAssetsYaml(nativeAssetPaths, buildUri, fileSystem);
return nativeAssetsUri;
......
......@@ -342,6 +342,149 @@ void main() {
Config: () => config,
});
});
const Map<String, String> llvmHostDirectoryName = <String, String>{
'macos': 'darwin-x86_64',
'linux': 'linux-x86_64',
'windows': 'windows-x86_64',
};
for (final String operatingSystem in <String>['windows', 'linux', 'macos']) {
final FileSystem fileSystem;
final String extension;
if (operatingSystem == 'windows') {
fileSystem = MemoryFileSystem.test(style: FileSystemStyle.windows);
extension = '.exe';
} else {
fileSystem = MemoryFileSystem.test();
extension = '';
}
testWithoutContext('ndk executables $operatingSystem', () {
final Platform platform = FakePlatform(operatingSystem: operatingSystem);
final Directory sdkDir = createSdkDirectory(
fileSystem: fileSystem,
platform: platform,
);
config.setValue('android-sdk', sdkDir.path);
final AndroidSdk sdk = AndroidSdk(sdkDir, fileSystem: fileSystem);
late File clang;
late File ar;
late File ld;
const List<String> versions = <String>['22.1.7171670', '24.0.8215888'];
for (final String version in versions) {
final Directory binDir = sdk.directory
.childDirectory('ndk')
.childDirectory(version)
.childDirectory('toolchains')
.childDirectory('llvm')
.childDirectory('prebuilt')
.childDirectory(llvmHostDirectoryName[operatingSystem]!)
.childDirectory('bin')
..createSync(recursive: true);
// Save the last version.
clang = binDir.childFile('clang$extension')..createSync();
ar = binDir.childFile('llvm-ar$extension')..createSync();
ld = binDir.childFile('ld.lld$extension')..createSync();
}
// Check the last NDK version is used.
expect(
sdk.getNdkClangPath(platform: platform, config: config),
clang.path,
);
expect(
sdk.getNdkArPath(platform: platform, config: config),
ar.path,
);
expect(
sdk.getNdkLdPath(platform: platform, config: config),
ld.path,
);
});
for (final String envVar in <String>[
kAndroidNdkHome,
kAndroidNdkPath,
kAndroidNdkRoot,
]) {
final Directory ndkDir = fileSystem.systemTempDirectory
.createTempSync('flutter_mock_android_ndk.');
testWithoutContext('ndk executables with $operatingSystem $envVar', () {
final Platform platform = FakePlatform(
operatingSystem: operatingSystem,
environment: <String, String>{
envVar: ndkDir.path,
},
);
final Directory sdkDir =
createSdkDirectory(fileSystem: fileSystem, platform: platform);
config.setValue('android-sdk', sdkDir.path);
final Directory binDir = ndkDir
.childDirectory('toolchains')
.childDirectory('llvm')
.childDirectory('prebuilt')
.childDirectory(llvmHostDirectoryName[operatingSystem]!)
.childDirectory('bin')
..createSync(recursive: true);
final File clang = binDir.childFile('clang$extension')..createSync();
final File ar = binDir.childFile('llvm-ar$extension')..createSync();
final File ld = binDir.childFile('ld.lld$extension')..createSync();
final AndroidSdk sdk = AndroidSdk(sdkDir, fileSystem: fileSystem);
expect(
sdk.getNdkClangPath(platform: platform, config: config),
clang.path,
);
expect(
sdk.getNdkArPath(platform: platform, config: config),
ar.path,
);
expect(
sdk.getNdkLdPath(platform: platform, config: config),
ld.path,
);
});
}
testWithoutContext('ndk executables with config override $operatingSystem',
() {
final Platform platform = FakePlatform(operatingSystem: operatingSystem);
final Directory sdkDir = createSdkDirectory(
fileSystem: fileSystem,
platform: platform,
);
final Directory ndkDir = fileSystem.systemTempDirectory
.createTempSync('flutter_mock_android_ndk.');
config.setValue('android-sdk', sdkDir.path);
config.setValue('android-ndk', ndkDir.path);
final Directory binDir = ndkDir
.childDirectory('toolchains')
.childDirectory('llvm')
.childDirectory('prebuilt')
.childDirectory(llvmHostDirectoryName[operatingSystem]!)
.childDirectory('bin')
..createSync(recursive: true);
final File clang = binDir.childFile('clang$extension')..createSync();
final File ar = binDir.childFile('llvm-ar$extension')..createSync();
final File ld = binDir.childFile('ld.lld$extension')..createSync();
final AndroidSdk sdk = AndroidSdk(sdkDir, fileSystem: fileSystem);
expect(
sdk.getNdkClangPath(platform: platform, config: config),
clang.path,
);
expect(
sdk.getNdkArPath(platform: platform, config: config),
ar.path,
);
expect(
sdk.getNdkLdPath(platform: platform, config: config),
ld.path,
);
});
}
}
/// A broken SDK installation.
......@@ -379,10 +522,12 @@ Directory createSdkDirectory({
bool withBuildTools = true,
required FileSystem fileSystem,
String buildProp = _buildProp,
Platform? platform,
}) {
platform ??= globals.platform;
final Directory dir = fileSystem.systemTempDirectory.createTempSync('flutter_mock_android_sdk.');
final String exe = globals.platform.isWindows ? '.exe' : '';
final String bat = globals.platform.isWindows ? '.bat' : '';
final String exe = platform.isWindows ? '.exe' : '';
final String bat = platform.isWindows ? '.bat' : '';
void createDir(Directory dir, String path) {
final Directory directory = dir.fileSystem.directory(dir.fileSystem.path.join(dir.path, path));
......
......@@ -24,6 +24,7 @@ import '../../fake_native_assets_build_runner.dart';
void main() {
late FakeProcessManager processManager;
late Environment iosEnvironment;
late Environment androidEnvironment;
late Artifacts artifacts;
late FileSystem fileSystem;
late Logger logger;
......@@ -47,7 +48,21 @@ void main() {
fileSystem: fileSystem,
logger: logger,
);
androidEnvironment = Environment.test(
fileSystem.currentDirectory,
defines: <String, String>{
kBuildMode: BuildMode.profile.cliName,
kTargetPlatform: getNameForTargetPlatform(TargetPlatform.android),
kAndroidArchs: AndroidArch.arm64_v8a.platformName,
},
inputs: <String, String>{},
artifacts: artifacts,
processManager: processManager,
fileSystem: fileSystem,
logger: logger,
);
iosEnvironment.buildDir.createSync(recursive: true);
androidEnvironment.buildDir.createSync(recursive: true);
});
testWithoutContext('NativeAssets throws error if missing target platform', () async {
......@@ -155,6 +170,63 @@ void main() {
);
},
);
for (final bool isAndroidLibrary in <bool>[true, false]) {
for (final bool hasAssets in <bool>[true, false]) {
final String buildType = isAndroidLibrary ? 'aar' : 'not-aar';
final String withOrWithout = hasAssets ? 'with' : 'without';
final String throwsOrDoesntThrow =
(isAndroidLibrary && hasAssets) ? 'throws' : 'does not throw';
testUsingContext(
'flutter build $buildType $withOrWithout native assets $throwsOrDoesntThrow',
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
FeatureFlags: () => TestFeatureFlags(
isNativeAssetsEnabled: true,
),
},
() async {
await createPackageConfig(androidEnvironment);
await fileSystem.file('libfoo.so').create();
final NativeAssetsBuildRunner buildRunner =
FakeNativeAssetsBuildRunner(
packagesWithNativeAssetsResult: <Package>[
Package('foo', androidEnvironment.buildDir.uri)
],
buildResult:
FakeNativeAssetsBuilderResult(assets: <native_assets_cli.Asset>[
if (hasAssets)
native_assets_cli.Asset(
id: 'package:foo/foo.dart',
linkMode: native_assets_cli.LinkMode.dynamic,
target: native_assets_cli.Target.androidArm64,
path: native_assets_cli.AssetAbsolutePath(
Uri.file('libfoo.so'),
),
)
], dependencies: <Uri>[
Uri.file('src/foo.c'),
]),
);
if (isAndroidLibrary) {
androidEnvironment.defines[kIsAndroidLibrary] = 'true';
}
if (hasAssets && isAndroidLibrary) {
expect(
NativeAssets(buildRunner: buildRunner).build(androidEnvironment),
throwsToolExit(),
);
} else {
await NativeAssets(buildRunner: buildRunner)
.build(androidEnvironment);
}
},
);
}
}
}
Future<void> createPackageConfig(Environment iosEnvironment) async {
......
......@@ -1289,7 +1289,7 @@ class FakeAndroidSdk extends Fake implements AndroidSdk {
bool reinitialized = false;
@override
void reinitialize() {
void reinitialize({FileSystem? fileSystem}) {
reinitialized = true;
}
}
......
......@@ -17,13 +17,16 @@ class FakeNativeAssetsBuildRunner implements NativeAssetsBuildRunner {
this.dryRunResult = const FakeNativeAssetsBuilderResult(),
this.buildResult = const FakeNativeAssetsBuilderResult(),
CCompilerConfig? cCompilerConfigResult,
}) : cCompilerConfigResult = cCompilerConfigResult ?? CCompilerConfig();
CCompilerConfig? ndkCCompilerConfigResult,
}) : cCompilerConfigResult = cCompilerConfigResult ?? CCompilerConfig(),
ndkCCompilerConfigResult = ndkCCompilerConfigResult ?? CCompilerConfig();
final native_assets_builder.BuildResult buildResult;
final native_assets_builder.DryRunResult dryRunResult;
final bool hasPackageConfigResult;
final List<Package> packagesWithNativeAssetsResult;
final CCompilerConfig cCompilerConfigResult;
final CCompilerConfig ndkCCompilerConfigResult;
int buildInvocations = 0;
int dryRunInvocations = 0;
......@@ -70,6 +73,9 @@ class FakeNativeAssetsBuildRunner implements NativeAssetsBuildRunner {
@override
Future<CCompilerConfig> get cCompilerConfig async => cCompilerConfigResult;
@override
Future<CCompilerConfig> get ndkCCompilerConfig async => cCompilerConfigResult;
}
final class FakeNativeAssetsBuilderResult
......
......@@ -17,6 +17,8 @@ import 'dart:io';
import 'package:file/file.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart';
import 'package:native_assets_cli/native_assets_cli.dart';
import '../src/common.dart';
......@@ -33,6 +35,7 @@ final List<String> devices = <String>[
final List<String> buildSubcommands = <String>[
hostOs,
if (hostOs == 'macos') 'ios',
'apk',
];
final List<String> add2appBuildSubcommands = <String>[
......@@ -208,6 +211,8 @@ void main() {
expectDylibIsBundledLinux(exampleDirectory, buildMode);
} else if (buildSubcommand == 'windows') {
expectDylibIsBundledWindows(exampleDirectory, buildMode);
} else if (buildSubcommand == 'apk') {
expectDylibIsBundledAndroid(exampleDirectory, buildMode);
}
expectCCompilerIsConfigured(exampleDirectory);
});
......@@ -246,11 +251,11 @@ void main() {
],
workingDirectory: exampleDirectory.path,
);
expect(result.exitCode, isNot(0));
expect(
(result.stdout as String) + (result.stderr as String),
contains('link mode set to static, but this is not yet supported'),
);
expect(result.exitCode, isNot(0));
});
});
}
......@@ -338,6 +343,37 @@ void expectDylibIsBundledWindows(Directory appDirectory, String buildMode) {
expect(dylib, exists);
}
void expectDylibIsBundledAndroid(Directory appDirectory, String buildMode) {
final File apk = appDirectory
.childDirectory('build')
.childDirectory('app')
.childDirectory('outputs')
.childDirectory('flutter-apk')
.childFile('app-$buildMode.apk');
expect(apk, exists);
final OperatingSystemUtils osUtils = OperatingSystemUtils(
fileSystem: fileSystem,
logger: BufferLogger.test(),
platform: platform,
processManager: processManager,
);
final Directory apkUnzipped = appDirectory.childDirectory('apk-unzipped');
apkUnzipped.createSync();
osUtils.unzip(apk, apkUnzipped);
final Directory lib = apkUnzipped.childDirectory('lib');
for (final String arch in <String>['arm64-v8a', 'armeabi-v7a', 'x86_64']) {
final Directory archDir = lib.childDirectory(arch);
expect(archDir, exists);
// The dylibs should be next to the flutter and app so.
expect(archDir.childFile('libflutter.so'), exists);
if (buildMode != 'debug') {
expect(archDir.childFile('libapp.so'), exists);
}
final File dylib = archDir.childFile(OS.android.dylibFileName(packageName));
expect(dylib, exists);
}
}
/// For `flutter build` we can't easily test whether running the app works.
/// Check that we have the dylibs in the app.
void expectDylibIsBundledWithFrameworks(Directory appDirectory, String buildMode, String os) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment