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';
......
......@@ -6,6 +6,8 @@ import 'package:meta/meta.dart';
import 'package:native_assets_cli/native_assets_cli.dart' show Asset;
import 'package:package_config/package_config_types.dart';
import '../../android/gradle_utils.dart';
import '../../android/native_assets.dart';
import '../../base/common.dart';
import '../../base/file_system.dart';
import '../../base/platform.dart';
......@@ -78,84 +80,37 @@ class NativeAssets extends Target {
environment.logger,
);
switch (targetPlatform) {
case TargetPlatform.ios:
final List<DarwinArch> iosArchs =
_emptyToNull(environment.defines[kIosArchs])
?.split(' ')
.map(getIOSArchForName)
.toList()
?? <DarwinArch>[DarwinArch.arm64];
final String? environmentBuildMode = environment.defines[kBuildMode];
if (environmentBuildMode == null) {
throw MissingDefineException(kBuildMode, name);
}
final BuildMode buildMode = BuildMode.fromCliName(environmentBuildMode);
final String? sdkRoot = environment.defines[kSdkRoot];
if (sdkRoot == null) {
throw MissingDefineException(kSdkRoot, name);
}
final EnvironmentType environmentType = environmentTypeFromSdkroot(sdkRoot, environment.fileSystem)!;
dependencies = await buildNativeAssetsIOS(
environmentType: environmentType,
darwinArchs: iosArchs,
buildMode: buildMode,
projectUri: projectUri,
codesignIdentity: environment.defines[kCodesignIdentity],
fileSystem: fileSystem,
buildRunner: buildRunner,
yamlParentDirectory: environment.buildDir.uri,
dependencies = await _buildIOS(
environment,
projectUri,
fileSystem,
buildRunner,
);
case TargetPlatform.darwin:
final List<DarwinArch> darwinArchs =
_emptyToNull(environment.defines[kDarwinArchs])
?.split(' ')
.map(getDarwinArchForName)
.toList()
?? <DarwinArch>[DarwinArch.x86_64, DarwinArch.arm64];
final String? environmentBuildMode = environment.defines[kBuildMode];
if (environmentBuildMode == null) {
throw MissingDefineException(kBuildMode, name);
}
final BuildMode buildMode = BuildMode.fromCliName(environmentBuildMode);
(_, dependencies) = await buildNativeAssetsMacOS(
darwinArchs: darwinArchs,
buildMode: buildMode,
projectUri: projectUri,
codesignIdentity: environment.defines[kCodesignIdentity],
yamlParentDirectory: environment.buildDir.uri,
fileSystem: fileSystem,
buildRunner: buildRunner,
dependencies = await _buildMacOS(
environment,
projectUri,
fileSystem,
buildRunner,
);
case TargetPlatform.linux_arm64:
case TargetPlatform.linux_x64:
final String? environmentBuildMode = environment.defines[kBuildMode];
if (environmentBuildMode == null) {
throw MissingDefineException(kBuildMode, name);
}
final BuildMode buildMode = BuildMode.fromCliName(environmentBuildMode);
(_, dependencies) = await buildNativeAssetsLinux(
targetPlatform: targetPlatform,
buildMode: buildMode,
projectUri: projectUri,
yamlParentDirectory: environment.buildDir.uri,
fileSystem: fileSystem,
buildRunner: buildRunner,
dependencies = await _buildLinux(
environment,
targetPlatform,
projectUri,
fileSystem,
buildRunner,
);
case TargetPlatform.windows_x64:
final String? environmentBuildMode = environment.defines[kBuildMode];
if (environmentBuildMode == null) {
throw MissingDefineException(kBuildMode, name);
}
final BuildMode buildMode = BuildMode.fromCliName(environmentBuildMode);
(_, dependencies) = await buildNativeAssetsWindows(
targetPlatform: targetPlatform,
buildMode: buildMode,
projectUri: projectUri,
yamlParentDirectory: environment.buildDir.uri,
fileSystem: fileSystem,
buildRunner: buildRunner,
dependencies = await _buildWindows(
environment,
targetPlatform,
projectUri,
fileSystem,
buildRunner,
);
case TargetPlatform.tester:
if (const LocalPlatform().isMacOS) {
......@@ -197,6 +152,13 @@ class NativeAssets extends Target {
case TargetPlatform.android_x64:
case TargetPlatform.android_x86:
case TargetPlatform.android:
(_, dependencies) = await _buildAndroid(
environment,
targetPlatform,
projectUri,
fileSystem,
buildRunner,
);
case TargetPlatform.fuchsia_arm64:
case TargetPlatform.fuchsia_x64:
case TargetPlatform.web_javascript:
......@@ -228,6 +190,177 @@ class NativeAssets extends Target {
}
}
Future<List<Uri>> _buildWindows(
Environment environment,
TargetPlatform targetPlatform,
Uri projectUri,
FileSystem fileSystem,
NativeAssetsBuildRunner buildRunner,
) async {
final String? environmentBuildMode = environment.defines[kBuildMode];
if (environmentBuildMode == null) {
throw MissingDefineException(kBuildMode, name);
}
final BuildMode buildMode = BuildMode.fromCliName(environmentBuildMode);
final (_, List<Uri> dependencies) = await buildNativeAssetsWindows(
targetPlatform: targetPlatform,
buildMode: buildMode,
projectUri: projectUri,
yamlParentDirectory: environment.buildDir.uri,
fileSystem: fileSystem,
buildRunner: buildRunner,
);
return dependencies;
}
Future<List<Uri>> _buildLinux(
Environment environment,
TargetPlatform targetPlatform,
Uri projectUri,
FileSystem fileSystem,
NativeAssetsBuildRunner buildRunner,
) async {
final String? environmentBuildMode = environment.defines[kBuildMode];
if (environmentBuildMode == null) {
throw MissingDefineException(kBuildMode, name);
}
final BuildMode buildMode = BuildMode.fromCliName(environmentBuildMode);
final (_, List<Uri> dependencies) = await buildNativeAssetsLinux(
targetPlatform: targetPlatform,
buildMode: buildMode,
projectUri: projectUri,
yamlParentDirectory: environment.buildDir.uri,
fileSystem: fileSystem,
buildRunner: buildRunner,
);
return dependencies;
}
Future<List<Uri>> _buildMacOS(
Environment environment,
Uri projectUri,
FileSystem fileSystem,
NativeAssetsBuildRunner buildRunner,
) async {
final List<DarwinArch> darwinArchs =
_emptyToNull(environment.defines[kDarwinArchs])
?.split(' ')
.map(getDarwinArchForName)
.toList() ??
<DarwinArch>[DarwinArch.x86_64, DarwinArch.arm64];
final String? environmentBuildMode = environment.defines[kBuildMode];
if (environmentBuildMode == null) {
throw MissingDefineException(kBuildMode, name);
}
final BuildMode buildMode = BuildMode.fromCliName(environmentBuildMode);
final (_, List<Uri> dependencies) = await buildNativeAssetsMacOS(
darwinArchs: darwinArchs,
buildMode: buildMode,
projectUri: projectUri,
codesignIdentity: environment.defines[kCodesignIdentity],
yamlParentDirectory: environment.buildDir.uri,
fileSystem: fileSystem,
buildRunner: buildRunner,
);
return dependencies;
}
Future<List<Uri>> _buildIOS(
Environment environment,
Uri projectUri,
FileSystem fileSystem,
NativeAssetsBuildRunner buildRunner,
) {
final List<DarwinArch> iosArchs =
_emptyToNull(environment.defines[kIosArchs])
?.split(' ')
.map(getIOSArchForName)
.toList() ??
<DarwinArch>[DarwinArch.arm64];
final String? environmentBuildMode = environment.defines[kBuildMode];
if (environmentBuildMode == null) {
throw MissingDefineException(kBuildMode, name);
}
final BuildMode buildMode = BuildMode.fromCliName(environmentBuildMode);
final String? sdkRoot = environment.defines[kSdkRoot];
if (sdkRoot == null) {
throw MissingDefineException(kSdkRoot, name);
}
final EnvironmentType environmentType =
environmentTypeFromSdkroot(sdkRoot, environment.fileSystem)!;
return buildNativeAssetsIOS(
environmentType: environmentType,
darwinArchs: iosArchs,
buildMode: buildMode,
projectUri: projectUri,
codesignIdentity: environment.defines[kCodesignIdentity],
fileSystem: fileSystem,
buildRunner: buildRunner,
yamlParentDirectory: environment.buildDir.uri,
);
}
Future<(Uri? nativeAssetsYaml, List<Uri> dependencies)> _buildAndroid(
Environment environment,
TargetPlatform targetPlatform,
Uri projectUri,
FileSystem fileSystem,
NativeAssetsBuildRunner buildRunner) {
final bool isAndroidLibrary =
environment.defines[kIsAndroidLibrary] == 'true';
final String? androidArchsEnvironment = environment.defines[kAndroidArchs];
final List<AndroidArch> androidArchs = _androidArchs(
targetPlatform,
androidArchsEnvironment,
);
final int targetAndroidNdkApi =
int.parse(environment.defines[kMinSdkVersion] ?? minSdkVersion);
return buildNativeAssetsAndroid(
buildMode: BuildMode.debug,
projectUri: projectUri,
yamlParentDirectory: environment.buildDir.uri,
fileSystem: fileSystem,
buildRunner: buildRunner,
androidArchs: androidArchs,
targetAndroidNdkApi: targetAndroidNdkApi,
isAndroidLibrary: isAndroidLibrary,
);
}
List<AndroidArch> _androidArchs(
TargetPlatform targetPlatform,
String? androidArchsEnvironment,
) {
switch (targetPlatform) {
case TargetPlatform.android_arm:
return <AndroidArch>[AndroidArch.armeabi_v7a];
case TargetPlatform.android_arm64:
return <AndroidArch>[AndroidArch.arm64_v8a];
case TargetPlatform.android_x64:
return <AndroidArch>[AndroidArch.x86_64];
case TargetPlatform.android_x86:
return <AndroidArch>[AndroidArch.x86];
case TargetPlatform.android:
if (androidArchsEnvironment == null) {
throw MissingDefineException(kAndroidArchs, name);
}
return androidArchsEnvironment
.split(' ')
.map(getAndroidArchForName)
.toList();
case TargetPlatform.darwin:
case TargetPlatform.fuchsia_arm64:
case TargetPlatform.fuchsia_x64:
case TargetPlatform.ios:
case TargetPlatform.linux_arm64:
case TargetPlatform.linux_x64:
case TargetPlatform.tester:
case TargetPlatform.web_javascript:
case TargetPlatform.windows_x64:
throwToolExit('Unsupported Android target platform: $targetPlatform.');
}
}
@override
List<String> get depfiles => <String>[
'native_assets.d',
......
......@@ -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));
......
// 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:file/file.dart';
import 'package:file/memory.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/android/native_assets.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:native_assets_cli/native_assets_cli.dart' as native_assets_cli;
import 'package:native_assets_cli/native_assets_cli.dart' hide BuildMode, Target;
import 'package:package_config/package_config_types.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fakes.dart';
import '../fake_native_assets_build_runner.dart';
void main() {
late FakeProcessManager processManager;
late Environment environment;
late Artifacts artifacts;
late FileSystem fileSystem;
late BufferLogger logger;
late Uri projectUri;
setUp(() {
processManager = FakeProcessManager.empty();
logger = BufferLogger.test();
artifacts = Artifacts.test();
fileSystem = MemoryFileSystem.test();
environment = Environment.test(
fileSystem.currentDirectory,
inputs: <String, String>{},
artifacts: artifacts,
processManager: processManager,
fileSystem: fileSystem,
logger: logger,
);
environment.buildDir.createSync(recursive: true);
projectUri = environment.projectDir.uri;
});
testUsingContext('dry run with no package config', overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.empty(),
}, () async {
expect(
await dryRunNativeAssetsAndroid(
projectUri: projectUri,
fileSystem: fileSystem,
buildRunner: FakeNativeAssetsBuildRunner(
hasPackageConfigResult: false,
),
),
null,
);
expect(
(globals.logger as BufferLogger).traceText,
contains('No package config found. Skipping native assets compilation.'),
);
});
testUsingContext('build with no package config', overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.empty(),
}, () async {
await buildNativeAssetsAndroid(
androidArchs: <AndroidArch>[AndroidArch.arm64_v8a],
targetAndroidNdkApi: 21,
projectUri: projectUri,
buildMode: BuildMode.debug,
fileSystem: fileSystem,
yamlParentDirectory: environment.buildDir.uri,
buildRunner: FakeNativeAssetsBuildRunner(
hasPackageConfigResult: false,
),
);
expect(
(globals.logger as BufferLogger).traceText,
contains('No package config found. Skipping native assets compilation.'),
);
});
testUsingContext('dry run with assets but not enabled', overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.empty(),
}, () async {
final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json');
await packageConfig.parent.create();
await packageConfig.create();
expect(
() => dryRunNativeAssetsAndroid(
projectUri: projectUri,
fileSystem: fileSystem,
buildRunner: FakeNativeAssetsBuildRunner(
packagesWithNativeAssetsResult: <Package>[
Package('bar', projectUri),
],
),
),
throwsToolExit(
message: 'Package(s) bar require the native assets feature to be enabled. '
'Enable using `flutter config --enable-native-assets`.',
),
);
});
testUsingContext('dry run with assets', overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true),
ProcessManager: () => FakeProcessManager.empty(),
}, () async {
final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json');
await packageConfig.parent.create();
await packageConfig.create();
final Uri? nativeAssetsYaml = await dryRunNativeAssetsAndroid(
projectUri: projectUri,
fileSystem: fileSystem,
buildRunner: FakeNativeAssetsBuildRunner(
packagesWithNativeAssetsResult: <Package>[
Package('bar', projectUri),
],
dryRunResult: FakeNativeAssetsBuilderResult(
assets: <Asset>[
Asset(
id: 'package:bar/bar.dart',
linkMode: LinkMode.dynamic,
target: native_assets_cli.Target.macOSArm64,
path: AssetAbsolutePath(Uri.file('libbar.so')),
),
Asset(
id: 'package:bar/bar.dart',
linkMode: LinkMode.dynamic,
target: native_assets_cli.Target.macOSX64,
path: AssetAbsolutePath(Uri.file('libbar.so')),
),
],
),
),
);
expect(
(globals.logger as BufferLogger).traceText,
stringContainsInOrder(<String>[
'Dry running native assets for android.',
'Dry running native assets for android done.',
]),
);
expect(
nativeAssetsYaml,
projectUri.resolve('build/native_assets/android/native_assets.yaml'),
);
expect(
await fileSystem.file(nativeAssetsYaml).readAsString(),
contains('package:bar/bar.dart'),
);
});
testUsingContext('build with assets but not enabled', () async {
final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json');
await packageConfig.parent.create();
await packageConfig.create();
expect(
() => buildNativeAssetsAndroid(
androidArchs: <AndroidArch>[AndroidArch.arm64_v8a],
targetAndroidNdkApi: 21,
projectUri: projectUri,
buildMode: BuildMode.debug,
fileSystem: fileSystem,
yamlParentDirectory: environment.buildDir.uri,
buildRunner: FakeNativeAssetsBuildRunner(
packagesWithNativeAssetsResult: <Package>[
Package('bar', projectUri),
],
),
),
throwsToolExit(
message: 'Package(s) bar require the native assets feature to be enabled. '
'Enable using `flutter config --enable-native-assets`.',
),
);
});
testUsingContext('build no assets', overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true),
ProcessManager: () => FakeProcessManager.empty(),
}, () async {
final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json');
await packageConfig.parent.create();
await packageConfig.create();
await buildNativeAssetsAndroid(
androidArchs: <AndroidArch>[AndroidArch.arm64_v8a],
targetAndroidNdkApi: 21,
projectUri: projectUri,
buildMode: BuildMode.debug,
fileSystem: fileSystem,
yamlParentDirectory: environment.buildDir.uri,
buildRunner: FakeNativeAssetsBuildRunner(
packagesWithNativeAssetsResult: <Package>[
Package('bar', projectUri),
],
),
);
expect(
environment.buildDir.childFile('native_assets.yaml'),
exists,
);
});
testUsingContext('build with assets',
skip: const LocalPlatform().isWindows, // [intended] Backslashes in commands, but we will never run these commands on Windows.
overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true),
ProcessManager: () => FakeProcessManager.empty(),
}, () async {
final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json');
await packageConfig.parent.create();
await packageConfig.create();
final File dylibAfterCompiling = fileSystem.file('libbar.so');
// The mock doesn't create the file, so create it here.
await dylibAfterCompiling.create();
await buildNativeAssetsAndroid(
androidArchs: <AndroidArch>[AndroidArch.arm64_v8a],
targetAndroidNdkApi: 21,
projectUri: projectUri,
buildMode: BuildMode.debug,
fileSystem: fileSystem,
yamlParentDirectory: environment.buildDir.uri,
buildRunner: FakeNativeAssetsBuildRunner(
packagesWithNativeAssetsResult: <Package>[
Package('bar', projectUri),
],
buildResult: FakeNativeAssetsBuilderResult(
assets: <Asset>[
Asset(
id: 'package:bar/bar.dart',
linkMode: LinkMode.dynamic,
target: native_assets_cli.Target.androidArm64,
path: AssetAbsolutePath(Uri.file('libbar.so')),
),
],
),
),
);
expect(
(globals.logger as BufferLogger).traceText,
stringContainsInOrder(<String>[
'Building native assets for [android_arm64] debug.',
'Building native assets for [android_arm64] done.',
]),
);
expect(
environment.buildDir.childFile('native_assets.yaml'),
exists,
);
});
// Ensure no exceptions for a non installed NDK are thrown if no native
// assets have to be build.
testUsingContext(
'does not throw if NDK not present but no native assets present',
overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true),
ProcessManager: () => FakeProcessManager.empty(),
}, () async {
final File packageConfig =
environment.projectDir.childFile('.dart_tool/package_config.json');
await packageConfig.create(recursive: true);
await buildNativeAssetsAndroid(
androidArchs: <AndroidArch>[AndroidArch.x86_64],
targetAndroidNdkApi: 21,
projectUri: projectUri,
buildMode: BuildMode.debug,
fileSystem: fileSystem,
buildRunner: _BuildRunnerWithoutNdk(),
);
expect(
(globals.logger as BufferLogger).traceText,
isNot(contains('Building native assets for ')),
);
});
testUsingContext('throw if NDK not present and there are native assets',
overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true),
}, () async {
final File packageConfig =
environment.projectDir.childFile('.dart_tool/package_config.json');
await packageConfig.parent.create();
await packageConfig.create();
expect(
() => buildNativeAssetsAndroid(
androidArchs: <AndroidArch>[AndroidArch.arm64_v8a],
targetAndroidNdkApi: 21,
projectUri: projectUri,
buildMode: BuildMode.debug,
fileSystem: fileSystem,
yamlParentDirectory: environment.buildDir.uri,
buildRunner: _BuildRunnerWithoutNdk(
packagesWithNativeAssetsResult: <Package>[
Package('bar', projectUri),
],
),
),
throwsToolExit(
message: 'Android NDK Clang could not be found.',
),
);
});
testUsingContext('Native assets dry run error', overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true),
ProcessManager: () => FakeProcessManager.empty(),
}, () async {
final File packageConfig =
environment.projectDir.childFile('.dart_tool/package_config.json');
await packageConfig.parent.create();
await packageConfig.create();
expect(
() => dryRunNativeAssetsAndroid(
projectUri: projectUri,
fileSystem: fileSystem,
buildRunner: FakeNativeAssetsBuildRunner(
packagesWithNativeAssetsResult: <Package>[
Package('bar', projectUri),
],
dryRunResult: const FakeNativeAssetsBuilderResult(
success: false,
),
),
),
throwsToolExit(
message:
'Building native assets failed. See the logs for more details.',
),
);
});
testUsingContext('Native assets build error', overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true),
ProcessManager: () => FakeProcessManager.empty(),
}, () async {
final File packageConfig =
environment.projectDir.childFile('.dart_tool/package_config.json');
await packageConfig.parent.create();
await packageConfig.create();
expect(
() => buildNativeAssetsAndroid(
androidArchs: <AndroidArch>[AndroidArch.arm64_v8a],
targetAndroidNdkApi: 21,
projectUri: projectUri,
buildMode: BuildMode.debug,
fileSystem: fileSystem,
yamlParentDirectory: environment.buildDir.uri,
buildRunner: FakeNativeAssetsBuildRunner(
packagesWithNativeAssetsResult: <Package>[
Package('bar', projectUri),
],
buildResult: const FakeNativeAssetsBuilderResult(
success: false,
),
),
),
throwsToolExit(
message:
'Building native assets failed. See the logs for more details.',
),
);
});
}
class _BuildRunnerWithoutNdk extends FakeNativeAssetsBuildRunner {
_BuildRunnerWithoutNdk({
super.packagesWithNativeAssetsResult = const <Package>[],
});
@override
Future<CCompilerConfig> get ndkCCompilerConfig async =>
throwToolExit('Android NDK Clang could not be found.');
}
......@@ -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
......
......@@ -21,8 +21,7 @@ import 'package:flutter_tools/src/resident_devtools_handler.dart';
import 'package:flutter_tools/src/resident_runner.dart';
import 'package:flutter_tools/src/run_hot.dart';
import 'package:flutter_tools/src/vmservice.dart';
import 'package:native_assets_cli/native_assets_cli.dart'
hide BuildMode, Target;
import 'package:native_assets_cli/native_assets_cli.dart' hide BuildMode, Target;
import 'package:native_assets_cli/native_assets_cli.dart' as native_assets_cli;
import 'package:package_config/package_config.dart';
import 'package:test/fake.dart';
......@@ -37,116 +36,84 @@ import 'fake_native_assets_build_runner.dart';
void main() {
group('validateReloadReport', () {
testUsingContext('invalid', () async {
expect(
HotRunner.validateReloadReport(
vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': false,
'details': <String, dynamic>{},
})),
false);
expect(
HotRunner.validateReloadReport(
vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': false,
'details': <String, dynamic>{
'notices': <Map<String, dynamic>>[],
},
})),
false);
expect(
HotRunner.validateReloadReport(
vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': false,
'details': <String, dynamic>{
'notices': <String, dynamic>{
'message': 'error',
},
},
})),
false);
expect(
HotRunner.validateReloadReport(
vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': false,
'details': <String, dynamic>{
'notices': <Map<String, dynamic>>[],
},
})),
false);
expect(
HotRunner.validateReloadReport(
vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': false,
'details': <String, dynamic>{
'notices': <Map<String, dynamic>>[
<String, dynamic>{'message': false},
],
},
})),
false);
expect(
HotRunner.validateReloadReport(
vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': false,
'details': <String, dynamic>{
'notices': <Map<String, dynamic>>[
<String, dynamic>{
'message': <String>['error']
},
],
},
})),
false);
expect(
HotRunner.validateReloadReport(
vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': false,
'details': <String, dynamic>{
'notices': <Map<String, dynamic>>[
<String, dynamic>{'message': 'error'},
<String, dynamic>{
'message': <String>['error']
},
],
},
})),
false);
expect(
HotRunner.validateReloadReport(
vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': false,
'details': <String, dynamic>{
'notices': <Map<String, dynamic>>[
<String, dynamic>{'message': 'error'},
],
},
})),
false);
expect(
HotRunner.validateReloadReport(
vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': true,
})),
true);
expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': false,
'details': <String, dynamic>{},
})), false);
expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': false,
'details': <String, dynamic>{
'notices': <Map<String, dynamic>>[
],
},
})), false);
expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': false,
'details': <String, dynamic>{
'notices': <String, dynamic>{
'message': 'error',
},
},
})), false);
expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': false,
'details': <String, dynamic>{
'notices': <Map<String, dynamic>>[],
},
})), false);
expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': false,
'details': <String, dynamic>{
'notices': <Map<String, dynamic>>[
<String, dynamic>{'message': false},
],
},
})), false);
expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': false,
'details': <String, dynamic>{
'notices': <Map<String, dynamic>>[
<String, dynamic>{'message': <String>['error']},
],
},
})), false);
expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': false,
'details': <String, dynamic>{
'notices': <Map<String, dynamic>>[
<String, dynamic>{'message': 'error'},
<String, dynamic>{'message': <String>['error']},
],
},
})), false);
expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': false,
'details': <String, dynamic>{
'notices': <Map<String, dynamic>>[
<String, dynamic>{'message': 'error'},
],
},
})), false);
expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse(<String, dynamic>{
'type': 'ReloadReport',
'success': true,
})), true);
});
testWithoutContext(
'ReasonForCancelling toString has a hint for specific errors', () {
testWithoutContext('ReasonForCancelling toString has a hint for specific errors', () {
final ReasonForCancelling reasonForCancelling = ReasonForCancelling(
message: 'Const class cannot remove fields',
);
expect(reasonForCancelling.toString(),
contains('Try performing a hot restart instead.'));
expect(reasonForCancelling.toString(), contains('Try performing a hot restart instead.'));
});
});
......@@ -180,10 +147,7 @@ void main() {
..writeAsStringSync('\n');
final FakeDevice device = FakeDevice();
final List<FlutterDevice> devices = <FlutterDevice>[
FlutterDevice(device,
generator: residentCompiler,
buildInfo: BuildInfo.debug,
developmentShaderCompiler: const FakeShaderCompiler())
FlutterDevice(device, generator: residentCompiler, buildInfo: BuildInfo.debug, developmentShaderCompiler: const FakeShaderCompiler())
..devFS = FakeDevFs(),
];
final OperationResult result = await HotRunner(
......@@ -192,6 +156,7 @@ void main() {
target: 'main.dart',
devtoolsHandler: createNoOpHandler,
analytics: fakeAnalytics,
).restart(fullRestart: true);
expect(result.isOk, false);
expect(result.message, 'setupHotRestart failed');
......@@ -223,13 +188,12 @@ void main() {
Map<FlutterDevice?, List<FlutterView>> viewCache,
void Function(String message)? onSlow,
String reloadMessage,
) async =>
ReassembleResult(
<FlutterView?, FlutterVmService?>{null: null},
false,
true,
),
analytics: fakeAnalytics,
) async => ReassembleResult(
<FlutterView?, FlutterVmService?>{null: null},
false,
true,
),
analytics: fakeAnalytics,
).restart();
expect(result.isOk, false);
expect(result.message, 'setupHotReload failed');
......@@ -256,10 +220,7 @@ void main() {
..writeAsStringSync('\n');
final FakeDevice device = FakeDevice();
final List<FlutterDevice> devices = <FlutterDevice>[
FlutterDevice(device,
generator: residentCompiler,
buildInfo: BuildInfo.debug,
developmentShaderCompiler: const FakeShaderCompiler()),
FlutterDevice(device, generator: residentCompiler, buildInfo: BuildInfo.debug, developmentShaderCompiler: const FakeShaderCompiler()),
];
await HotRunner(
devices,
......@@ -282,10 +243,7 @@ void main() {
..writeAsStringSync('\n');
final FakeDevice device = FakeDevice();
final List<FlutterDevice> devices = <FlutterDevice>[
FlutterDevice(device,
generator: residentCompiler,
buildInfo: BuildInfo.debug,
developmentShaderCompiler: const FakeShaderCompiler()),
FlutterDevice(device, generator: residentCompiler, buildInfo: BuildInfo.debug, developmentShaderCompiler: const FakeShaderCompiler()),
];
await HotRunner(
devices,
......@@ -310,36 +268,30 @@ void main() {
successfulHotRestartSetup: true,
);
});
testUsingContext(
'correctly tracks time spent for analytics for hot restart',
() async {
testUsingContext('correctly tracks time spent for analytics for hot restart', () async {
final FakeDevice device = FakeDevice();
final FakeFlutterDevice fakeFlutterDevice = FakeFlutterDevice(device);
final List<FlutterDevice> devices = <FlutterDevice>[
fakeFlutterDevice,
];
fakeFlutterDevice.updateDevFSReportCallback =
() async => UpdateFSReport(
success: true,
invalidatedSourcesCount: 2,
syncedBytes: 4,
scannedSourcesCount: 8,
compileDuration: const Duration(seconds: 16),
transferDuration: const Duration(seconds: 32),
);
fakeFlutterDevice.updateDevFSReportCallback = () async => UpdateFSReport(
success: true,
invalidatedSourcesCount: 2,
syncedBytes: 4,
scannedSourcesCount: 8,
compileDuration: const Duration(seconds: 16),
transferDuration: const Duration(seconds: 32),
);
final FakeStopwatchFactory fakeStopwatchFactory = FakeStopwatchFactory(
stopwatches: <String, Stopwatch>{
'fullRestartHelper': FakeStopwatch()
..elapsed = const Duration(seconds: 64),
'updateDevFS': FakeStopwatch()
..elapsed = const Duration(seconds: 128),
'fullRestartHelper': FakeStopwatch()..elapsed = const Duration(seconds: 64),
'updateDevFS': FakeStopwatch()..elapsed = const Duration(seconds: 128),
},
);
(fakeFlutterDevice.devFS! as FakeDevFs).baseUri =
Uri.parse('file:///base_uri');
(fakeFlutterDevice.devFS! as FakeDevFs).baseUri = Uri.parse('file:///base_uri');
final OperationResult result = await HotRunner(
devices,
......@@ -352,45 +304,37 @@ void main() {
expect(result.isOk, true);
expect(testUsage.events, <TestUsageEvent>[
const TestUsageEvent('hot', 'restart',
parameters: CustomDimensions(
hotEventTargetPlatform: 'flutter-tester',
hotEventSdkName: 'Tester',
hotEventEmulator: false,
hotEventFullRestart: true,
hotEventOverallTimeInMs: 64000,
hotEventSyncedBytes: 4,
hotEventInvalidatedSourcesCount: 2,
hotEventTransferTimeInMs: 32000,
hotEventCompileTimeInMs: 16000,
hotEventFindInvalidatedTimeInMs: 128000,
hotEventScannedSourcesCount: 8,
)),
const TestUsageEvent('hot', 'restart', parameters: CustomDimensions(
hotEventTargetPlatform: 'flutter-tester',
hotEventSdkName: 'Tester',
hotEventEmulator: false,
hotEventFullRestart: true,
hotEventOverallTimeInMs: 64000,
hotEventSyncedBytes: 4,
hotEventInvalidatedSourcesCount: 2,
hotEventTransferTimeInMs: 32000,
hotEventCompileTimeInMs: 16000,
hotEventFindInvalidatedTimeInMs: 128000,
hotEventScannedSourcesCount: 8,
)),
]);
expect(
fakeAnalytics.sentEvents,
contains(Event.hotRunnerInfo(
label: 'restart',
targetPlatform: 'flutter-tester',
sdkName: 'Tester',
emulator: false,
fullRestart: true,
syncedBytes: 4,
invalidatedSourcesCount: 2,
transferTimeInMs: 32000,
overallTimeInMs: 64000,
compileTimeInMs: 16000,
findInvalidatedTimeInMs: 128000,
scannedSourcesCount: 8)));
expect(
analyticsTimingEventExists(
sentEvents: fakeAnalytics.sentEvents,
workflow: 'hot',
variableName: 'restart',
),
true,
);
expect(fakeAnalytics.sentEvents, contains(
Event.hotRunnerInfo(
label: 'restart',
targetPlatform: 'flutter-tester',
sdkName: 'Tester',
emulator: false,
fullRestart: true,
syncedBytes: 4,
invalidatedSourcesCount: 2,
transferTimeInMs: 32000,
overallTimeInMs: 64000,
compileTimeInMs: 16000,
findInvalidatedTimeInMs: 128000,
scannedSourcesCount: 8
)
));
expect(testingConfig.updateDevFSCompleteCalled, true);
}, overrides: <Type, Generator>{
HotRunnerConfig: () => testingConfig,
......@@ -409,39 +353,32 @@ void main() {
successfulHotReloadSetup: true,
);
});
testUsingContext(
'correctly tracks time spent for analytics for hot reload', () async {
testUsingContext('correctly tracks time spent for analytics for hot reload', () async {
final FakeDevice device = FakeDevice();
final FakeFlutterDevice fakeFlutterDevice = FakeFlutterDevice(device);
final List<FlutterDevice> devices = <FlutterDevice>[
fakeFlutterDevice,
];
fakeFlutterDevice.updateDevFSReportCallback =
() async => UpdateFSReport(
success: true,
invalidatedSourcesCount: 6,
syncedBytes: 8,
scannedSourcesCount: 16,
compileDuration: const Duration(seconds: 16),
transferDuration: const Duration(seconds: 32),
);
fakeFlutterDevice.updateDevFSReportCallback = () async => UpdateFSReport(
success: true,
invalidatedSourcesCount: 6,
syncedBytes: 8,
scannedSourcesCount: 16,
compileDuration: const Duration(seconds: 16),
transferDuration: const Duration(seconds: 32),
);
final FakeStopwatchFactory fakeStopwatchFactory = FakeStopwatchFactory(
stopwatches: <String, Stopwatch>{
'updateDevFS': FakeStopwatch()
..elapsed = const Duration(seconds: 64),
'reloadSources:reload': FakeStopwatch()
..elapsed = const Duration(seconds: 128),
'reloadSources:reassemble': FakeStopwatch()
..elapsed = const Duration(seconds: 256),
'reloadSources:vm': FakeStopwatch()
..elapsed = const Duration(seconds: 512),
'updateDevFS': FakeStopwatch()..elapsed = const Duration(seconds: 64),
'reloadSources:reload': FakeStopwatch()..elapsed = const Duration(seconds: 128),
'reloadSources:reassemble': FakeStopwatch()..elapsed = const Duration(seconds: 256),
'reloadSources:vm': FakeStopwatch()..elapsed = const Duration(seconds: 512),
},
);
(fakeFlutterDevice.devFS! as FakeDevFs).baseUri =
Uri.parse('file:///base_uri');
(fakeFlutterDevice.devFS! as FakeDevFs).baseUri = Uri.parse('file:///base_uri');
final OperationResult result = await HotRunner(
devices,
......@@ -473,68 +410,57 @@ void main() {
Map<FlutterDevice?, List<FlutterView>> viewCache,
void Function(String message)? onSlow,
String reloadMessage,
) async =>
ReassembleResult(
<FlutterView?, FlutterVmService?>{null: null},
false,
true,
),
) async => ReassembleResult(
<FlutterView?, FlutterVmService?>{null: null},
false,
true,
),
).restart();
expect(result.isOk, true);
expect(testUsage.events, <TestUsageEvent>[
const TestUsageEvent('hot', 'reload',
parameters: CustomDimensions(
hotEventFinalLibraryCount: 2,
hotEventSyncedLibraryCount: 3,
hotEventSyncedClassesCount: 4,
hotEventSyncedProceduresCount: 5,
hotEventSyncedBytes: 8,
hotEventInvalidatedSourcesCount: 6,
hotEventTransferTimeInMs: 32000,
hotEventOverallTimeInMs: 128000,
hotEventTargetPlatform: 'flutter-tester',
hotEventSdkName: 'Tester',
hotEventEmulator: false,
hotEventFullRestart: false,
hotEventCompileTimeInMs: 16000,
hotEventFindInvalidatedTimeInMs: 64000,
hotEventScannedSourcesCount: 16,
hotEventReassembleTimeInMs: 256000,
hotEventReloadVMTimeInMs: 512000,
)),
const TestUsageEvent('hot', 'reload', parameters: CustomDimensions(
hotEventFinalLibraryCount: 2,
hotEventSyncedLibraryCount: 3,
hotEventSyncedClassesCount: 4,
hotEventSyncedProceduresCount: 5,
hotEventSyncedBytes: 8,
hotEventInvalidatedSourcesCount: 6,
hotEventTransferTimeInMs: 32000,
hotEventOverallTimeInMs: 128000,
hotEventTargetPlatform: 'flutter-tester',
hotEventSdkName: 'Tester',
hotEventEmulator: false,
hotEventFullRestart: false,
hotEventCompileTimeInMs: 16000,
hotEventFindInvalidatedTimeInMs: 64000,
hotEventScannedSourcesCount: 16,
hotEventReassembleTimeInMs: 256000,
hotEventReloadVMTimeInMs: 512000,
)),
]);
expect(
fakeAnalytics.sentEvents,
contains(
Event.hotRunnerInfo(
label: 'reload',
targetPlatform: 'flutter-tester',
sdkName: 'Tester',
emulator: false,
fullRestart: false,
finalLibraryCount: 2,
syncedLibraryCount: 3,
syncedClassesCount: 4,
syncedProceduresCount: 5,
syncedBytes: 8,
invalidatedSourcesCount: 6,
transferTimeInMs: 32000,
overallTimeInMs: 128000,
compileTimeInMs: 16000,
findInvalidatedTimeInMs: 64000,
scannedSourcesCount: 16,
reassembleTimeInMs: 256000,
reloadVMTimeInMs: 512000),
));
expect(
analyticsTimingEventExists(
sentEvents: fakeAnalytics.sentEvents,
workflow: 'hot',
variableName: 'reload',
expect(fakeAnalytics.sentEvents, contains(
Event.hotRunnerInfo(
label: 'reload',
targetPlatform: 'flutter-tester',
sdkName: 'Tester',
emulator: false,
fullRestart: false,
finalLibraryCount: 2,
syncedLibraryCount: 3,
syncedClassesCount: 4,
syncedProceduresCount: 5,
syncedBytes: 8,
invalidatedSourcesCount: 6,
transferTimeInMs: 32000,
overallTimeInMs: 128000,
compileTimeInMs: 16000,
findInvalidatedTimeInMs: 64000,
scannedSourcesCount: 16,
reassembleTimeInMs: 256000,
reloadVMTimeInMs: 512000
),
true,
);
));
expect(testingConfig.updateDevFSCompleteCalled, true);
}, overrides: <Type, Generator>{
HotRunnerConfig: () => testingConfig,
......@@ -559,8 +485,7 @@ void main() {
final List<FlutterDevice> devices = <FlutterDevice>[
fakeFlutterDevice,
];
fakeFlutterDevice.updateDevFSReportCallback =
() async => throw Exception('updateDevFS failed');
fakeFlutterDevice.updateDevFSReportCallback = () async => throw Exception('updateDevFS failed');
final HotRunner runner = HotRunner(
devices,
......@@ -570,10 +495,7 @@ void main() {
analytics: fakeAnalytics,
);
await expectLater(
runner.restart(fullRestart: true),
throwsA(isA<Exception>().having((Exception e) => e.toString(),
'message', 'Exception: updateDevFS failed')));
await expectLater(runner.restart(fullRestart: true), throwsA(isA<Exception>().having((Exception e) => e.toString(), 'message', 'Exception: updateDevFS failed')));
expect(testingConfig.updateDevFSCompleteCalled, true);
}, overrides: <Type, Generator>{
HotRunnerConfig: () => testingConfig,
......@@ -598,8 +520,7 @@ void main() {
final List<FlutterDevice> devices = <FlutterDevice>[
fakeFlutterDevice,
];
fakeFlutterDevice.updateDevFSReportCallback =
() async => throw Exception('updateDevFS failed');
fakeFlutterDevice.updateDevFSReportCallback = () async => throw Exception('updateDevFS failed');
final HotRunner runner = HotRunner(
devices,
......@@ -609,10 +530,7 @@ void main() {
analytics: fakeAnalytics,
);
await expectLater(
runner.restart(),
throwsA(isA<Exception>().having((Exception e) => e.toString(),
'message', 'Exception: updateDevFS failed')));
await expectLater(runner.restart(), throwsA(isA<Exception>().having((Exception e) => e.toString(), 'message', 'Exception: updateDevFS failed')));
expect(testingConfig.updateDevFSCompleteCalled, true);
}, overrides: <Type, Generator>{
HotRunnerConfig: () => testingConfig,
......@@ -637,9 +555,8 @@ void main() {
);
});
testUsingContext(
'Exits with code 2 when HttpException is thrown '
'during VM service connection', () async {
testUsingContext('Exits with code 2 when HttpException is thrown '
'during VM service connection', () async {
fileSystem.file('.packages')
..createSync(recursive: true)
..writeAsStringSync('\n');
......@@ -650,14 +567,12 @@ void main() {
TestFlutterDevice(
device: device,
generator: residentCompiler,
exception: const HttpException(
'Connection closed before full header was received, '
exception: const HttpException('Connection closed before full header was received, '
'uri = http://127.0.0.1:63394/5ZmLv8A59xY=/ws'),
),
];
final int exitCode = await HotRunner(
devices,
final int exitCode = await HotRunner(devices,
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
target: 'main.dart',
analytics: fakeAnalytics,
......@@ -695,8 +610,7 @@ void main() {
flutterDevice2,
];
await HotRunner(
devices,
await HotRunner(devices,
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
target: 'main.dart',
analytics: fakeAnalytics,
......@@ -733,19 +647,17 @@ void main() {
];
fakeFlutterDevice.updateDevFSReportCallback = () async => UpdateFSReport(
success: true,
invalidatedSourcesCount: 6,
syncedBytes: 8,
scannedSourcesCount: 16,
compileDuration: const Duration(seconds: 16),
transferDuration: const Duration(seconds: 32),
);
success: true,
invalidatedSourcesCount: 6,
syncedBytes: 8,
scannedSourcesCount: 16,
compileDuration: const Duration(seconds: 16),
transferDuration: const Duration(seconds: 32),
);
(fakeFlutterDevice.devFS! as FakeDevFs).baseUri =
Uri.parse('file:///base_uri');
(fakeFlutterDevice.devFS! as FakeDevFs).baseUri = Uri.parse('file:///base_uri');
final FakeNativeAssetsBuildRunner buildRunner =
FakeNativeAssetsBuildRunner(
final FakeNativeAssetsBuildRunner buildRunner = FakeNativeAssetsBuildRunner(
packagesWithNativeAssetsResult: <Package>[
Package('bar', fileSystem.currentDirectory.uri),
],
......@@ -783,32 +695,28 @@ void main() {
FileSystem: () => fileSystem,
Platform: () => FakePlatform(),
ProcessManager: () => FakeProcessManager.empty(),
FeatureFlags: () =>
TestFeatureFlags(isNativeAssetsEnabled: true, isMacOSEnabled: true),
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true, isMacOSEnabled: true),
});
testUsingContext('native assets run unsupported', () async {
final FakeDevice device =
FakeDevice(targetPlatform: TargetPlatform.android_arm64);
final FakeDevice device = FakeDevice(targetPlatform: TargetPlatform.fuchsia_arm64);
final FakeFlutterDevice fakeFlutterDevice = FakeFlutterDevice(device);
final List<FlutterDevice> devices = <FlutterDevice>[
fakeFlutterDevice,
];
fakeFlutterDevice.updateDevFSReportCallback = () async => UpdateFSReport(
success: true,
invalidatedSourcesCount: 6,
syncedBytes: 8,
scannedSourcesCount: 16,
compileDuration: const Duration(seconds: 16),
transferDuration: const Duration(seconds: 32),
);
success: true,
invalidatedSourcesCount: 6,
syncedBytes: 8,
scannedSourcesCount: 16,
compileDuration: const Duration(seconds: 16),
transferDuration: const Duration(seconds: 32),
);
(fakeFlutterDevice.devFS! as FakeDevFs).baseUri =
Uri.parse('file:///base_uri');
(fakeFlutterDevice.devFS! as FakeDevFs).baseUri = Uri.parse('file:///base_uri');
final FakeNativeAssetsBuildRunner buildRunner =
FakeNativeAssetsBuildRunner(
final FakeNativeAssetsBuildRunner buildRunner = FakeNativeAssetsBuildRunner(
packagesWithNativeAssetsResult: <Package>[
Package('bar', fileSystem.currentDirectory.uri),
],
......@@ -833,27 +741,28 @@ void main() {
analytics: fakeAnalytics,
);
expect(
() => hotRunner.run(),
throwsToolExit(
message: 'Package(s) bar require the native assets feature. '
'This feature has not yet been implemented for `TargetPlatform.android_arm64`. '
'For more info see https://github.com/flutter/flutter/issues/129757.',
));
() => hotRunner.run(),
throwsToolExit( message:
'Package(s) bar require the native assets feature. '
'This feature has not yet been implemented for `TargetPlatform.fuchsia_arm64`. '
'For more info see https://github.com/flutter/flutter/issues/129757.',
)
);
}, overrides: <Type, Generator>{
HotRunnerConfig: () => testingConfig,
Artifacts: () => Artifacts.test(),
FileSystem: () => fileSystem,
Platform: () => FakePlatform(),
ProcessManager: () => FakeProcessManager.empty(),
FeatureFlags: () =>
TestFeatureFlags(isNativeAssetsEnabled: true, isMacOSEnabled: true),
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true, isMacOSEnabled: true),
});
});
}
class FakeDevFs extends Fake implements DevFS {
@override
Future<void> destroy() async {}
Future<void> destroy() async { }
@override
List<Uri> sources = <Uri>[];
......@@ -868,10 +777,10 @@ class FakeDevFs extends Fake implements DevFS {
Set<String> assetPathsToEvict = <String>{};
@override
Set<String> shaderPathsToEvict = <String>{};
Set<String> shaderPathsToEvict= <String>{};
@override
Set<String> scenePathsToEvict = <String>{};
Set<String> scenePathsToEvict= <String>{};
@override
Uri? baseUri;
......@@ -964,8 +873,7 @@ class FakeFlutterDevice extends Fake implements FlutterDevice {
required String dillOutputPath,
required List<Uri> invalidatedFiles,
required PackageConfig packageConfig,
}) =>
updateDevFSReportCallback();
}) => updateDevFSReportCallback();
@override
TargetPlatform? get targetPlatform => device._targetPlatform;
......@@ -976,10 +884,7 @@ class TestFlutterDevice extends FlutterDevice {
required Device device,
required this.exception,
required ResidentCompiler generator,
}) : super(device,
buildInfo: BuildInfo.debug,
generator: generator,
developmentShaderCompiler: const FakeShaderCompiler());
}) : super(device, buildInfo: BuildInfo.debug, generator: generator, developmentShaderCompiler: const FakeShaderCompiler());
/// The exception to throw when the connect method is called.
final Exception exception;
......@@ -1005,8 +910,7 @@ class TestFlutterDevice extends FlutterDevice {
}
class TestHotRunnerConfig extends HotRunnerConfig {
TestHotRunnerConfig(
{this.successfulHotRestartSetup, this.successfulHotReloadSetup});
TestHotRunnerConfig({this.successfulHotRestartSetup, this.successfulHotReloadSetup});
bool? successfulHotRestartSetup;
bool? successfulHotReloadSetup;
bool shutdownHookCalled = false;
......@@ -1014,15 +918,13 @@ class TestHotRunnerConfig extends HotRunnerConfig {
@override
Future<bool?> setupHotRestart() async {
assert(successfulHotRestartSetup != null,
'setupHotRestart is not expected to be called in this test.');
assert(successfulHotRestartSetup != null, 'setupHotRestart is not expected to be called in this test.');
return successfulHotRestartSetup;
}
@override
Future<bool?> setupHotReload() async {
assert(successfulHotReloadSetup != null,
'setupHotReload is not expected to be called in this test.');
assert(successfulHotReloadSetup != null, 'setupHotReload is not expected to be called in this test.');
return successfulHotReloadSetup;
}
......@@ -1047,9 +949,7 @@ class FakeFlutterVmService extends Fake implements FlutterVmService {
vm_service.VmService get service => FakeVmService();
@override
Future<List<FlutterView>> getFlutterViews(
{bool returnEarly = false,
Duration delay = const Duration(milliseconds: 50)}) async {
Future<List<FlutterView>> getFlutterViews({bool returnEarly = false, Duration delay = const Duration(milliseconds: 50)}) async {
return <FlutterView>[];
}
}
......@@ -1071,7 +971,7 @@ class FakeShaderCompiler implements DevelopmentShaderCompiler {
void configureCompiler(
TargetPlatform? platform, {
required ImpellerStatus impellerStatus,
}) {}
}) { }
@override
Future<DevFSContent> recompileShader(DevFSContent inputShader) {
......
......@@ -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