Native assets support for MacOS and iOS (#130494)

Support for FFI calls with `@Native external` functions through Native assets on MacOS and iOS. 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 MacOS and iOS.

Dylibs are bundled by (1) making them fat binaries if multiple architectures are targeted, (2) code signing these, and (3) copying them to the frameworks folder. These steps are done manual rather than via CocoaPods. CocoaPods would have done the same steps, but (a) needs the dylibs to be there before the `xcodebuild` invocation (we could trick it, by having a minimal dylib in the place and replace it during the build process, that works), and (b) can't deal with having no dylibs to be bundled (we'd have to bundle a dummy dylib or include some dummy C code in the build file).

The dylibs are build as a new target inside flutter assemble, as that is the moment we know what build-mode and architecture to target.

The mapping from asset id to dylib-path is passed in to every kernel compilation path. The interesting case is hot-restart where the initial kernel file is compiled by the "inner" flutter assemble, while after hot restart the "outer" flutter run compiled kernel file is pushed to the device. Both kernel files need to contain the mapping. The "inner" flutter assemble gets its mapping from the NativeAssets target which builds the native assets. The "outer" flutter run get its mapping from a dry-run invocation. Since this hot restart can be used for multiple target devices (`flutter run -d all`) it contains the mapping for all known targets.

### Example vs template

The PR includes a new template that uses the new native assets in a package and has an app importing that. Separate discussion in: https://github.com/flutter/flutter/issues/131209.

### Tests

This PR adds new tests to cover the various use cases.

* dev/devicelab/bin/tasks/native_assets_ios.dart
  * Runs an example app with native assets in all build modes, doing hot reload and hot restart in debug mode.
* dev/devicelab/bin/tasks/native_assets_ios_simulator.dart
  * Runs an example app with native assets, doing hot reload and hot restart.
* packages/flutter_tools/test/integration.shard/native_assets_test.dart
  * Runs (incl hot reload/hot restart), builds, builds frameworks for iOS, MacOS and flutter-tester.
* packages/flutter_tools/test/general.shard/build_system/targets/native_assets_test.dart
  * Unit tests the new Target in the backend.
* packages/flutter_tools/test/general.shard/ios/native_assets_test.dart
* packages/flutter_tools/test/general.shard/macos/native_assets_test.dart
  * Unit tests the native assets being packaged on a iOS/MacOS build.

It also extends various existing tests:

* dev/devicelab/bin/tasks/module_test_ios.dart
   * Exercises the add2app scenario.
* packages/flutter_tools/test/general.shard/features_test.dart
   * Unit test the new feature flag.
This commit is contained in:
Daco Harkes 2023-09-10 10:07:13 +02:00 committed by GitHub
parent 690800bca3
commit aa36db1d29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 4132 additions and 412 deletions

View File

@ -4069,6 +4069,26 @@ targets:
["devicelab", "ios", "mac"]
task_name: microbenchmarks_ios
- name: Mac_ios native_assets_ios_simulator
recipe: devicelab/devicelab_drone
presubmit: false
bringup: true # TODO(dacoharkes): Set to false in follow up PR and check that test works on CI.
timeout: 60
properties:
tags: >
["devicelab", "ios", "mac"]
task_name: native_assets_ios_simulator
- name: Mac_ios native_assets_ios
recipe: devicelab/devicelab_drone
presubmit: false
bringup: true # TODO(dacoharkes): Set to false in follow up PR and check that test works on CI.
timeout: 60
properties:
tags: >
["devicelab", "ios", "mac"]
task_name: native_assets_ios
- name: Mac_ios native_platform_view_ui_tests_ios
recipe: devicelab/devicelab_drone
presubmit: false

View File

@ -199,6 +199,8 @@
/dev/devicelab/bin/tasks/large_image_changer_perf_ios.dart @zanderso @flutter/engine
/dev/devicelab/bin/tasks/macos_chrome_dev_mode.dart @zanderso @flutter/tool
/dev/devicelab/bin/tasks/microbenchmarks_ios.dart @cyanglaz @flutter/engine
/dev/devicelab/bin/tasks/native_assets_ios_simulator.dart @dacoharkes @flutter/ios
/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
/dev/devicelab/bin/tasks/new_gallery_skia_ios__transition_perf.dart @zanderso @flutter/engine

View File

@ -59,6 +59,36 @@ Future<void> main() async {
final File marquee = File(path.join(flutterModuleLibSource.path, 'marquee'));
marquee.copySync(path.join(flutterModuleLibDestination.path, 'marquee.dart'));
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:\n',
'''
dependencies:
$ffiPackageName:
path: ../$ffiPackageName
''',
);
await pubspec.writeAsString(content, flush: true);
await inDirectory(projectDir, () async {
await flutter(
'packages',
options: <String>['get'],
);
});
section('Build ephemeral host app in release mode without CocoaPods');
await inDirectory(projectDir, () async {
@ -162,10 +192,8 @@ Future<void> main() async {
section('Add plugins');
final File pubspec = File(path.join(projectDir.path, 'pubspec.yaml'));
String content = await pubspec.readAsString();
content = content.replaceFirst(
'\ndependencies:\n',
'dependencies:\n',
// One framework, one Dart-only, one that does not support iOS, and one with a resource bundle.
'''
dependencies:
@ -221,6 +249,11 @@ dependencies:
// Dart-only, no embedded framework.
checkDirectoryNotExists(path.join(ephemeralIOSHostApp.path, 'Frameworks', '$dartPluginName.framework'));
// Native assets embedded, no embedded framework.
const String libFfiPackageDylib = 'lib$ffiPackageName.dylib';
checkFileExists(path.join(ephemeralIOSHostApp.path, 'Frameworks', libFfiPackageDylib));
checkDirectoryNotExists(path.join(ephemeralIOSHostApp.path, 'Frameworks', '$ffiPackageName.framework'));
section('Clean and pub get module');
await inDirectory(projectDir, () async {
@ -350,6 +383,11 @@ end
'isolate_snapshot_data',
));
checkFileExists(path.join(
hostFrameworksDirectory,
libFfiPackageDylib,
));
section('Check the NOTICE file is correct');
final String licenseFilePath = path.join(
@ -449,6 +487,13 @@ end
throw TaskResult.failure('Unexpected armv7 architecture slice in $builtAppBinary');
}
// Check native assets are bundled.
checkFileExists(path.join(
archivedAppPath,
'Frameworks',
libFfiPackageDylib,
));
// The host app example builds plugins statically, url_launcher_ios.framework
// should not exist.
checkDirectoryNotExists(path.join(
@ -685,3 +730,17 @@ 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>[
'--org',
'io.flutter.devicelab',
'--template=package_ffi',
name,
],
);
});
}

View File

@ -0,0 +1,14 @@
// 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.ios;
return createNativeAssetsTest()();
});
}

View File

@ -0,0 +1,31 @@
// 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/framework/ios.dart';
import 'package:flutter_devicelab/framework/task_result.dart';
import 'package:flutter_devicelab/tasks/native_assets_test.dart';
Future<void> main() async {
await task(() async {
deviceOperatingSystem = DeviceOperatingSystem.ios;
String? simulatorDeviceId;
try {
await testWithNewIOSSimulator(
'TestNativeAssetsSim',
(String deviceId) async {
simulatorDeviceId = deviceId;
await createNativeAssetsTest(
deviceIdOverride: deviceId,
isIosSimulator: true,
)();
},
);
} finally {
await removeIOSimulator(simulatorDeviceId);
}
return TaskResult.success(null);
});
}

View File

@ -0,0 +1,191 @@
// 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 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as path;
import '../framework/devices.dart';
import '../framework/framework.dart';
import '../framework/task_result.dart';
import '../framework/utils.dart';
const String _packageName = 'package_with_native_assets';
const List<String> _buildModes = <String>[
'debug',
'profile',
'release',
];
TaskFunction createNativeAssetsTest({
String? deviceIdOverride,
bool checkAppRunningOnLocalDevice = true,
bool isIosSimulator = false,
}) {
return () async {
if (deviceIdOverride == null) {
final Device device = await devices.workingDevice;
await device.unlock();
deviceIdOverride = device.deviceId;
}
await enableNativeAssets();
for (final String buildMode in _buildModes) {
if (buildMode != 'debug' && isIosSimulator) {
continue;
}
final TaskResult buildModeResult = await inTempDir((Directory tempDirectory) async {
final Directory packageDirectory = await createTestProject(_packageName, tempDirectory);
final Directory exampleDirectory = dir(packageDirectory.uri.resolve('example/').toFilePath());
final List<String> options = <String>[
'-d',
deviceIdOverride!,
'--no-android-gradle-daemon',
'--no-publish-port',
'--verbose',
'--uninstall-first',
'--$buildMode',
];
int transitionCount = 0;
bool done = false;
await inDirectory<void>(exampleDirectory, () async {
final int runFlutterResult = await runFlutter(
options: options,
onLine: (String line, Process process) {
if (done) {
return;
}
switch (transitionCount) {
case 0:
if (!line.contains('Flutter run key commands.')) {
return;
}
if (buildMode == 'debug') {
// Do a hot reload diff on the initial dill file.
process.stdin.writeln('r');
} else {
done = true;
process.stdin.writeln('q');
}
case 1:
if (!line.contains('Reloaded')) {
return;
}
process.stdin.writeln('R');
case 2:
// Do a hot restart, pushing a new complete dill file.
if (!line.contains('Restarted application')) {
return;
}
// Do another hot reload, pushing a diff to the second dill file.
process.stdin.writeln('r');
case 3:
if (!line.contains('Reloaded')) {
return;
}
done = true;
process.stdin.writeln('q');
}
transitionCount += 1;
},
);
if (runFlutterResult != 0) {
print('Flutter run returned non-zero exit code: $runFlutterResult.');
}
});
final int expectedNumberOfTransitions = buildMode == 'debug' ? 4 : 1;
if (transitionCount != expectedNumberOfTransitions) {
return TaskResult.failure(
'Did not get expected number of transitions: $transitionCount '
'(expected $expectedNumberOfTransitions)',
);
}
return TaskResult.success(null);
});
if (buildModeResult.failed) {
return buildModeResult;
}
}
return TaskResult.success(null);
};
}
Future<int> runFlutter({
required List<String> options,
required void Function(String, Process) onLine,
}) async {
final Process process = await startFlutter(
'run',
options: options,
);
final Completer<void> stdoutDone = Completer<void>();
final Completer<void> stderrDone = Completer<void>();
process.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen((String line) {
onLine(line, process);
print('stdout: $line');
}, onDone: stdoutDone.complete);
process.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(
(String line) => print('stderr: $line'),
onDone: stderrDone.complete,
);
await Future.wait<void>(<Future<void>>[stdoutDone.future, stderrDone.future]);
final int exitCode = await process.exitCode;
return exitCode;
}
final String _flutterBin = path.join(flutterDirectory.path, 'bin', 'flutter');
Future<void> enableNativeAssets() async {
print('Enabling configs for native assets...');
final int configResult = await exec(
_flutterBin,
<String>[
'config',
'-v',
'--enable-native-assets',
],
canFail: true);
if (configResult != 0) {
print('Failed to enable configuration, tasks may not run.');
}
}
Future<Directory> createTestProject(
String packageName,
Directory tempDirectory,
) async {
final int createResult = await exec(
_flutterBin,
<String>[
'create',
'--template=package_ffi',
packageName,
],
workingDirectory: tempDirectory.path,
canFail: true,
);
assert(createResult == 0);
final Directory packageDirectory = Directory.fromUri(tempDirectory.uri.resolve('$packageName/'));
return packageDirectory;
}
Future<T> inTempDir<T>(Future<T> Function(Directory tempDirectory) fun) async {
final Directory tempDirectory = dir(Directory.systemTemp.createTempSync().resolveSymbolicLinksSync());
try {
return await fun(tempDirectory);
} finally {
tempDirectory.deleteSync(recursive: true);
}
}

View File

@ -7,6 +7,7 @@ import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:ffi_package/ffi_package.dart';
import 'marquee.dart';
@ -116,10 +117,15 @@ class _MyHomePageState extends State<MyHomePage> {
// button on the Flutter page has been tapped.
int _counter = 0;
late int sumResult;
late Future<int> sumAsyncResult;
@override
void initState() {
super.initState();
_platform.setMessageHandler(_handlePlatformIncrement);
sumResult = sum(1, 2);
sumAsyncResult = sumAsync(3, 4);
}
/// Directly increments our internal counter and rebuilds the UI.

View File

@ -144,8 +144,8 @@ BuildApp() {
RunCommand "${flutter_args[@]}"
}
# Adds the App.framework as an embedded binary and the flutter_assets as
# resources.
# Adds the App.framework as an embedded binary, the flutter_assets as
# resources, and the native assets.
EmbedFrameworks() {
# Embed App.framework from Flutter into the app (after creating the Frameworks directory
# if it doesn't already exist).
@ -164,6 +164,17 @@ EmbedFrameworks() {
RunCommand codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${xcode_frameworks_dir}/App.framework/App"
RunCommand codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${xcode_frameworks_dir}/FlutterMacOS.framework/FlutterMacOS"
fi
# Copy the native assets. These do not have to be codesigned here because,
# they are already codesigned in buildNativeAssetsMacOS.
local project_path="${SOURCE_ROOT}/.."
if [[ -n "$FLUTTER_APPLICATION_PATH" ]]; then
project_path="${FLUTTER_APPLICATION_PATH}"
fi
local native_assets_path="${project_path}/${FLUTTER_BUILD_DIR}/native_assets/macos/"
if [[ -d "$native_assets_path" ]]; then
RunCommand rsync -av --filter "- .DS_Store" --filter "- native_assets.yaml" "${native_assets_path}" "${xcode_frameworks_dir}"
fi
}
# Main entry point.

View File

@ -171,6 +171,32 @@ class Context {
exitApp(-1);
}
/// Copies all files from [source] to [destination].
///
/// Does not copy `.DS_Store`.
///
/// If [delete], delete extraneous files from [destination].
void runRsync(
String source,
String destination, {
List<String> extraArgs = const <String>[],
bool delete = false,
}) {
runSync(
'rsync',
<String>[
'-8', // Avoid mangling filenames with encodings that do not match the current locale.
'-av',
if (delete) '--delete',
'--filter',
'- .DS_Store',
...extraArgs,
source,
destination,
],
);
}
// Adds the App.framework as an embedded binary and the flutter_assets as
// resources.
void embedFlutterFrameworks() {
@ -185,33 +211,46 @@ class Context {
xcodeFrameworksDir,
]
);
runSync(
'rsync',
<String>[
'-8', // Avoid mangling filenames with encodings that do not match the current locale.
'-av',
'--delete',
'--filter',
'- .DS_Store',
'${environment['BUILT_PRODUCTS_DIR']}/App.framework',
xcodeFrameworksDir,
],
runRsync(
delete: true,
'${environment['BUILT_PRODUCTS_DIR']}/App.framework',
xcodeFrameworksDir,
);
// Embed the actual Flutter.framework that the Flutter app expects to run against,
// which could be a local build or an arch/type specific build.
runSync(
'rsync',
<String>[
'-av',
'--delete',
'--filter',
'- .DS_Store',
'${environment['BUILT_PRODUCTS_DIR']}/Flutter.framework',
'$xcodeFrameworksDir/',
],
runRsync(
delete: true,
'${environment['BUILT_PRODUCTS_DIR']}/Flutter.framework',
'$xcodeFrameworksDir/',
);
// Copy the native assets. These do not have to be codesigned here because,
// they are already codesigned in buildNativeAssetsMacOS.
final String sourceRoot = environment['SOURCE_ROOT'] ?? '';
String projectPath = '$sourceRoot/..';
if (environment['FLUTTER_APPLICATION_PATH'] != null) {
projectPath = environment['FLUTTER_APPLICATION_PATH']!;
}
final String flutterBuildDir = environment['FLUTTER_BUILD_DIR']!;
final String nativeAssetsPath = '$projectPath/$flutterBuildDir/native_assets/ios/';
final bool verbose = (environment['VERBOSE_SCRIPT_LOGGING'] ?? '').isNotEmpty;
if (Directory(nativeAssetsPath).existsSync()) {
if (verbose) {
print('♦ Copying native assets from $nativeAssetsPath.');
}
runRsync(
extraArgs: <String>[
'--filter',
'- native_assets.yaml',
],
nativeAssetsPath,
xcodeFrameworksDir,
);
} else if (verbose) {
print("♦ No native assets to bundle. $nativeAssetsPath doesn't exist.");
}
addVmServiceBonjourService();
}

View File

@ -777,6 +777,8 @@ TargetPlatform getTargetPlatformForName(String platform) {
return TargetPlatform.windows_x64;
case 'web-javascript':
return TargetPlatform.web_javascript;
case 'flutter-tester':
return TargetPlatform.tester;
}
throw Exception('Unsupported platform name "$platform"');
}

View File

@ -6,6 +6,7 @@ import 'package:package_config/package_config.dart';
import '../../artifacts.dart';
import '../../base/build.dart';
import '../../base/common.dart';
import '../../base/file_system.dart';
import '../../base/io.dart';
import '../../build_info.dart';
@ -19,6 +20,7 @@ import 'assets.dart';
import 'dart_plugin_registrant.dart';
import 'icon_tree_shaker.dart';
import 'localizations.dart';
import 'native_assets.dart';
import 'shader_compiler.dart';
/// Copies the pre-built flutter bundle.
@ -125,6 +127,7 @@ class KernelSnapshot extends Target {
@override
List<Source> get inputs => const <Source>[
Source.pattern('{BUILD_DIR}/native_assets.yaml'),
Source.pattern('{PROJECT_DIR}/.dart_tool/package_config_subset'),
Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/common.dart'),
Source.artifact(Artifact.platformKernelDill),
@ -142,6 +145,7 @@ class KernelSnapshot extends Target {
@override
List<Target> get dependencies => const <Target>[
NativeAssets(),
GenerateLocalizationsTarget(),
DartPluginRegistrantTarget(),
];
@ -178,6 +182,13 @@ class KernelSnapshot extends Target {
final List<String>? fileSystemRoots = environment.defines[kFileSystemRoots]?.split(',');
final String? fileSystemScheme = environment.defines[kFileSystemScheme];
final File nativeAssetsFile = environment.buildDir.childFile('native_assets.yaml');
final String nativeAssets = nativeAssetsFile.path;
if (!await nativeAssetsFile.exists()) {
throwToolExit("$nativeAssets doesn't exist.");
}
environment.logger.printTrace('Embedding native assets mapping $nativeAssets in kernel.');
TargetModel targetModel = TargetModel.flutter;
if (targetPlatform == TargetPlatform.fuchsia_x64 ||
targetPlatform == TargetPlatform.fuchsia_arm64) {
@ -251,6 +262,7 @@ class KernelSnapshot extends Target {
buildDir: environment.buildDir,
targetOS: targetOS,
checkDartPluginRegistry: environment.generateDartPluginRegistry,
nativeAssets: nativeAssets,
);
if (output == null || output.errorCount != 0) {
throw Exception();

View File

@ -0,0 +1,183 @@
// 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:meta/meta.dart';
import 'package:native_assets_cli/native_assets_cli.dart' show Asset;
import '../../base/common.dart';
import '../../base/file_system.dart';
import '../../base/platform.dart';
import '../../build_info.dart';
import '../../ios/native_assets.dart';
import '../../macos/native_assets.dart';
import '../../macos/xcode.dart';
import '../../native_assets.dart';
import '../build_system.dart';
import '../depfile.dart';
import '../exceptions.dart';
import 'common.dart';
/// Builds the right native assets for a Flutter app.
///
/// The build mode and target architecture can be changed from the
/// native build project (Xcode etc.), so only `flutter assemble` has the
/// information about build-mode and target architecture.
/// Invocations of flutter_tools other than `flutter assemble` are dry runs.
///
/// This step needs to be consistent with the dry run invocations in `flutter
/// run`s so that the kernel mapping of asset id to dylib lines up after hot
/// restart.
///
/// [KernelSnapshot] depends on this target. We produce a native_assets.yaml
/// here, and embed that mapping inside the kernel snapshot.
///
/// The build always produces a valid native_assets.yaml and a native_assets.d
/// even if there are no native assets. This way the caching logic won't try to
/// rebuild.
class NativeAssets extends Target {
const NativeAssets({
@visibleForTesting NativeAssetsBuildRunner? buildRunner,
}) : _buildRunner = buildRunner;
final NativeAssetsBuildRunner? _buildRunner;
@override
Future<void> build(Environment environment) async {
final String? targetPlatformEnvironment = environment.defines[kTargetPlatform];
if (targetPlatformEnvironment == null) {
throw MissingDefineException(kTargetPlatform, name);
}
final TargetPlatform targetPlatform = getTargetPlatformForName(targetPlatformEnvironment);
final Uri projectUri = environment.projectDir.uri;
final FileSystem fileSystem = environment.fileSystem;
final NativeAssetsBuildRunner buildRunner = _buildRunner ?? NativeAssetsBuildRunnerImpl(projectUri, fileSystem, environment.logger);
final List<Uri> dependencies;
switch (targetPlatform) {
case TargetPlatform.ios:
final String? iosArchsEnvironment = environment.defines[kIosArchs];
if (iosArchsEnvironment == null) {
throw MissingDefineException(kIosArchs, name);
}
final List<DarwinArch> iosArchs = iosArchsEnvironment.split(' ').map(getDarwinArchForName).toList();
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,
);
case TargetPlatform.darwin:
final String? darwinArchsEnvironment = environment.defines[kDarwinArchs];
if (darwinArchsEnvironment == null) {
throw MissingDefineException(kDarwinArchs, name);
}
final List<DarwinArch> darwinArchs = darwinArchsEnvironment.split(' ').map(getDarwinArchForName).toList();
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,
);
case TargetPlatform.tester:
if (const LocalPlatform().isMacOS) {
(_, dependencies) = await buildNativeAssetsMacOS(
buildMode: BuildMode.debug,
projectUri: projectUri,
codesignIdentity: environment.defines[kCodesignIdentity],
yamlParentDirectory: environment.buildDir.uri,
fileSystem: fileSystem,
buildRunner: buildRunner,
flutterTester: true,
);
} else {
// TODO(dacoharkes): Implement other OSes. https://github.com/flutter/flutter/issues/129757
// Write the file we claim to have in the [outputs].
await writeNativeAssetsYaml(<Asset>[], environment.buildDir.uri, fileSystem);
dependencies = <Uri>[];
}
case TargetPlatform.android_arm:
case TargetPlatform.android_arm64:
case TargetPlatform.android_x64:
case TargetPlatform.android_x86:
case TargetPlatform.android:
case TargetPlatform.fuchsia_arm64:
case TargetPlatform.fuchsia_x64:
case TargetPlatform.linux_arm64:
case TargetPlatform.linux_x64:
case TargetPlatform.web_javascript:
case TargetPlatform.windows_x64:
// TODO(dacoharkes): Implement other OSes. https://github.com/flutter/flutter/issues/129757
// Write the file we claim to have in the [outputs].
await writeNativeAssetsYaml(<Asset>[], environment.buildDir.uri, fileSystem);
dependencies = <Uri>[];
}
final File nativeAssetsFile = environment.buildDir.childFile('native_assets.yaml');
final Depfile depfile = Depfile(
<File>[
for (final Uri dependency in dependencies) fileSystem.file(dependency),
],
<File>[
nativeAssetsFile,
],
);
final File outputDepfile = environment.buildDir.childFile('native_assets.d');
if (!outputDepfile.parent.existsSync()) {
outputDepfile.parent.createSync(recursive: true);
}
environment.depFileService.writeToFile(depfile, outputDepfile);
if (!await nativeAssetsFile.exists()) {
throwToolExit("${nativeAssetsFile.path} doesn't exist.");
}
if (!await outputDepfile.exists()) {
throwToolExit("${outputDepfile.path} doesn't exist.");
}
}
@override
List<String> get depfiles => <String>[
'native_assets.d',
];
@override
List<Target> get dependencies => <Target>[];
@override
List<Source> get inputs => const <Source>[
Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/native_assets.dart'),
// If different packages are resolved, different native assets might need to be built.
Source.pattern('{PROJECT_DIR}/.dart_tool/package_config_subset'),
];
@override
String get name => 'native_assets';
@override
List<Source> get outputs => const <Source>[
Source.pattern('{BUILD_DIR}/native_assets.yaml'),
];
}

View File

@ -270,6 +270,28 @@ class BuildIOSFrameworkCommand extends BuildFrameworkCommand {
final Status status = globals.logger.startProgress(
' └─Moving to ${globals.fs.path.relative(modeDirectory.path)}');
// Copy the native assets. The native assets have already been signed in
// buildNativeAssetsMacOS.
final Directory nativeAssetsDirectory = globals.fs
.directory(getBuildDirectory())
.childDirectory('native_assets/ios/');
if (await nativeAssetsDirectory.exists()) {
final ProcessResult rsyncResult = await globals.processManager.run(<Object>[
'rsync',
'-av',
'--filter',
'- .DS_Store',
'--filter',
'- native_assets.yaml',
nativeAssetsDirectory.path,
modeDirectory.path,
]);
if (rsyncResult.exitCode != 0) {
throwToolExit('Failed to copy native assets:\n${rsyncResult.stderr}');
}
}
try {
// Delete the intermediaries since they would have been copied into our
// output frameworks.

View File

@ -6,6 +6,7 @@ import 'package:meta/meta.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/process.dart';
import '../base/utils.dart';
@ -96,6 +97,26 @@ class BuildMacOSFrameworkCommand extends BuildFrameworkCommand {
globals.logger.printStatus(' └─Moving to ${globals.fs.path.relative(modeDirectory.path)}');
// Copy the native assets.
final Directory nativeAssetsDirectory = globals.fs
.directory(getBuildDirectory())
.childDirectory('native_assets/macos/');
if (await nativeAssetsDirectory.exists()) {
final ProcessResult rsyncResult = await globals.processManager.run(<Object>[
'rsync',
'-av',
'--filter',
'- .DS_Store',
'--filter',
'- native_assets.yaml',
nativeAssetsDirectory.path,
modeDirectory.path,
]);
if (rsyncResult.exitCode != 0) {
throwToolExit('Failed to copy native assets:\n${rsyncResult.stderr}');
}
}
// Delete the intermediaries since they would have been copied into our
// output frameworks.
if (buildOutput.existsSync()) {

View File

@ -36,10 +36,11 @@ class CreateCommand extends CreateBase {
argParser.addOption(
'template',
abbr: 't',
allowed: FlutterProjectType.values.map<String>((FlutterProjectType e) => e.cliName),
allowed: FlutterProjectType.enabledValues
.map<String>((FlutterProjectType e) => e.cliName),
help: 'Specify the type of project to create.',
valueHelp: 'type',
allowedHelp: CliEnum.allowedHelp(FlutterProjectType.values),
allowedHelp: CliEnum.allowedHelp(FlutterProjectType.enabledValues),
);
argParser.addOption(
'sample',
@ -206,12 +207,14 @@ class CreateCommand extends CreateBase {
final FlutterProjectType template = _getProjectType(projectDir);
final bool generateModule = template == FlutterProjectType.module;
final bool generateMethodChannelsPlugin = template == FlutterProjectType.plugin;
final bool generateFfiPackage = template == FlutterProjectType.packageFfi;
final bool generateFfiPlugin = template == FlutterProjectType.pluginFfi;
final bool generateFfi = generateFfiPlugin || generateFfiPackage;
final bool generatePackage = template == FlutterProjectType.package;
final List<String> platforms = stringsArg('platforms');
// `--platforms` does not support module or package.
if (argResults!.wasParsed('platforms') && (generateModule || generatePackage)) {
if (argResults!.wasParsed('platforms') && (generateModule || generatePackage || generateFfiPackage)) {
final String template = generateModule ? 'module' : 'package';
throwToolExit(
'The "--platforms" argument is not supported in $template template.',
@ -225,15 +228,15 @@ class CreateCommand extends CreateBase {
'The web platform is not supported in plugin_ffi template.',
exitCode: 2,
);
} else if (generateFfiPlugin && argResults!.wasParsed('ios-language')) {
} else if (generateFfi && argResults!.wasParsed('ios-language')) {
throwToolExit(
'The "ios-language" option is not supported with the plugin_ffi '
'The "ios-language" option is not supported with the ${template.cliName} '
'template: the language will always be C or C++.',
exitCode: 2,
);
} else if (generateFfiPlugin && argResults!.wasParsed('android-language')) {
} else if (generateFfi && argResults!.wasParsed('android-language')) {
throwToolExit(
'The "android-language" option is not supported with the plugin_ffi '
'The "android-language" option is not supported with the ${template.cliName} '
'template: the language will always be C or C++.',
exitCode: 2,
);
@ -306,6 +309,7 @@ class CreateCommand extends CreateBase {
flutterRoot: flutterRoot,
withPlatformChannelPluginHook: generateMethodChannelsPlugin,
withFfiPluginHook: generateFfiPlugin,
withFfiPackage: generateFfiPackage,
withEmptyMain: emptyArgument,
androidLanguage: stringArg('android-language'),
iosLanguage: stringArg('ios-language'),
@ -393,6 +397,15 @@ class CreateCommand extends CreateBase {
projectType: template,
);
pubContext = PubContext.createPlugin;
case FlutterProjectType.packageFfi:
generatedFileCount += await _generateFfiPackage(
relativeDir,
templateContext,
overwrite: overwrite,
printStatusWhenWriting: !creatingNewProject,
projectType: template,
);
pubContext = PubContext.createPackage;
}
if (boolArg('pub')) {
@ -403,14 +416,21 @@ class CreateCommand extends CreateBase {
offline: boolArg('offline'),
outputMode: PubOutputMode.summaryOnly,
);
await project.ensureReadyForPlatformSpecificTooling(
androidPlatform: includeAndroid,
iosPlatform: includeIos,
linuxPlatform: includeLinux,
macOSPlatform: includeMacos,
windowsPlatform: includeWindows,
webPlatform: includeWeb,
);
// Setting `includeIos` etc to false as with FlutterProjectType.package
// causes the example sub directory to not get os sub directories.
// This will lead to `flutter build ios` to fail in the example.
// TODO(dacoharkes): Uncouple the app and parent project platforms. https://github.com/flutter/flutter/issues/133874
// Then this if can be removed.
if (!generateFfiPackage) {
await project.ensureReadyForPlatformSpecificTooling(
androidPlatform: includeAndroid,
iosPlatform: includeIos,
linuxPlatform: includeLinux,
macOSPlatform: includeMacos,
windowsPlatform: includeWindows,
webPlatform: includeWeb,
);
}
}
if (sampleCode != null) {
_applySample(relativeDir, sampleCode);
@ -663,6 +683,48 @@ Your $application code is in $relativeAppMain.
return generatedCount;
}
Future<int> _generateFfiPackage(
Directory directory,
Map<String, Object?> templateContext, {
bool overwrite = false,
bool printStatusWhenWriting = true,
required FlutterProjectType projectType,
}) async {
int generatedCount = 0;
final String? description = argResults!.wasParsed('description')
? stringArg('description')
: 'A new Dart FFI package project.';
templateContext['description'] = description;
generatedCount += await renderMerged(
<String>[
'package_ffi',
],
directory,
templateContext,
overwrite: overwrite,
printStatusWhenWriting: printStatusWhenWriting,
);
final FlutterProject project = FlutterProject.fromDirectory(directory);
final String? projectName = templateContext['projectName'] as String?;
final String exampleProjectName = '${projectName}_example';
templateContext['projectName'] = exampleProjectName;
templateContext['description'] = 'Demonstrates how to use the $projectName package.';
templateContext['pluginProjectName'] = projectName;
generatedCount += await generateApp(
<String>['app'],
project.example.directory,
templateContext,
overwrite: overwrite,
pluginExampleApp: true,
printStatusWhenWriting: printStatusWhenWriting,
projectType: projectType,
);
return generatedCount;
}
// Takes an application template and replaces the main.dart with one from the
// documentation website in sampleCode. Returns the difference in the number
// of files after applying the sample, since it also deletes the application's

View File

@ -352,6 +352,7 @@ abstract class CreateBase extends FlutterCommand {
String? gradleVersion,
bool withPlatformChannelPluginHook = false,
bool withFfiPluginHook = false,
bool withFfiPackage = false,
bool withEmptyMain = false,
bool ios = false,
bool android = false,
@ -399,9 +400,11 @@ abstract class CreateBase extends FlutterCommand {
'pluginClassCapitalSnakeCase': pluginClassCapitalSnakeCase,
'pluginDartClass': pluginDartClass,
'pluginProjectUUID': const Uuid().v4().toUpperCase(),
'withFfi': withFfiPluginHook || withFfiPackage,
'withFfiPackage': withFfiPackage,
'withFfiPluginHook': withFfiPluginHook,
'withPlatformChannelPluginHook': withPlatformChannelPluginHook,
'withPluginHook': withFfiPluginHook || withPlatformChannelPluginHook,
'withPluginHook': withFfiPluginHook || withFfiPackage || withPlatformChannelPluginHook,
'withEmptyMain': withEmptyMain,
'androidLanguage': androidLanguage,
'iosLanguage': iosLanguage,

View File

@ -240,6 +240,7 @@ class KernelCompiler {
required bool trackWidgetCreation,
required List<String> dartDefines,
required PackageConfig packageConfig,
String? nativeAssets,
}) async {
final TargetPlatform? platform = targetModel == TargetModel.dartdevc ? TargetPlatform.web_javascript : null;
final String frontendServer = _artifacts.getArtifactPath(
@ -337,6 +338,10 @@ class KernelCompiler {
'package:flutter/src/dart_plugin_registrant.dart',
'-Dflutter.dart_plugin_registrant=$dartPluginRegistrantUri',
],
if (nativeAssets != null) ...<String>[
'--native-assets',
nativeAssets,
],
// See: https://github.com/flutter/flutter/issues/103994
'--verbosity=error',
...?extraFrontEndOptions,
@ -381,9 +386,10 @@ class _RecompileRequest extends _CompilationRequest {
this.invalidatedFiles,
this.outputPath,
this.packageConfig,
this.suppressErrors,
{this.additionalSourceUri}
);
this.suppressErrors, {
this.additionalSourceUri,
this.nativeAssetsYamlUri,
});
Uri mainUri;
List<Uri>? invalidatedFiles;
@ -391,6 +397,7 @@ class _RecompileRequest extends _CompilationRequest {
PackageConfig packageConfig;
bool suppressErrors;
final Uri? additionalSourceUri;
final Uri? nativeAssetsYamlUri;
@override
Future<CompilerOutput?> _run(DefaultResidentCompiler compiler) async =>
@ -515,6 +522,7 @@ abstract class ResidentCompiler {
bool suppressErrors = false,
bool checkDartPluginRegistry = false,
File? dartPluginRegistrant,
Uri? nativeAssetsYaml,
});
Future<CompilerOutput?> compileExpression(
@ -663,6 +671,7 @@ class DefaultResidentCompiler implements ResidentCompiler {
File? dartPluginRegistrant,
String? projectRootPath,
FileSystem? fs,
Uri? nativeAssetsYaml,
}) async {
if (!_controller.hasListener) {
_controller.stream.listen(_handleCompilationRequest);
@ -681,6 +690,7 @@ class DefaultResidentCompiler implements ResidentCompiler {
packageConfig,
suppressErrors,
additionalSourceUri: additionalSourceUri,
nativeAssetsYamlUri: nativeAssetsYaml,
));
return completer.future;
}
@ -699,12 +709,22 @@ class DefaultResidentCompiler implements ResidentCompiler {
toMultiRootPath(request.additionalSourceUri!, fileSystemScheme, fileSystemRoots, _platform.isWindows);
}
final String? nativeAssets = request.nativeAssetsYamlUri?.toString();
final Process? server = _server;
if (server == null) {
return _compile(mainUri, request.outputPath, additionalSourceUri: additionalSourceUri);
return _compile(
mainUri,
request.outputPath,
additionalSourceUri: additionalSourceUri,
nativeAssetsUri: nativeAssets,
);
}
final String inputKey = Uuid().generateV4();
if (nativeAssets != null && nativeAssets.isNotEmpty) {
server.stdin.writeln('native-assets $nativeAssets');
_logger.printTrace('<- native-assets $nativeAssets');
}
server.stdin.writeln('recompile $mainUri $inputKey');
_logger.printTrace('<- recompile $mainUri $inputKey');
final List<Uri>? invalidatedFiles = request.invalidatedFiles;
@ -746,9 +766,10 @@ class DefaultResidentCompiler implements ResidentCompiler {
Future<CompilerOutput?> _compile(
String scriptUri,
String? outputPath,
{String? additionalSourceUri}
) async {
String? outputPath, {
String? additionalSourceUri,
String? nativeAssetsUri,
}) async {
final TargetPlatform? platform = (targetModel == TargetModel.dartdevc) ? TargetPlatform.web_javascript : null;
final String frontendServer = artifacts.getArtifactPath(
Artifact.frontendServerSnapshotForEngineDartSdk,
@ -806,6 +827,10 @@ class DefaultResidentCompiler implements ResidentCompiler {
'package:flutter/src/dart_plugin_registrant.dart',
'-Dflutter.dart_plugin_registrant=$additionalSourceUri',
],
if (nativeAssetsUri != null) ...<String>[
'--native-assets',
nativeAssetsUri,
],
if (platformDill != null) ...<String>[
'--platform',
platformDill!,
@ -842,6 +867,11 @@ class DefaultResidentCompiler implements ResidentCompiler {
}
}));
if (nativeAssetsUri != null && nativeAssetsUri.isNotEmpty) {
_server?.stdin.writeln('native-assets $nativeAssetsUri');
_logger.printTrace('<- native-assets $nativeAssetsUri');
}
_server?.stdin.writeln('compile $scriptUri');
_logger.printTrace('<- compile $scriptUri');

View File

@ -50,6 +50,9 @@ abstract class FeatureFlags {
/// Whether animations are used in the command line interface.
bool get isCliAnimationEnabled => true;
/// Whether native assets compilation and bundling is enabled.
bool get isNativeAssetsEnabled => false;
/// Whether a particular feature is enabled for the current channel.
///
/// Prefer using one of the specific getters above instead of this API.
@ -68,6 +71,7 @@ const List<Feature> allFeatures = <Feature>[
flutterCustomDevicesFeature,
flutterWebWasm,
cliAnimation,
nativeAssets,
];
/// All current Flutter feature flags that can be configured.
@ -158,6 +162,16 @@ const Feature cliAnimation = Feature.fullyEnabled(
configSetting: 'cli-animations',
);
/// Enable native assets compilation and bundling.
const Feature nativeAssets = Feature(
name: 'native assets compilation and bundling',
configSetting: 'enable-native-assets',
environmentOverride: 'FLUTTER_NATIVE_ASSETS',
master: FeatureChannelSetting(
available: true,
),
);
/// A [Feature] is a process for conditionally enabling tool features.
///
/// All settings are optional, and if not provided will generally default to

View File

@ -87,7 +87,6 @@ class FlutterDeviceManager extends DeviceManager {
processManager: processManager,
logger: logger,
artifacts: artifacts,
operatingSystemUtils: operatingSystemUtils,
),
MacOSDevices(
processManager: processManager,

View File

@ -55,6 +55,9 @@ class FlutterFeatureFlags implements FeatureFlags {
return isEnabled(cliAnimation);
}
@override
bool get isNativeAssetsEnabled => isEnabled(nativeAssets);
@override
bool isEnabled(Feature feature) {
final String currentChannel = _flutterVersion.channel;

View File

@ -970,7 +970,7 @@ Future<void> _writeWebPluginRegistrant(FlutterProject project, List<Plugin> plug
/// be created only if missing.
///
/// This uses [project.flutterPluginsDependenciesFile], so it should only be
/// run after refreshPluginList has been run since the last plugin change.
/// run after [refreshPluginsList] has been run since the last plugin change.
void createPluginSymlinks(FlutterProject project, {bool force = false, @visibleForTesting FeatureFlags? featureFlagsOverride}) {
final FeatureFlags localFeatureFlags = featureFlagsOverride ?? featureFlags;
Map<String, Object?>? platformPlugins;

View File

@ -7,6 +7,7 @@ import 'package:yaml/yaml.dart';
import 'base/file_system.dart';
import 'base/logger.dart';
import 'base/utils.dart';
import 'features.dart';
import 'project.dart';
import 'template.dart';
import 'version.dart';
@ -28,6 +29,9 @@ enum FlutterProjectType implements CliEnum {
/// components, only Dart.
package,
/// This is a Dart package project with external builds for native components.
packageFfi,
/// This is a native plugin project.
plugin,
@ -52,6 +56,10 @@ enum FlutterProjectType implements CliEnum {
'Generate a shareable Flutter project containing an API '
'in Dart code with a platform-specific implementation through dart:ffi for Android, iOS, '
'Linux, macOS, Windows, or any combination of these.',
FlutterProjectType.packageFfi =>
'Generate a shareable Dart/Flutter project containing an API '
'in Dart code with a platform-specific implementation through dart:ffi for Android, iOS, '
'Linux, macOS, and Windows.',
FlutterProjectType.module =>
'Generate a project to add a Flutter module to an existing Android or iOS application.',
};
@ -64,6 +72,16 @@ enum FlutterProjectType implements CliEnum {
}
return null;
}
static List<FlutterProjectType> get enabledValues {
return <FlutterProjectType>[
for (final FlutterProjectType value in values)
if (value == FlutterProjectType.packageFfi) ...<FlutterProjectType>[
if (featureFlags.isNativeAssetsEnabled) value
] else
value,
];
}
}
/// Verifies the expected yaml keys are present in the file.

View File

@ -0,0 +1,171 @@
// 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;
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/file_system.dart';
import '../build_info.dart';
import '../globals.dart' as globals;
import '../macos/native_assets_host.dart';
import '../native_assets.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 and
/// the Xcode project.
Future<Uri?> dryRunNativeAssetsIOS({
required NativeAssetsBuildRunner buildRunner,
required Uri projectUri,
required FileSystem fileSystem,
}) async {
if (await hasNoPackageConfig(buildRunner) || await isDisabledAndNoNativeAssets(buildRunner)) {
return null;
}
final Uri buildUri_ = nativeAssetsBuildUri(projectUri, OS.iOS);
final Iterable<Asset> assetTargetLocations = await dryRunNativeAssetsIOSInternal(
fileSystem,
projectUri,
buildRunner,
);
final Uri nativeAssetsUri = await writeNativeAssetsYaml(
assetTargetLocations,
buildUri_,
fileSystem,
);
return nativeAssetsUri;
}
Future<Iterable<Asset>> dryRunNativeAssetsIOSInternal(
FileSystem fileSystem,
Uri projectUri,
NativeAssetsBuildRunner buildRunner,
) async {
const OS targetOs = OS.iOS;
globals.logger.printTrace('Dry running native assets for $targetOs.');
final List<Asset> nativeAssets = (await buildRunner.dryRun(
linkModePreference: LinkModePreference.dynamic,
targetOs: targetOs,
workingDirectory: projectUri,
includeParentEnvironment: true,
))
.assets;
ensureNoLinkModeStatic(nativeAssets);
globals.logger.printTrace('Dry running native assets for $targetOs done.');
final Iterable<Asset> assetTargetLocations = _assetTargetLocations(nativeAssets).values;
return assetTargetLocations;
}
/// Builds native assets.
Future<List<Uri>> buildNativeAssetsIOS({
required NativeAssetsBuildRunner buildRunner,
required List<DarwinArch> darwinArchs,
required EnvironmentType environmentType,
required Uri projectUri,
required BuildMode buildMode,
String? codesignIdentity,
required Uri yamlParentDirectory,
required FileSystem fileSystem,
}) async {
if (await hasNoPackageConfig(buildRunner) || await isDisabledAndNoNativeAssets(buildRunner)) {
await writeNativeAssetsYaml(<Asset>[], yamlParentDirectory, fileSystem);
return <Uri>[];
}
final List<Target> targets = darwinArchs.map(_getNativeTarget).toList();
final native_assets_cli.BuildMode buildModeCli = nativeAssetsBuildMode(buildMode);
const OS targetOs = OS.iOS;
final Uri buildUri_ = nativeAssetsBuildUri(projectUri, targetOs);
final IOSSdk iosSdk = _getIOSSdk(environmentType);
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,
targetIOSSdk: iosSdk,
buildMode: buildModeCli,
workingDirectory: projectUri,
includeParentEnvironment: true,
cCompilerConfig: await buildRunner.cCompilerConfig,
);
nativeAssets.addAll(result.assets);
dependencies.addAll(result.dependencies);
}
ensureNoLinkModeStatic(nativeAssets);
globals.logger.printTrace('Building native assets for $targets done.');
final Map<AssetPath, List<Asset>> fatAssetTargetLocations = _fatAssetTargetLocations(nativeAssets);
await copyNativeAssetsMacOSHost(
buildUri_,
fatAssetTargetLocations,
codesignIdentity,
buildMode,
fileSystem,
);
final Map<Asset, Asset> assetTargetLocations = _assetTargetLocations(nativeAssets);
await writeNativeAssetsYaml(
assetTargetLocations.values,
yamlParentDirectory,
fileSystem,
);
return dependencies.toList();
}
IOSSdk _getIOSSdk(EnvironmentType environmentType) {
switch (environmentType) {
case EnvironmentType.physical:
return IOSSdk.iPhoneOs;
case EnvironmentType.simulator:
return IOSSdk.iPhoneSimulator;
}
}
/// Extract the [Target] from a [DarwinArch].
Target _getNativeTarget(DarwinArch darwinArch) {
switch (darwinArch) {
case DarwinArch.armv7:
return Target.iOSArm;
case DarwinArch.arm64:
return Target.iOSArm64;
case DarwinArch.x86_64:
return Target.iOSX64;
}
}
Map<AssetPath, List<Asset>> _fatAssetTargetLocations(List<Asset> nativeAssets) {
final Map<AssetPath, List<Asset>> result = <AssetPath, List<Asset>>{};
for (final Asset asset in nativeAssets) {
final AssetPath path = _targetLocationIOS(asset).path;
result[path] ??= <Asset>[];
result[path]!.add(asset);
}
return result;
}
Map<Asset, Asset> _assetTargetLocations(List<Asset> nativeAssets) => <Asset, Asset>{
for (final Asset asset in nativeAssets)
asset: _targetLocationIOS(asset),
};
Asset _targetLocationIOS(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');
}

View File

@ -0,0 +1,162 @@
// 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;
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/file_system.dart';
import '../build_info.dart';
import '../globals.dart' as globals;
import '../native_assets.dart';
import 'native_assets_host.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 and
/// the Xcode project.
Future<Uri?> dryRunNativeAssetsMacOS({
required NativeAssetsBuildRunner buildRunner,
required Uri projectUri,
bool flutterTester = false,
required FileSystem fileSystem,
}) async {
if (await hasNoPackageConfig(buildRunner) || await isDisabledAndNoNativeAssets(buildRunner)) {
return null;
}
final Uri buildUri_ = nativeAssetsBuildUri(projectUri, OS.macOS);
final Iterable<Asset> nativeAssetPaths = await dryRunNativeAssetsMacOSInternal(fileSystem, projectUri, flutterTester, buildRunner);
final Uri nativeAssetsUri = await writeNativeAssetsYaml(nativeAssetPaths, buildUri_, fileSystem);
return nativeAssetsUri;
}
Future<Iterable<Asset>> dryRunNativeAssetsMacOSInternal(
FileSystem fileSystem,
Uri projectUri,
bool flutterTester,
NativeAssetsBuildRunner buildRunner,
) async {
const OS targetOs = OS.macOS;
final Uri buildUri_ = nativeAssetsBuildUri(projectUri, targetOs);
globals.logger.printTrace('Dry running native assets for $targetOs.');
final List<Asset> nativeAssets = (await buildRunner.dryRun(
linkModePreference: LinkModePreference.dynamic,
targetOs: targetOs,
workingDirectory: projectUri,
includeParentEnvironment: true,
))
.assets;
ensureNoLinkModeStatic(nativeAssets);
globals.logger.printTrace('Dry running native assets for $targetOs done.');
final Uri? absolutePath = flutterTester ? buildUri_ : null;
final Map<Asset, Asset> assetTargetLocations = _assetTargetLocations(nativeAssets, absolutePath);
final Iterable<Asset> nativeAssetPaths = assetTargetLocations.values;
return nativeAssetPaths;
}
/// Builds native assets.
///
/// If [darwinArchs] is omitted, the current target architecture is used.
///
/// If [flutterTester] is true, absolute paths are emitted in the native
/// assets mapping. This can be used for JIT mode without sandbox on the host.
/// This is used in `flutter test` and `flutter run -d flutter-tester`.
Future<(Uri? nativeAssetsYaml, List<Uri> dependencies)> buildNativeAssetsMacOS({
required NativeAssetsBuildRunner buildRunner,
List<DarwinArch>? darwinArchs,
required Uri projectUri,
required BuildMode buildMode,
bool flutterTester = false,
String? codesignIdentity,
Uri? yamlParentDirectory,
required FileSystem fileSystem,
}) async {
const OS targetOs = OS.macOS;
final Uri buildUri_ = nativeAssetsBuildUri(projectUri, targetOs);
if (await hasNoPackageConfig(buildRunner) || await isDisabledAndNoNativeAssets(buildRunner)) {
final Uri nativeAssetsYaml = await writeNativeAssetsYaml(<Asset>[], yamlParentDirectory ?? buildUri_, fileSystem);
return (nativeAssetsYaml, <Uri>[]);
}
final List<Target> targets = darwinArchs != null ? darwinArchs.map(_getNativeTarget).toList() : <Target>[Target.current];
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.cCompilerConfig,
);
nativeAssets.addAll(result.assets);
dependencies.addAll(result.dependencies);
}
ensureNoLinkModeStatic(nativeAssets);
globals.logger.printTrace('Building native assets for $targets done.');
final Uri? absolutePath = flutterTester ? buildUri_ : null;
final Map<Asset, Asset> assetTargetLocations = _assetTargetLocations(nativeAssets, absolutePath);
final Map<AssetPath, List<Asset>> fatAssetTargetLocations = _fatAssetTargetLocations(nativeAssets, absolutePath);
await copyNativeAssetsMacOSHost(buildUri_, fatAssetTargetLocations, codesignIdentity, buildMode, fileSystem);
final Uri nativeAssetsUri = await writeNativeAssetsYaml(assetTargetLocations.values, yamlParentDirectory ?? buildUri_, fileSystem);
return (nativeAssetsUri, dependencies.toList());
}
/// Extract the [Target] from a [DarwinArch].
Target _getNativeTarget(DarwinArch darwinArch) {
switch (darwinArch) {
case DarwinArch.arm64:
return Target.macOSArm64;
case DarwinArch.x86_64:
return Target.macOSX64;
case DarwinArch.armv7:
throw Exception('Unknown DarwinArch: $darwinArch.');
}
}
Map<AssetPath, List<Asset>> _fatAssetTargetLocations(List<Asset> nativeAssets, Uri? absolutePath) {
final Map<AssetPath, List<Asset>> result = <AssetPath, List<Asset>>{};
for (final Asset asset in nativeAssets) {
final AssetPath path = _targetLocationMacOS(asset, absolutePath).path;
result[path] ??= <Asset>[];
result[path]!.add(asset);
}
return result;
}
Map<Asset, Asset> _assetTargetLocations(List<Asset> nativeAssets, Uri? absolutePath) => <Asset, Asset>{
for (final Asset asset in nativeAssets)
asset: _targetLocationMacOS(asset, absolutePath),
};
Asset _targetLocationMacOS(Asset asset, Uri? absolutePath) {
final AssetPath path = asset.path;
switch (path) {
case AssetSystemPath _:
case AssetInExecutable _:
case AssetInProcess _:
return asset;
case AssetAbsolutePath _:
final String fileName = path.uri.pathSegments.last;
Uri uri;
if (absolutePath != null) {
// Flutter tester needs full host paths.
uri = absolutePath.resolve(fileName);
} else {
// Flutter Desktop needs "absolute" paths inside the app.
// "relative" in the context of native assets would be relative to the
// kernel or aot snapshot.
uri = Uri(path: fileName);
}
return asset.copyWith(path: AssetAbsolutePath(uri));
}
throw Exception('Unsupported asset path type ${path.runtimeType} in asset $asset');
}

View File

@ -0,0 +1,141 @@
// 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.
// Shared logic between iOS and macOS implementations of native assets.
import 'package:native_assets_cli/native_assets_cli.dart' hide BuildMode;
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../build_info.dart';
import '../convert.dart';
import '../globals.dart' as globals;
/// The target location for native assets on macOS.
///
/// Because we need to have a multi-architecture solution for
/// `flutter run --release`, we use `lipo` to combine all target architectures
/// into a single file.
///
/// We need to set the install name so that it matches what the place it will
/// be bundled in the final app.
///
/// Code signing is also done here, so that we don't have to worry about it
/// in xcode_backend.dart and macos_assemble.sh.
Future<void> copyNativeAssetsMacOSHost(
Uri buildUri,
Map<AssetPath, List<Asset>> assetTargetLocations,
String? codesignIdentity,
BuildMode buildMode,
FileSystem fileSystem,
) async {
if (assetTargetLocations.isNotEmpty) {
globals.logger.printTrace('Copying native assets to ${buildUri.toFilePath()}.');
final Directory buildDir = fileSystem.directory(buildUri.toFilePath());
if (!buildDir.existsSync()) {
buildDir.createSync(recursive: true);
}
for (final MapEntry<AssetPath, List<Asset>> assetMapping in assetTargetLocations.entries) {
final Uri target = (assetMapping.key as AssetAbsolutePath).uri;
final List<Uri> sources = <Uri>[for (final Asset source in assetMapping.value) (source.path as AssetAbsolutePath).uri];
final Uri targetUri = buildUri.resolveUri(target);
final String targetFullPath = targetUri.toFilePath();
await lipoDylibs(targetFullPath, sources);
await setInstallNameDylib(targetUri);
await codesignDylib(codesignIdentity, buildMode, targetFullPath);
}
globals.logger.printTrace('Copying native assets done.');
}
}
/// Combines dylibs from [sources] into a fat binary at [targetFullPath].
///
/// The dylibs must have different architectures. E.g. a dylib targeting
/// arm64 ios simulator cannot be combined with a dylib targeting arm64
/// ios device or macos arm64.
Future<void> lipoDylibs(String targetFullPath, List<Uri> sources) async {
final ProcessResult lipoResult = await globals.processManager.run(
<String>[
'lipo',
'-create',
'-output',
targetFullPath,
for (final Uri source in sources) source.toFilePath(),
],
);
if (lipoResult.exitCode != 0) {
throwToolExit('Failed to create universal binary:\n${lipoResult.stderr}');
}
globals.logger.printTrace(lipoResult.stdout as String);
globals.logger.printTrace(lipoResult.stderr as String);
}
/// Sets the install name in a dylib with a Mach-O format.
///
/// On macOS and iOS, opening a dylib at runtime fails if the path inside the
/// dylib itself does not correspond to the path that the file is at. Therefore,
/// native assets copied into their final location also need their install name
/// updated with the `install_name_tool`.
Future<void> setInstallNameDylib(Uri targetUri) async {
final String fileName = targetUri.pathSegments.last;
final ProcessResult installNameResult = await globals.processManager.run(
<String>[
'install_name_tool',
'-id',
'@executable_path/Frameworks/$fileName',
targetUri.toFilePath(),
],
);
if (installNameResult.exitCode != 0) {
throwToolExit('Failed to change the install name of $targetUri:\n${installNameResult.stderr}');
}
}
Future<void> codesignDylib(
String? codesignIdentity,
BuildMode buildMode,
String targetFullPath,
) async {
if (codesignIdentity == null || codesignIdentity.isEmpty) {
codesignIdentity = '-';
}
final List<String> codesignCommand = <String>[
'codesign',
'--force',
'--sign',
codesignIdentity,
if (buildMode != BuildMode.release) ...<String>[
// Mimic Xcode's timestamp codesigning behavior on non-release binaries.
'--timestamp=none',
],
targetFullPath,
];
globals.logger.printTrace(codesignCommand.join(' '));
final ProcessResult codesignResult = await globals.processManager.run(codesignCommand);
if (codesignResult.exitCode != 0) {
throwToolExit('Failed to code sign binary:\n${codesignResult.stderr}');
}
globals.logger.printTrace(codesignResult.stdout as String);
globals.logger.printTrace(codesignResult.stderr as String);
}
/// Flutter expects `xcrun` to be on the path on macOS hosts.
///
/// Use the `clang`, `ar`, and `ld` that would be used if run with `xcrun`.
Future<CCompilerConfig> cCompilerConfigMacOS() async {
final ProcessResult xcrunResult = await globals.processManager.run(<String>['xcrun', 'clang', '--version']);
if (xcrunResult.exitCode != 0) {
throwToolExit('Failed to find clang with xcrun:\n${xcrunResult.stderr}');
}
final String installPath = LineSplitter.split(xcrunResult.stdout as String)
.firstWhere((String s) => s.startsWith('InstalledDir: '))
.split(' ')
.last;
return CCompilerConfig(
cc: Uri.file('$installPath/clang'),
ar: Uri.file('$installPath/ar'),
ld: Uri.file('$installPath/ld'),
);
}

View File

@ -0,0 +1,378 @@
// 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.
// Logic for native assets shared between all host OSes.
import 'package:logging/logging.dart' as logging;
import 'package:native_assets_builder/native_assets_builder.dart' as native_assets_builder;
import 'package:native_assets_cli/native_assets_cli.dart';
import 'package:package_config/package_config_types.dart';
import 'base/common.dart';
import 'base/file_system.dart';
import 'base/logger.dart';
import 'base/platform.dart';
import 'build_info.dart' as build_info;
import 'cache.dart';
import 'features.dart';
import 'globals.dart' as globals;
import 'ios/native_assets.dart';
import 'macos/native_assets.dart';
import 'macos/native_assets_host.dart';
import 'resident_runner.dart';
/// Programmatic API to be used by Dart launchers to invoke native builds.
///
/// It enables mocking `package:native_assets_builder` package.
/// It also enables mocking native toolchain discovery via [cCompilerConfig].
abstract class NativeAssetsBuildRunner {
/// Whether the project has a `.dart_tools/package_config.json`.
///
/// If there is no package config, [packagesWithNativeAssets], [build], and
/// [dryRun] must not be invoked.
Future<bool> hasPackageConfig();
/// All packages in the transitive dependencies that have a `build.dart`.
Future<List<Package>> packagesWithNativeAssets();
/// Runs all [packagesWithNativeAssets] `build.dart` in dry run.
Future<native_assets_builder.DryRunResult> dryRun({
required bool includeParentEnvironment,
required LinkModePreference linkModePreference,
required OS targetOs,
required Uri workingDirectory,
});
/// Runs all [packagesWithNativeAssets] `build.dart`.
Future<native_assets_builder.BuildResult> build({
required bool includeParentEnvironment,
required BuildMode buildMode,
required LinkModePreference linkModePreference,
required Target target,
required Uri workingDirectory,
CCompilerConfig? cCompilerConfig,
int? targetAndroidNdkApi,
IOSSdk? targetIOSSdk,
});
/// The C compiler config to use for compilation.
Future<CCompilerConfig> get cCompilerConfig;
}
/// Uses `package:native_assets_builder` for its implementation.
class NativeAssetsBuildRunnerImpl implements NativeAssetsBuildRunner {
NativeAssetsBuildRunnerImpl(this.projectUri, this.fileSystem, this.logger);
final Uri projectUri;
final FileSystem fileSystem;
final Logger logger;
late final logging.Logger _logger = logging.Logger('')
..onRecord.listen((logging.LogRecord record) {
final int levelValue = record.level.value;
final String message = record.message;
if (levelValue >= logging.Level.SEVERE.value) {
logger.printError(message);
} else if (levelValue >= logging.Level.WARNING.value) {
logger.printWarning(message);
} else if (levelValue >= logging.Level.INFO.value) {
logger.printTrace(message);
} else {
logger.printTrace(message);
}
});
late final Uri _dartExecutable = fileSystem.directory(Cache.flutterRoot).uri.resolve('bin/dart');
late final native_assets_builder.NativeAssetsBuildRunner _buildRunner = native_assets_builder.NativeAssetsBuildRunner(
logger: _logger,
dartExecutable: _dartExecutable,
);
native_assets_builder.PackageLayout? _packageLayout;
@override
Future<bool> hasPackageConfig() {
final File packageConfigJson = fileSystem
.directory(projectUri.toFilePath())
.childDirectory('.dart_tool')
.childFile('package_config.json');
return packageConfigJson.exists();
}
@override
Future<List<Package>> packagesWithNativeAssets() async {
_packageLayout ??= await native_assets_builder.PackageLayout.fromRootPackageRoot(projectUri);
return _packageLayout!.packagesWithNativeAssets;
}
@override
Future<native_assets_builder.DryRunResult> dryRun({
required bool includeParentEnvironment,
required LinkModePreference linkModePreference,
required OS targetOs,
required Uri workingDirectory,
}) {
return _buildRunner.dryRun(
includeParentEnvironment: includeParentEnvironment,
linkModePreference: linkModePreference,
targetOs: targetOs,
workingDirectory: workingDirectory,
);
}
@override
Future<native_assets_builder.BuildResult> build({
required bool includeParentEnvironment,
required BuildMode buildMode,
required LinkModePreference linkModePreference,
required Target target,
required Uri workingDirectory,
CCompilerConfig? cCompilerConfig,
int? targetAndroidNdkApi,
IOSSdk? targetIOSSdk,
}) {
return _buildRunner.build(
buildMode: buildMode,
cCompilerConfig: cCompilerConfig,
includeParentEnvironment: includeParentEnvironment,
linkModePreference: linkModePreference,
target: target,
targetAndroidNdkApi: targetAndroidNdkApi,
targetIOSSdk: targetIOSSdk,
workingDirectory: workingDirectory,
);
}
@override
late final Future<CCompilerConfig> cCompilerConfig = () {
if (globals.platform.isMacOS || globals.platform.isIOS) {
return cCompilerConfigMacOS();
}
throwToolExit(
'Native assets feature not yet implemented for Linux, Windows and Android.',
);
}();
}
/// Write [assets] to `native_assets.yaml` in [yamlParentDirectory].
Future<Uri> writeNativeAssetsYaml(
Iterable<Asset> assets,
Uri yamlParentDirectory,
FileSystem fileSystem,
) async {
globals.logger.printTrace('Writing native_assets.yaml.');
final String nativeAssetsDartContents = assets.toNativeAssetsFile();
final Directory parentDirectory = fileSystem.directory(yamlParentDirectory);
if (!await parentDirectory.exists()) {
await parentDirectory.create(recursive: true);
}
final File nativeAssetsFile = parentDirectory.childFile('native_assets.yaml');
await nativeAssetsFile.writeAsString(nativeAssetsDartContents);
globals.logger.printTrace('Writing ${nativeAssetsFile.path} done.');
return nativeAssetsFile.uri;
}
/// Select the native asset build mode for a given Flutter build mode.
BuildMode nativeAssetsBuildMode(build_info.BuildMode buildMode) {
switch (buildMode) {
case build_info.BuildMode.debug:
return BuildMode.debug;
case build_info.BuildMode.jitRelease:
case build_info.BuildMode.profile:
case build_info.BuildMode.release:
return BuildMode.release;
}
}
/// Checks whether this project does not yet have a package config file.
///
/// A project has no package config when `pub get` has not yet been run.
///
/// Native asset builds cannot be run without a package config. If there is
/// no package config, leave a logging trace about that.
Future<bool> hasNoPackageConfig(NativeAssetsBuildRunner buildRunner) async {
final bool packageConfigExists = await buildRunner.hasPackageConfig();
if (!packageConfigExists) {
globals.logger.printTrace('No package config found. Skipping native assets compilation.');
}
return !packageConfigExists;
}
/// Checks that if native assets is disabled, none of the dependencies declare
/// native assets.
///
/// If any of the dependencies have native assets, but native assets are
/// disabled, exits the tool.
Future<bool> isDisabledAndNoNativeAssets(NativeAssetsBuildRunner buildRunner) async {
if (featureFlags.isNativeAssetsEnabled) {
return false;
}
final List<Package> packagesWithNativeAssets = await buildRunner.packagesWithNativeAssets();
if (packagesWithNativeAssets.isEmpty) {
return true;
}
final String packageNames = packagesWithNativeAssets.map((Package p) => p.name).join(' ');
throwToolExit(
'Package(s) $packageNames require the native assets feature to be enabled. '
'Enable using `flutter config --enable-native-assets`.',
);
}
/// Ensures that either this project has no native assets, or that native assets
/// are supported on that operating system.
///
/// Exits the tool if the above condition is not satisfied.
Future<void> ensureNoNativeAssetsOrOsIsSupported(
Uri workingDirectory,
String os,
FileSystem fileSystem,
NativeAssetsBuildRunner buildRunner,
) async {
if (await hasNoPackageConfig(buildRunner)) {
return;
}
final List<Package> packagesWithNativeAssets = await buildRunner.packagesWithNativeAssets();
if (packagesWithNativeAssets.isEmpty) {
return;
}
final String packageNames = packagesWithNativeAssets.map((Package p) => p.name).join(' ');
throwToolExit(
'Package(s) $packageNames require the native assets feature. '
'This feature has not yet been implemented for `$os`. '
'For more info see https://github.com/flutter/flutter/issues/129757.',
);
}
/// Ensure all native assets have a linkmode declared to be dynamic loading.
///
/// In JIT, the link mode must always be dynamic linking.
/// In AOT, the static linking has not yet been implemented in Dart:
/// https://github.com/dart-lang/sdk/issues/49418.
///
/// Therefore, ensure all `build.dart` scripts return only dynamic libraries.
void ensureNoLinkModeStatic(List<Asset> nativeAssets) {
final Iterable<Asset> staticAssets = nativeAssets.whereLinkMode(LinkMode.static);
if (staticAssets.isNotEmpty) {
final String assetIds = staticAssets.map((Asset a) => a.id).toSet().join(', ');
throwToolExit(
'Native asset(s) $assetIds have their link mode set to static, '
'but this is not yet supported. '
'For more info see https://github.com/dart-lang/sdk/issues/49418.',
);
}
}
/// This should be the same for different archs, debug/release, etc.
/// It should work for all macOS.
Uri nativeAssetsBuildUri(Uri projectUri, OS os) {
final String buildDir = build_info.getBuildDirectory();
return projectUri.resolve('$buildDir/native_assets/$os/');
}
/// Gets the native asset id to dylib mapping to embed in the kernel file.
///
/// Run hot compiles a kernel file that is pushed to the device after hot
/// restart. We need to embed the native assets mapping in order to access
/// native assets after hot restart.
Future<Uri?> dryRunNativeAssets({
required Uri projectUri,
required FileSystem fileSystem,
required NativeAssetsBuildRunner buildRunner,
required List<FlutterDevice> flutterDevices,
}) async {
if (flutterDevices.length != 1) {
return dryRunNativeAssetsMultipeOSes(
projectUri: projectUri,
fileSystem: fileSystem,
targetPlatforms: flutterDevices.map((FlutterDevice d) => d.targetPlatform).nonNulls,
buildRunner: buildRunner,
);
}
final FlutterDevice flutterDevice = flutterDevices.single;
final build_info.TargetPlatform targetPlatform = flutterDevice.targetPlatform!;
final Uri? nativeAssetsYaml;
switch (targetPlatform) {
case build_info.TargetPlatform.darwin:
nativeAssetsYaml = await dryRunNativeAssetsMacOS(
projectUri: projectUri,
fileSystem: fileSystem,
buildRunner: buildRunner,
);
case build_info.TargetPlatform.ios:
nativeAssetsYaml = await dryRunNativeAssetsIOS(
projectUri: projectUri,
fileSystem: fileSystem,
buildRunner: buildRunner,
);
case build_info.TargetPlatform.tester:
if (const LocalPlatform().isMacOS) {
nativeAssetsYaml = await dryRunNativeAssetsMacOS(
projectUri: projectUri,
flutterTester: true,
fileSystem: fileSystem,
buildRunner: buildRunner,
);
} else {
await ensureNoNativeAssetsOrOsIsSupported(
projectUri,
const LocalPlatform().operatingSystem,
fileSystem,
buildRunner,
);
nativeAssetsYaml = null;
}
case build_info.TargetPlatform.android_arm:
case build_info.TargetPlatform.android_arm64:
case build_info.TargetPlatform.android_x64:
case build_info.TargetPlatform.android_x86:
case build_info.TargetPlatform.android:
case build_info.TargetPlatform.fuchsia_arm64:
case build_info.TargetPlatform.fuchsia_x64:
case build_info.TargetPlatform.linux_arm64:
case build_info.TargetPlatform.linux_x64:
case build_info.TargetPlatform.web_javascript:
case build_info.TargetPlatform.windows_x64:
await ensureNoNativeAssetsOrOsIsSupported(
projectUri,
targetPlatform.toString(),
fileSystem,
buildRunner,
);
nativeAssetsYaml = null;
}
return nativeAssetsYaml;
}
/// Dry run the native builds for multiple OSes.
///
/// Needed for `flutter run -d all`.
Future<Uri?> dryRunNativeAssetsMultipeOSes({
required NativeAssetsBuildRunner buildRunner,
required Uri projectUri,
required FileSystem fileSystem,
required Iterable<build_info.TargetPlatform> targetPlatforms,
}) async {
if (await hasNoPackageConfig(buildRunner) || await isDisabledAndNoNativeAssets(buildRunner)) {
return null;
}
final Uri buildUri_ = buildUriMultiple(projectUri);
final Iterable<Asset> nativeAssetPaths = <Asset>[
if (targetPlatforms.contains(build_info.TargetPlatform.darwin) ||
(targetPlatforms.contains(build_info.TargetPlatform.tester) && OS.current == OS.macOS))
...await dryRunNativeAssetsMacOSInternal(fileSystem, projectUri, false, buildRunner),
if (targetPlatforms.contains(build_info.TargetPlatform.ios)) ...await dryRunNativeAssetsIOSInternal(fileSystem, projectUri, buildRunner)
];
final Uri nativeAssetsUri = await writeNativeAssetsYaml(nativeAssetPaths, buildUri_, fileSystem);
return nativeAssetsUri;
}
/// With `flutter run -d all` we need a place to store the native assets
/// mapping for multiple OSes combined.
Uri buildUriMultiple(Uri projectUri) {
final String buildDir = build_info.getBuildDirectory();
return projectUri.resolve('$buildDir/native_assets/multiple/');
}

View File

@ -21,6 +21,7 @@ import 'dart/package_map.dart';
import 'devfs.dart';
import 'device.dart';
import 'globals.dart' as globals;
import 'native_assets.dart';
import 'project.dart';
import 'reporting/reporting.dart';
import 'resident_runner.dart';
@ -92,9 +93,11 @@ class HotRunner extends ResidentRunner {
StopwatchFactory stopwatchFactory = const StopwatchFactory(),
ReloadSourcesHelper reloadSourcesHelper = defaultReloadSourcesHelper,
ReassembleHelper reassembleHelper = _defaultReassembleHelper,
NativeAssetsBuildRunner? buildRunner,
}) : _stopwatchFactory = stopwatchFactory,
_reloadSourcesHelper = reloadSourcesHelper,
_reassembleHelper = reassembleHelper,
_buildRunner = buildRunner,
super(
hotMode: true,
);
@ -132,6 +135,8 @@ class HotRunner extends ResidentRunner {
String? _sdkName;
bool? _emulator;
NativeAssetsBuildRunner? _buildRunner;
Future<void> _calculateTargetPlatform() async {
if (_targetPlatform != null) {
return;
@ -360,6 +365,15 @@ class HotRunner extends ResidentRunner {
}) async {
await _calculateTargetPlatform();
final Uri projectUri = Uri.directory(projectRootPath);
_buildRunner ??= NativeAssetsBuildRunnerImpl(projectUri, fileSystem, globals.logger);
final Uri? nativeAssetsYaml = await dryRunNativeAssets(
projectUri: projectUri,
fileSystem: fileSystem,
buildRunner: _buildRunner!,
flutterDevices: flutterDevices,
);
final Stopwatch appStartedTimer = Stopwatch()..start();
final File mainFile = globals.fs.file(mainPath);
firstBuildTime = DateTime.now();
@ -391,6 +405,7 @@ class HotRunner extends ResidentRunner {
packageConfig: debuggingOptions.buildInfo.packageConfig,
projectRootPath: FlutterProject.current().directory.absolute.path,
fs: globals.fs,
nativeAssetsYaml: nativeAssetsYaml,
).then((CompilerOutput? output) {
compileTimer.stop();
totalCompileTime += compileTimer.elapsed;

View File

@ -10,11 +10,14 @@ import 'package:meta/meta.dart';
import '../artifacts.dart';
import '../base/file_system.dart';
import '../base/platform.dart';
import '../build_info.dart';
import '../bundle.dart';
import '../compile.dart';
import '../flutter_plugins.dart';
import '../globals.dart' as globals;
import '../macos/native_assets.dart';
import '../native_assets.dart';
import '../project.dart';
import 'test_time_recorder.dart';
@ -163,6 +166,26 @@ class TestCompiler {
invalidatedRegistrantFiles.add(flutterProject!.dartPluginRegistrant.absolute.uri);
}
Uri? nativeAssetsYaml;
final Uri projectUri = FlutterProject.current().directory.uri;
final NativeAssetsBuildRunner buildRunner = NativeAssetsBuildRunnerImpl(projectUri, globals.fs, globals.logger);
if (globals.platform.isMacOS) {
(nativeAssetsYaml, _) = await buildNativeAssetsMacOS(
buildMode: BuildMode.debug,
projectUri: projectUri,
flutterTester: true,
fileSystem: globals.fs,
buildRunner: buildRunner,
);
} else {
await ensureNoNativeAssetsOrOsIsSupported(
projectUri,
const LocalPlatform().operatingSystem,
globals.fs,
buildRunner,
);
}
final CompilerOutput? compilerOutput = await compiler!.recompile(
request.mainUri,
<Uri>[request.mainUri, ...invalidatedRegistrantFiles],
@ -171,6 +194,7 @@ class TestCompiler {
projectRootPath: flutterProject?.directory.absolute.path,
checkDartPluginRegistry: true,
fs: globals.fs,
nativeAssetsYaml: nativeAssetsYaml,
);
final String? outputPath = compilerOutput?.outputFilename;

View File

@ -11,7 +11,6 @@ import '../artifacts.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/os.dart';
import '../build_info.dart';
import '../bundle.dart';
import '../bundle_builder.dart';
@ -50,13 +49,11 @@ class FlutterTesterDevice extends Device {
required Logger logger,
required FileSystem fileSystem,
required Artifacts artifacts,
required OperatingSystemUtils operatingSystemUtils,
}) : _processManager = processManager,
_flutterVersion = flutterVersion,
_logger = logger,
_fileSystem = fileSystem,
_artifacts = artifacts,
_operatingSystemUtils = operatingSystemUtils,
_artifacts = artifacts,
super(
platformType: null,
category: null,
@ -68,7 +65,6 @@ class FlutterTesterDevice extends Device {
final Logger _logger;
final FileSystem _fileSystem;
final Artifacts _artifacts;
final OperatingSystemUtils _operatingSystemUtils;
Process? _process;
final DevicePortForwarder _portForwarder = const NoOpDevicePortForwarder();
@ -157,7 +153,7 @@ class FlutterTesterDevice extends Device {
buildInfo: buildInfo,
mainPath: mainPath,
applicationKernelFilePath: applicationKernelFilePath,
platform: getTargetPlatformForName(getNameForHostPlatform(_operatingSystemUtils.hostPlatform)),
platform: TargetPlatform.tester,
assetDirPath: assetDirectory.path,
);
@ -258,15 +254,13 @@ class FlutterTesterDevices extends PollingDeviceDiscovery {
required ProcessManager processManager,
required Logger logger,
required FlutterVersion flutterVersion,
required OperatingSystemUtils operatingSystemUtils,
}) : _testerDevice = FlutterTesterDevice(
kTesterDeviceId,
fileSystem: fileSystem,
artifacts: artifacts,
processManager: processManager,
logger: logger,
flutterVersion: flutterVersion,
operatingSystemUtils: operatingSystemUtils,
flutterVersion: flutterVersion,
),
super('Flutter tester');

View File

@ -50,6 +50,11 @@ dependencies:
async: 2.11.0
unified_analytics: 3.0.0
cli_config: 0.1.1
graphs: 2.3.1
native_assets_builder: 0.2.0
native_assets_cli: 0.2.0
# We depend on very specific internal implementation details of the
# 'test' package, which change between versions, so when upgrading
# this, make sure the tests are still running correctly.
@ -107,4 +112,4 @@ dartdoc:
# Exclude this package from the hosted API docs.
nodoc: true
# PUBSPEC CHECKSUM: a4aa
# PUBSPEC CHECKSUM: 284b

View File

@ -27,11 +27,11 @@ import 'dart:async';
import 'package:flutter/services.dart';
import 'package:{{pluginProjectName}}/{{pluginProjectName}}.dart';
{{/withPlatformChannelPluginHook}}
{{#withFfiPluginHook}}
{{#withFfi}}
import 'dart:async';
import 'package:{{pluginProjectName}}/{{pluginProjectName}}.dart' as {{pluginProjectName}};
{{/withFfiPluginHook}}
{{/withFfi}}
void main() {
runApp(const MyApp());
@ -213,7 +213,7 @@ class _MyAppState extends State<MyApp> {
}
}
{{/withPlatformChannelPluginHook}}
{{#withFfiPluginHook}}
{{#withFfi}}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@ -279,5 +279,5 @@ class _MyAppState extends State<MyApp> {
);
}
}
{{/withFfiPluginHook}}
{{/withFfi}}
{{/withEmptyMain}}

View File

@ -8,9 +8,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
{{^withFfiPluginHook}}
{{^withFfi}}
import 'package:{{projectName}}/main.dart';
{{/withFfiPluginHook}}
{{/withFfi}}
{{^withPluginHook}}
void main() {

View File

@ -0,0 +1,30 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.packages
build/

View File

@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: {{flutterRevision}}
channel: {{flutterChannel}}
project_type: package_ffi

View File

@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.

View File

@ -0,0 +1 @@
TODO: Add your license here.

View File

@ -0,0 +1,49 @@
# {{projectName}}
{{description}}
## Getting Started
This project is a starting point for a Flutter
[FFI package](https://docs.flutter.dev/development/platform-integration/c-interop),
a specialized package that includes native code directly invoked with Dart FFI.
## Project stucture
This template uses the following structure:
* `src`: Contains the native source code, and a CmakeFile.txt file for building
that source code into a dynamic library.
* `lib`: Contains the Dart code that defines the API of the plugin, and which
calls into the native code using `dart:ffi`.
* `bin`: Contains the `build.dart` that performs the external native builds.
## Buidling and bundling native code
`build.dart` does the building of native components.
Bundling is done by Flutter based on the output from `build.dart`.
## Binding to native code
To use the native code, bindings in Dart are needed.
To avoid writing these by hand, they are generated from the header file
(`src/{{projectName}}.h`) by `package:ffigen`.
Regenerate the bindings by running `flutter pub run ffigen --config ffigen.yaml`.
## Invoking native code
Very short-running native functions can be directly invoked from any isolate.
For example, see `sum` in `lib/{{projectName}}.dart`.
Longer-running functions should be invoked on a helper isolate to avoid
dropping frames in Flutter applications.
For example, see `sumAsync` in `lib/{{projectName}}.dart`.
## Flutter help
For help getting started with Flutter, view our
[online documentation](https://flutter.dev/docs), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

@ -0,0 +1,24 @@
import 'package:native_toolchain_c/native_toolchain_c.dart';
import 'package:logging/logging.dart';
import 'package:native_assets_cli/native_assets_cli.dart';
const packageName = '{{projectName}}';
void main(List<String> args) async {
final buildConfig = await BuildConfig.fromArgs(args);
final buildOutput = BuildOutput();
final cbuilder = CBuilder.library(
name: packageName,
assetId:
'package:$packageName/${packageName}_bindings_generated.dart',
sources: [
'src/$packageName.c',
],
);
await cbuilder.run(
buildConfig: buildConfig,
buildOutput: buildOutput,
logger: Logger('')..onRecord.listen((record) => print(record.message)),
);
await buildOutput.writeToFile(outDir: buildConfig.outDir);
}

View File

@ -0,0 +1,20 @@
# Run with `flutter pub run ffigen --config ffigen.yaml`.
name: {{pluginDartClass}}Bindings
description: |
Bindings for `src/{{projectName}}.h`.
Regenerate bindings with `flutter pub run ffigen --config ffigen.yaml`.
output: 'lib/{{projectName}}_bindings_generated.dart'
headers:
entry-points:
- 'src/{{projectName}}.h'
include-directives:
- 'src/{{projectName}}.h'
ffi-native:
preamble: |
// ignore_for_file: always_specify_types
// ignore_for_file: camel_case_types
// ignore_for_file: non_constant_identifier_names
comments:
style: any
length: full

View File

@ -0,0 +1,108 @@
import 'dart:async';
import 'dart:isolate';
import '{{projectName}}_bindings_generated.dart' as bindings;
/// A very short-lived native function.
///
/// For very short-lived functions, it is fine to call them on the main isolate.
/// They will block the Dart execution while running the native function, so
/// only do this for native functions which are guaranteed to be short-lived.
int sum(int a, int b) => bindings.sum(a, b);
/// A longer lived native function, which occupies the thread calling it.
///
/// Do not call these kind of native functions in the main isolate. They will
/// block Dart execution. This will cause dropped frames in Flutter applications.
/// Instead, call these native functions on a separate isolate.
///
/// Modify this to suit your own use case. Example use cases:
///
/// 1. Reuse a single isolate for various different kinds of requests.
/// 2. Use multiple helper isolates for parallel execution.
Future<int> sumAsync(int a, int b) async {
final SendPort helperIsolateSendPort = await _helperIsolateSendPort;
final int requestId = _nextSumRequestId++;
final _SumRequest request = _SumRequest(requestId, a, b);
final Completer<int> completer = Completer<int>();
_sumRequests[requestId] = completer;
helperIsolateSendPort.send(request);
return completer.future;
}
/// A request to compute `sum`.
///
/// Typically sent from one isolate to another.
class _SumRequest {
final int id;
final int a;
final int b;
const _SumRequest(this.id, this.a, this.b);
}
/// A response with the result of `sum`.
///
/// Typically sent from one isolate to another.
class _SumResponse {
final int id;
final int result;
const _SumResponse(this.id, this.result);
}
/// Counter to identify [_SumRequest]s and [_SumResponse]s.
int _nextSumRequestId = 0;
/// Mapping from [_SumRequest] `id`s to the completers corresponding to the correct future of the pending request.
final Map<int, Completer<int>> _sumRequests = <int, Completer<int>>{};
/// The SendPort belonging to the helper isolate.
Future<SendPort> _helperIsolateSendPort = () async {
// The helper isolate is going to send us back a SendPort, which we want to
// wait for.
final Completer<SendPort> completer = Completer<SendPort>();
// Receive port on the main isolate to receive messages from the helper.
// We receive two types of messages:
// 1. A port to send messages on.
// 2. Responses to requests we sent.
final ReceivePort receivePort = ReceivePort()
..listen((dynamic data) {
if (data is SendPort) {
// The helper isolate sent us the port on which we can sent it requests.
completer.complete(data);
return;
}
if (data is _SumResponse) {
// The helper isolate sent us a response to a request we sent.
final Completer<int> completer = _sumRequests[data.id]!;
_sumRequests.remove(data.id);
completer.complete(data.result);
return;
}
throw UnsupportedError('Unsupported message type: ${data.runtimeType}');
});
// Start the helper isolate.
await Isolate.spawn((SendPort sendPort) async {
final ReceivePort helperReceivePort = ReceivePort()
..listen((dynamic data) {
// On the helper isolate listen to requests and respond to them.
if (data is _SumRequest) {
final int result = bindings.sum_long_running(data.a, data.b);
final _SumResponse response = _SumResponse(data.id, result);
sendPort.send(response);
return;
}
throw UnsupportedError('Unsupported message type: ${data.runtimeType}');
});
// Send the the port to the main isolate on which we can receive requests.
sendPort.send(helperReceivePort.sendPort);
}, receivePort.sendPort);
// Wait until the helper isolate has sent us back the SendPort on which we
// can start sending requests.
return completer.future;
}();

View File

@ -0,0 +1,30 @@
// ignore_for_file: always_specify_types
// ignore_for_file: camel_case_types
// ignore_for_file: non_constant_identifier_names
// AUTO GENERATED FILE, DO NOT EDIT.
//
// Generated by `package:ffigen`.
import 'dart:ffi' as ffi;
/// A very short-lived native function.
///
/// For very short-lived functions, it is fine to call them on the main isolate.
/// They will block the Dart execution while running the native function, so
/// only do this for native functions which are guaranteed to be short-lived.
@ffi.Native<ffi.IntPtr Function(ffi.IntPtr, ffi.IntPtr)>()
external int sum(
int a,
int b,
);
/// A longer lived native function, which occupies the thread calling it.
///
/// Do not call these kind of native functions in the main isolate. They will
/// block Dart execution. This will cause dropped frames in Flutter applications.
/// Instead, call these native functions on a separate isolate.
@ffi.Native<ffi.IntPtr Function(ffi.IntPtr, ffi.IntPtr)>()
external int sum_long_running(
int a,
int b,
);

View File

@ -0,0 +1,19 @@
name: {{projectName}}
description: {{description}}
version: 0.0.1
homepage:
environment:
sdk: {{dartSdkVersionBounds}}
dependencies:
cli_config: ^0.1.1
logging: ^1.1.1
native_assets_cli: ^0.2.0
native_toolchain_c: ^0.2.0
dev_dependencies:
ffi: ^2.0.2
ffigen: ^9.0.0
flutter_lints: ^2.0.0
test: ^1.21.0

View File

@ -0,0 +1,29 @@
#include "{{projectName}}.h"
// A very short-lived native function.
//
// For very short-lived functions, it is fine to call them on the main isolate.
// They will block the Dart execution while running the native function, so
// only do this for native functions which are guaranteed to be short-lived.
FFI_PLUGIN_EXPORT intptr_t sum(intptr_t a, intptr_t b) {
#ifdef DEBUG
return a + b + 1000;
#else
return a + b;
#endif
}
// A longer-lived native function, which occupies the thread calling it.
//
// Do not call these kind of native functions in the main isolate. They will
// block Dart execution. This will cause dropped frames in Flutter applications.
// Instead, call these native functions on a separate isolate.
FFI_PLUGIN_EXPORT intptr_t sum_long_running(intptr_t a, intptr_t b) {
// Simulate work.
#if _WIN32
Sleep(5000);
#else
usleep(5000 * 1000);
#endif
return a + b;
}

View File

@ -0,0 +1,30 @@
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#if _WIN32
#include <windows.h>
#else
#include <pthread.h>
#include <unistd.h>
#endif
#if _WIN32
#define FFI_PLUGIN_EXPORT __declspec(dllexport)
#else
#define FFI_PLUGIN_EXPORT
#endif
// A very short-lived native function.
//
// For very short-lived functions, it is fine to call them on the main isolate.
// They will block the Dart execution while running the native function, so
// only do this for native functions which are guaranteed to be short-lived.
FFI_PLUGIN_EXPORT intptr_t sum(intptr_t a, intptr_t b);
// A longer lived native function, which occupies the thread calling it.
//
// Do not call these kind of native functions in the main isolate. They will
// block Dart execution. This will cause dropped frames in Flutter applications.
// Instead, call these native functions on a separate isolate.
FFI_PLUGIN_EXPORT intptr_t sum_long_running(intptr_t a, intptr_t b);

View File

@ -0,0 +1,14 @@
import 'package:test/test.dart';
import 'package:{{projectName}}/{{projectName}}.dart';
void main() {
test('invoke native function', () {
// Tests are run in debug mode.
expect(sum(24, 18), 1042);
});
test('invoke async native callback', () async {
expect(await sumAsync(24, 18), 42);
});
}

View File

@ -10,6 +10,9 @@ version:
{{#withFfiPluginHook}}
project_type: plugin_ffi
{{/withFfiPluginHook}}
{{#withFfiPackage}}
project_type: package_ffi
{{/withFfiPackage}}
{{#withPlatformChannelPluginHook}}
project_type: plugin
{{/withPlatformChannelPluginHook}}

View File

@ -17,10 +17,10 @@ dependencies:
plugin_platform_interface: ^2.0.2
dev_dependencies:
{{#withFfiPluginHook}}
ffi: ^2.0.1
ffigen: ^6.1.2
{{/withFfiPluginHook}}
{{#withFfi}}
ffi: ^2.0.2
ffigen: ^9.0.0
{{/withFfi}}
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0

View File

@ -248,6 +248,21 @@
"templates/package/README.md.tmpl",
"templates/package/test/projectName_test.dart.tmpl",
"templates/package_ffi/.gitignore.tmpl",
"templates/package_ffi/.metadata.tmpl",
"templates/package_ffi/analysis_options.yaml.tmpl",
"templates/package_ffi/build.dart.tmpl",
"templates/package_ffi/CHANGELOG.md.tmpl",
"templates/package_ffi/ffigen.yaml.tmpl",
"templates/package_ffi/lib/projectName_bindings_generated.dart.tmpl",
"templates/package_ffi/lib/projectName.dart.tmpl",
"templates/package_ffi/LICENSE.tmpl",
"templates/package_ffi/pubspec.yaml.tmpl",
"templates/package_ffi/README.md.tmpl",
"templates/package_ffi/src.tmpl/projectName.c.tmpl",
"templates/package_ffi/src.tmpl/projectName.h.tmpl",
"templates/package_ffi/test/projectName_test.dart.tmpl",
"templates/plugin/android-java.tmpl/build.gradle.tmpl",
"templates/plugin/android-java.tmpl/projectName_android.iml.tmpl",
"templates/plugin/android-java.tmpl/src/main/java/androidIdentifier/pluginClass.java.tmpl",

View File

@ -10,11 +10,14 @@ import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/dart/pub.dart';
import 'package:flutter_tools/src/doctor.dart';
import 'package:flutter_tools/src/doctor_validator.dart';
import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/project.dart';
import 'package:test/fake.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fakes.dart';
import '../../src/test_flutter_command_runner.dart';
import '../../src/testbed.dart';
@ -79,6 +82,7 @@ void main() {
globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'skeleton'),
globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'module', 'common'),
globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'package'),
globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'package_ffi'),
globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'plugin'),
globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'plugin_ffi'),
globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'plugin_shared'),
@ -109,6 +113,7 @@ void main() {
flutterManifest.writeAsStringSync('{"files":[]}');
}, overrides: <Type, Generator>{
DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(),
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true),
});
});
@ -133,6 +138,9 @@ void main() {
await runner.run(<String>['create', '--no-pub', '--template=plugin_ffi', 'testy5']);
expect((await command.usageValues).commandCreateProjectType, 'plugin_ffi');
await runner.run(<String>['create', '--no-pub', '--template=package_ffi', 'testy6']);
expect((await command.usageValues).commandCreateProjectType, 'package_ffi');
}));
testUsingContext('set iOS host language type as usage value', () => testbed.run(() async {
@ -183,6 +191,29 @@ void main() {
}, overrides: <Type, Generator>{
Pub: () => fakePub,
}));
testUsingContext('package_ffi template not enabled', () async {
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
expect(
runner.run(
<String>[
'create',
'--no-pub',
'--template=package_ffi',
'my_ffi_package',
],
),
throwsUsageException(
message: '"package_ffi" is not an allowed value for option "template"',
),
);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(
isNativeAssetsEnabled: false, // ignore: avoid_redundant_argument_values, If we graduate the feature to true by default, don't break this test.
),
});
});
}

View File

@ -856,6 +856,7 @@ class FakeBundleBuilder extends Fake implements BundleBuilder {
String? applicationKernelFilePath,
String? depfilePath,
String? assetDirPath,
Uri? nativeAssets,
@visibleForTesting BuildSystem? buildSystem,
}) async {}
}

View File

@ -2610,6 +2610,18 @@ void main() {
, throwsToolExit(message: 'The "--platforms" argument is not supported', exitCode: 2));
});
testUsingContext('create an ffi package with --platforms throws error.', () async {
Cache.flutterRoot = '../..';
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await expectLater(
runner.run(<String>['create', '--no-pub', '--template=package_ffi', '--platform=ios', projectDir.path])
, throwsToolExit(message: 'The "--platforms" argument is not supported', exitCode: 2));
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true),
});
testUsingContext('create a plugin with android, delete then re-create folders', () async {
Cache.flutterRoot = '../..';
@ -3315,43 +3327,49 @@ void main() {
FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
});
testUsingContext('FFI plugins error android language', () async {
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
final List<String> args = <String>[
'create',
'--no-pub',
'--template=plugin_ffi',
'-a',
'kotlin',
'--platforms=android',
projectDir.path,
];
for (final String template in <String>['package_ffi', 'plugin_ffi']) {
testUsingContext('$template error android language', () async {
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
final List<String> args = <String>[
'create',
'--no-pub',
'--template=$template',
'-a',
'kotlin',
if (template == 'plugin_ffi') '--platforms=android',
projectDir.path,
];
await expectLater(
runner.run(args),
throwsToolExit(message: 'The "android-language" option is not supported with the plugin_ffi template: the language will always be C or C++.'),
);
});
await expectLater(
runner.run(args),
throwsToolExit(message: 'The "android-language" option is not supported with the $template template: the language will always be C or C++.'),
);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true),
});
testUsingContext('FFI plugins error ios language', () async {
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
final List<String> args = <String>[
'create',
'--no-pub',
'--template=plugin_ffi',
'--ios-language',
'swift',
'--platforms=ios',
projectDir.path,
];
testUsingContext('$template error ios language', () async {
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
final List<String> args = <String>[
'create',
'--no-pub',
'--template=$template',
'--ios-language',
'swift',
if (template == 'plugin_ffi') '--platforms=ios',
projectDir.path,
];
await expectLater(
runner.run(args),
throwsToolExit(message: 'The "ios-language" option is not supported with the plugin_ffi template: the language will always be C or C++.'),
);
});
await expectLater(
runner.run(args),
throwsToolExit(message: 'The "ios-language" option is not supported with the $template template: the language will always be C or C++.'),
);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true),
});
}
testUsingContext('FFI plugins error web platform', () async {
final CreateCommand command = CreateCommand();

View File

@ -71,10 +71,21 @@ void main() {
throwsA(isA<MissingDefineException>()));
});
const String emptyNativeAssets = '''
format-version:
- 1
- 0
- 0
native-assets: {}
''';
testWithoutContext('KernelSnapshot handles null result from kernel compilation', () async {
fileSystem.file('.dart_tool/package_config.json')
..createSync(recursive: true)
..writeAsStringSync('{"configVersion": 2, "packages":[]}');
androidEnvironment.buildDir.childFile('native_assets.yaml')
..createSync(recursive: true)
..writeAsStringSync(emptyNativeAssets);
final String build = androidEnvironment.buildDir.path;
final String flutterPatchedSdkPath = artifacts.getArtifactPath(
Artifact.flutterPatchedSdkPath,
@ -102,6 +113,8 @@ void main() {
'$build/app.dill',
'--depfile',
'$build/kernel_snapshot.d',
'--native-assets',
'$build/native_assets.yaml',
'--verbosity=error',
'file:///lib/main.dart',
], exitCode: 1),
@ -115,6 +128,9 @@ void main() {
fileSystem.file('.dart_tool/package_config.json')
..createSync(recursive: true)
..writeAsStringSync('{"configVersion": 2, "packages":[]}');
androidEnvironment.buildDir.childFile('native_assets.yaml')
..createSync(recursive: true)
..writeAsStringSync(emptyNativeAssets);
final String build = androidEnvironment.buildDir.path;
final String flutterPatchedSdkPath = artifacts.getArtifactPath(
Artifact.flutterPatchedSdkPath,
@ -142,6 +158,8 @@ void main() {
'$build/app.dill',
'--depfile',
'$build/kernel_snapshot.d',
'--native-assets',
'$build/native_assets.yaml',
'--verbosity=error',
'file:///lib/main.dart',
], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n'),
@ -156,6 +174,9 @@ void main() {
fileSystem.file('.dart_tool/package_config.json')
..createSync(recursive: true)
..writeAsStringSync('{"configVersion": 2, "packages":[]}');
androidEnvironment.buildDir.childFile('native_assets.yaml')
..createSync(recursive: true)
..writeAsStringSync(emptyNativeAssets);
final String build = androidEnvironment.buildDir.path;
final String flutterPatchedSdkPath = artifacts.getArtifactPath(
Artifact.flutterPatchedSdkPath,
@ -183,6 +204,8 @@ void main() {
'$build/app.dill',
'--depfile',
'$build/kernel_snapshot.d',
'--native-assets',
'$build/native_assets.yaml',
'--verbosity=error',
'file:///lib/main.dart',
], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n'),
@ -198,6 +221,9 @@ void main() {
fileSystem.file('.dart_tool/package_config.json')
..createSync(recursive: true)
..writeAsStringSync('{"configVersion": 2, "packages":[]}');
androidEnvironment.buildDir.childFile('native_assets.yaml')
..createSync(recursive: true)
..writeAsStringSync(emptyNativeAssets);
final String build = androidEnvironment.buildDir.path;
final String flutterPatchedSdkPath = artifacts.getArtifactPath(
Artifact.flutterPatchedSdkPath,
@ -225,6 +251,8 @@ void main() {
'$build/app.dill',
'--depfile',
'$build/kernel_snapshot.d',
'--native-assets',
'$build/native_assets.yaml',
'--verbosity=error',
'foo',
'bar',
@ -242,6 +270,9 @@ void main() {
fileSystem.file('.dart_tool/package_config.json')
..createSync(recursive: true)
..writeAsStringSync('{"configVersion": 2, "packages":[]}');
androidEnvironment.buildDir.childFile('native_assets.yaml')
..createSync(recursive: true)
..writeAsStringSync(emptyNativeAssets);
final String build = androidEnvironment.buildDir.path;
final String flutterPatchedSdkPath = artifacts.getArtifactPath(
Artifact.flutterPatchedSdkPath,
@ -268,6 +299,8 @@ void main() {
'--incremental',
'--initialize-from-dill',
'$build/app.dill',
'--native-assets',
'$build/native_assets.yaml',
'--verbosity=error',
'file:///lib/main.dart',
], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n'),
@ -284,6 +317,9 @@ void main() {
fileSystem.file('.dart_tool/package_config.json')
..createSync(recursive: true)
..writeAsStringSync('{"configVersion": 2, "packages":[]}');
androidEnvironment.buildDir.childFile('native_assets.yaml')
..createSync(recursive: true)
..writeAsStringSync(emptyNativeAssets);
final String build = androidEnvironment.buildDir.path;
final String flutterPatchedSdkPath = artifacts.getArtifactPath(
Artifact.flutterPatchedSdkPath,
@ -309,6 +345,8 @@ void main() {
'--incremental',
'--initialize-from-dill',
'$build/app.dill',
'--native-assets',
'$build/native_assets.yaml',
'--verbosity=error',
'file:///lib/main.dart',
], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n'),
@ -338,6 +376,9 @@ void main() {
fileSystem: fileSystem,
logger: logger,
);
testEnvironment.buildDir.childFile('native_assets.yaml')
..createSync(recursive: true)
..writeAsStringSync(emptyNativeAssets);
final String build = testEnvironment.buildDir.path;
final String flutterPatchedSdkPath = artifacts.getArtifactPath(
Artifact.flutterPatchedSdkPath,
@ -365,6 +406,8 @@ void main() {
'--incremental',
'--initialize-from-dill',
'$build/app.dill',
'--native-assets',
'$build/native_assets.yaml',
'--verbosity=error',
'file:///lib/main.dart',
], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey /build/653e11a8e6908714056a57cd6b4f602a/app.dill 0\n'),

View File

@ -0,0 +1,140 @@
// 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/memory.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/exceptions.dart';
import 'package:flutter_tools/src/build_system/targets/native_assets.dart';
import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/native_assets.dart';
import 'package:native_assets_cli/native_assets_cli.dart' as native_assets_cli;
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 iosEnvironment;
late Artifacts artifacts;
late FileSystem fileSystem;
late Logger logger;
setUp(() {
processManager = FakeProcessManager.empty();
logger = BufferLogger.test();
artifacts = Artifacts.test();
fileSystem = MemoryFileSystem.test();
iosEnvironment = Environment.test(
fileSystem.currentDirectory,
defines: <String, String>{
kBuildMode: BuildMode.profile.cliName,
kTargetPlatform: getNameForTargetPlatform(TargetPlatform.ios),
kIosArchs: 'arm64',
kSdkRoot: 'path/to/iPhoneOS.sdk',
},
inputs: <String, String>{},
artifacts: artifacts,
processManager: processManager,
fileSystem: fileSystem,
logger: logger,
);
iosEnvironment.buildDir.createSync(recursive: true);
});
testWithoutContext('NativeAssets throws error if missing target platform', () async {
iosEnvironment.defines.remove(kTargetPlatform);
expect(const NativeAssets().build(iosEnvironment), throwsA(isA<MissingDefineException>()));
});
testUsingContext('NativeAssets throws error if missing ios archs', () async {
iosEnvironment.defines.remove(kIosArchs);
expect(const NativeAssets().build(iosEnvironment), throwsA(isA<MissingDefineException>()));
});
testUsingContext('NativeAssets throws error if missing sdk root', () async {
iosEnvironment.defines.remove(kSdkRoot);
expect(const NativeAssets().build(iosEnvironment), throwsA(isA<MissingDefineException>()));
});
// The NativeAssets Target should _always_ be creating a yaml an d file.
// The caching logic depends on this.
for (final bool isNativeAssetsEnabled in <bool>[true, false]) {
final String postFix = isNativeAssetsEnabled ? 'enabled' : 'disabled';
testUsingContext(
'Successfull native_assets.yaml and native_assets.d creation with feature $postFix',
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
FeatureFlags: () => TestFeatureFlags(
isNativeAssetsEnabled: isNativeAssetsEnabled,
),
},
() async {
final NativeAssetsBuildRunner buildRunner = FakeNativeAssetsBuildRunner();
await NativeAssets(buildRunner: buildRunner).build(iosEnvironment);
expect(iosEnvironment.buildDir.childFile('native_assets.d'), exists);
expect(iosEnvironment.buildDir.childFile('native_assets.yaml'), exists);
},
);
}
testUsingContext(
'NativeAssets with an asset',
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true),
},
() async {
final NativeAssetsBuildRunner buildRunner = FakeNativeAssetsBuildRunner(
buildResult: FakeNativeAssetsBuilderResult(assets: <native_assets_cli.Asset>[
native_assets_cli.Asset(
id: 'package:foo/foo.dart',
linkMode: native_assets_cli.LinkMode.dynamic,
target: native_assets_cli.Target.iOSArm64,
path: native_assets_cli.AssetAbsolutePath(
Uri.file('libfoo.dylib'),
),
)
], dependencies: <Uri>[
Uri.file('src/foo.c'),
]),
);
await NativeAssets(buildRunner: buildRunner).build(iosEnvironment);
final File nativeAssetsYaml = iosEnvironment.buildDir.childFile('native_assets.yaml');
final File depsFile = iosEnvironment.buildDir.childFile('native_assets.d');
expect(depsFile, exists);
// We don't care about the specific format, but it should contain the
// yaml as the file depending on the source files that went in to the
// build.
expect(
depsFile.readAsStringSync(),
stringContainsInOrder(<String>[
nativeAssetsYaml.path,
':',
'src/foo.c',
]),
);
expect(nativeAssetsYaml, exists);
// We don't care about the specific format, but it should contain the
// asset id and the path to the dylib.
expect(
nativeAssetsYaml.readAsStringSync(),
stringContainsInOrder(<String>[
'package:foo/foo.dart',
'libfoo.dylib',
]),
);
},
);
}

View File

@ -435,4 +435,55 @@ void main() {
completer.complete();
await output;
});
testWithoutContext('KernelCompiler passes native assets', () async {
final BufferLogger logger = BufferLogger.test();
final StdoutHandler stdoutHandler = StdoutHandler(logger: logger, fileSystem: MemoryFileSystem.test());
final Completer<void> completer = Completer<void>();
final KernelCompiler kernelCompiler = KernelCompiler(
artifacts: Artifacts.test(),
fileSystem: MemoryFileSystem.test(),
fileSystemRoots: <String>[],
fileSystemScheme: '',
logger: logger,
processManager: FakeProcessManager.list(<FakeCommand>[
FakeCommand(command: const <String>[
'Artifact.engineDartBinary',
'--disable-dart-dev',
'Artifact.frontendServerSnapshotForEngineDartSdk',
'--sdk-root',
'/path/to/sdkroot/',
'--target=flutter',
'--no-print-incremental-dependencies',
'-Ddart.vm.profile=false',
'-Ddart.vm.product=false',
'--enable-asserts',
'--no-link-platform',
'--packages',
'.packages',
'--native-assets',
'path/to/native_assets.yaml',
'--verbosity=error',
'file:///path/to/main.dart',
], completer: completer),
]),
stdoutHandler: stdoutHandler,
);
final Future<CompilerOutput?> output = kernelCompiler.compile(
sdkRoot: '/path/to/sdkroot',
mainPath: '/path/to/main.dart',
buildMode: BuildMode.debug,
trackWidgetCreation: false,
dartDefines: const <String>[],
packageConfig: PackageConfig.empty,
packagesPath: '.packages',
nativeAssets: 'path/to/native_assets.yaml',
);
stdoutHandler.compilerOutput
?.complete(const CompilerOutput('', 0, <Uri>[]));
completer.complete();
expect((await output)?.outputFilename, '');
});
}

View File

@ -644,6 +644,7 @@ class FakeBundleBuilder extends Fake implements BundleBuilder {
String? applicationKernelFilePath,
String? depfilePath,
String? assetDirPath,
Uri? nativeAssets,
@visibleForTesting BuildSystem? buildSystem
}) async {}
}

View File

@ -702,7 +702,18 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler {
Future<CompilerOutput> Function(Uri mainUri, List<Uri>? invalidatedFiles)? onRecompile;
@override
Future<CompilerOutput> recompile(Uri mainUri, List<Uri>? invalidatedFiles, {String? outputPath, PackageConfig? packageConfig, String? projectRootPath, FileSystem? fs, bool suppressErrors = false, bool checkDartPluginRegistry = false, File? dartPluginRegistrant}) {
Future<CompilerOutput> recompile(
Uri mainUri,
List<Uri>? invalidatedFiles, {
String? outputPath,
PackageConfig? packageConfig,
String? projectRootPath,
FileSystem? fs,
bool suppressErrors = false,
bool checkDartPluginRegistry = false,
File? dartPluginRegistrant,
Uri? nativeAssetsYaml,
}) {
return onRecompile?.call(mainUri, invalidatedFiles)
?? Future<CompilerOutput>.value(const CompilerOutput('', 1, <Uri>[]));
}

View File

@ -0,0 +1,91 @@
// 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_tools/src/native_assets.dart';
import 'package:native_assets_builder/native_assets_builder.dart'
as native_assets_builder;
import 'package:native_assets_cli/native_assets_cli.dart';
import 'package:package_config/package_config_types.dart';
/// Mocks all logic instead of using `package:native_assets_builder`, which
/// relies on doing process calls to `pub` and the local file system.
class FakeNativeAssetsBuildRunner implements NativeAssetsBuildRunner {
FakeNativeAssetsBuildRunner({
this.hasPackageConfigResult = true,
this.packagesWithNativeAssetsResult = const <Package>[],
this.dryRunResult = const FakeNativeAssetsBuilderResult(),
this.buildResult = const FakeNativeAssetsBuilderResult(),
CCompilerConfig? cCompilerConfigResult,
}) : cCompilerConfigResult = cCompilerConfigResult ?? CCompilerConfig();
final native_assets_builder.BuildResult buildResult;
final native_assets_builder.DryRunResult dryRunResult;
final bool hasPackageConfigResult;
final List<Package> packagesWithNativeAssetsResult;
final CCompilerConfig cCompilerConfigResult;
int buildInvocations = 0;
int dryRunInvocations = 0;
int hasPackageConfigInvocations = 0;
int packagesWithNativeAssetsInvocations = 0;
@override
Future<native_assets_builder.BuildResult> build({
required bool includeParentEnvironment,
required BuildMode buildMode,
required LinkModePreference linkModePreference,
required Target target,
required Uri workingDirectory,
CCompilerConfig? cCompilerConfig,
int? targetAndroidNdkApi,
IOSSdk? targetIOSSdk,
}) async {
buildInvocations++;
return buildResult;
}
@override
Future<native_assets_builder.DryRunResult> dryRun({
required bool includeParentEnvironment,
required LinkModePreference linkModePreference,
required OS targetOs,
required Uri workingDirectory,
}) async {
dryRunInvocations++;
return dryRunResult;
}
@override
Future<bool> hasPackageConfig() async {
hasPackageConfigInvocations++;
return hasPackageConfigResult;
}
@override
Future<List<Package>> packagesWithNativeAssets() async {
packagesWithNativeAssetsInvocations++;
return packagesWithNativeAssetsResult;
}
@override
Future<CCompilerConfig> get cCompilerConfig async => cCompilerConfigResult;
}
final class FakeNativeAssetsBuilderResult
implements native_assets_builder.BuildResult {
const FakeNativeAssetsBuilderResult({
this.assets = const <Asset>[],
this.dependencies = const <Uri>[],
this.success = true,
});
@override
final List<Asset> assets;
@override
final List<Uri> dependencies;
@override
final bool success;
}

View File

@ -400,5 +400,13 @@ void main() {
});
}
test('${nativeAssets.name} availability and default enabled', () {
expect(nativeAssets.master.enabledByDefault, false);
expect(nativeAssets.master.available, true);
expect(nativeAssets.beta.enabledByDefault, false);
expect(nativeAssets.beta.available, false);
expect(nativeAssets.stable.enabledByDefault, false);
expect(nativeAssets.stable.available, false);
});
});
}

View File

@ -5,10 +5,13 @@
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/flutter_project_metadata.dart';
import 'package:flutter_tools/src/project.dart';
import '../src/common.dart';
import '../src/context.dart';
import '../src/fakes.dart';
void main() {
late FileSystem fileSystem;
@ -184,4 +187,16 @@ migration:
expect(logger.traceText, contains('The key `create_revision` was not found'));
});
testUsingContext('enabledValues does not contain packageFfi if native-assets not enabled', () {
expect(FlutterProjectType.enabledValues, isNot(contains(FlutterProjectType.packageFfi)));
expect(FlutterProjectType.enabledValues, contains(FlutterProjectType.plugin));
});
testUsingContext('enabledValues contains packageFfi if natives-assets enabled', () {
expect(FlutterProjectType.enabledValues, contains(FlutterProjectType.packageFfi));
expect(FlutterProjectType.enabledValues, contains(FlutterProjectType.plugin));
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true),
});
}

View File

@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:file/memory.dart';
import 'package:flutter_tools/src/application_package.dart';
import 'package:flutter_tools/src/artifacts.dart';
@ -16,12 +14,15 @@ import 'package:flutter_tools/src/build_system/targets/shader_compiler.dart';
import 'package:flutter_tools/src/compile.dart';
import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/reporting/reporting.dart';
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' as native_assets_cli;
import 'package:package_config/package_config.dart';
import 'package:test/fake.dart';
import 'package:vm_service/vm_service.dart' as vm_service;
@ -29,6 +30,7 @@ import 'package:vm_service/vm_service.dart' as vm_service;
import '../src/common.dart';
import '../src/context.dart';
import '../src/fakes.dart';
import 'fake_native_assets_build_runner.dart';
void main() {
group('validateReloadReport', () {
@ -548,6 +550,134 @@ void main() {
expect(flutterDevice2.stoppedEchoingDeviceLog, true);
});
});
group('native assets', () {
late TestHotRunnerConfig testingConfig;
late FileSystem fileSystem;
setUp(() {
fileSystem = MemoryFileSystem.test();
testingConfig = TestHotRunnerConfig(
successfulHotRestartSetup: true,
);
});
testUsingContext('native assets restart', () 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.devFS! as FakeDevFs).baseUri = Uri.parse('file:///base_uri');
final FakeNativeAssetsBuildRunner buildRunner = FakeNativeAssetsBuildRunner(
packagesWithNativeAssetsResult: <Package>[
Package('bar', fileSystem.currentDirectory.uri),
],
dryRunResult: FakeNativeAssetsBuilderResult(
assets: <Asset>[
Asset(
id: 'package:bar/bar.dart',
linkMode: LinkMode.dynamic,
target: native_assets_cli.Target.macOSArm64,
path: AssetAbsolutePath(Uri.file('bar.dylib')),
),
],
),
);
final HotRunner hotRunner = HotRunner(
devices,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug),
target: 'main.dart',
devtoolsHandler: createNoOpHandler,
buildRunner: buildRunner,
);
final OperationResult result = await hotRunner.restart(fullRestart: true);
expect(result.isOk, true);
// Hot restart does not require reruning anything for native assets.
// The previous native assets mapping should be used.
expect(buildRunner.buildInvocations, 0);
expect(buildRunner.dryRunInvocations, 0);
expect(buildRunner.hasPackageConfigInvocations, 0);
expect(buildRunner.packagesWithNativeAssetsInvocations, 0);
}, overrides: <Type, Generator>{
HotRunnerConfig: () => testingConfig,
Artifacts: () => Artifacts.test(),
FileSystem: () => fileSystem,
Platform: () => FakePlatform(),
ProcessManager: () => FakeProcessManager.empty(),
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true, isMacOSEnabled: true),
});
testUsingContext('native assets run unsupported', () async {
final FakeDevice device = FakeDevice(targetPlatform: TargetPlatform.android_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),
);
(fakeFlutterDevice.devFS! as FakeDevFs).baseUri = Uri.parse('file:///base_uri');
final FakeNativeAssetsBuildRunner buildRunner = FakeNativeAssetsBuildRunner(
packagesWithNativeAssetsResult: <Package>[
Package('bar', fileSystem.currentDirectory.uri),
],
dryRunResult: FakeNativeAssetsBuilderResult(
assets: <Asset>[
Asset(
id: 'package:bar/bar.dart',
linkMode: LinkMode.dynamic,
target: native_assets_cli.Target.macOSArm64,
path: AssetAbsolutePath(Uri.file('bar.dylib')),
),
],
),
);
final HotRunner hotRunner = HotRunner(
devices,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug),
target: 'main.dart',
devtoolsHandler: createNoOpHandler,
buildRunner: buildRunner,
);
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.',
)
);
}, overrides: <Type, Generator>{
HotRunnerConfig: () => testingConfig,
Artifacts: () => Artifacts.test(),
FileSystem: () => fileSystem,
Platform: () => FakePlatform(),
ProcessManager: () => FakeProcessManager.empty(),
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true, isMacOSEnabled: true),
});
});
}
class FakeDevFs extends Fake implements DevFS {
@ -580,6 +710,12 @@ class FakeDevFs extends Fake implements DevFS {
// Until we fix that, we have to also ignore related lints here.
// ignore: avoid_implementing_value_types
class FakeDevice extends Fake implements Device {
FakeDevice({
TargetPlatform targetPlatform = TargetPlatform.tester,
}) : _targetPlatform = targetPlatform;
final TargetPlatform _targetPlatform;
bool disposed = false;
@override
@ -595,7 +731,7 @@ class FakeDevice extends Fake implements Device {
bool supportsFlutterExit = true;
@override
Future<TargetPlatform> get targetPlatform async => TargetPlatform.tester;
Future<TargetPlatform> get targetPlatform async => _targetPlatform;
@override
Future<String> get sdkNameAndVersion async => 'Tester';
@ -658,6 +794,9 @@ class FakeFlutterDevice extends Fake implements FlutterDevice {
required List<Uri> invalidatedFiles,
required PackageConfig packageConfig,
}) => updateDevFSReportCallback();
@override
TargetPlatform? get targetPlatform => device._targetPlatform;
}
class TestFlutterDevice extends FlutterDevice {

View File

@ -0,0 +1,274 @@
// 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/artifacts.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:flutter_tools/src/ios/native_assets.dart';
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_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 dryRunNativeAssetsIOS(
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 buildNativeAssetsIOS(
darwinArchs: <DarwinArch>[DarwinArch.arm64],
environmentType: EnvironmentType.simulator,
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(
() => dryRunNativeAssetsIOS(
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 dryRunNativeAssetsIOS(
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('bar.dylib')),
),
Asset(
id: 'package:bar/bar.dart',
linkMode: LinkMode.dynamic,
target: native_assets_cli.Target.macOSX64,
path: AssetAbsolutePath(Uri.file('bar.dylib')),
),
],
),
),
);
expect(
nativeAssetsYaml,
projectUri.resolve('build/native_assets/ios/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(
() => buildNativeAssetsIOS(
darwinArchs: <DarwinArch>[DarwinArch.arm64],
environmentType: EnvironmentType.simulator,
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 buildNativeAssetsIOS(
darwinArchs: <DarwinArch>[DarwinArch.arm64],
environmentType: EnvironmentType.simulator,
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', overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true),
ProcessManager: () => FakeProcessManager.list(
<FakeCommand>[
const FakeCommand(
command: <Pattern>[
'lipo',
'-create',
'-output',
'/build/native_assets/ios/bar.dylib',
'bar.dylib',
],
),
const FakeCommand(
command: <Pattern>[
'install_name_tool',
'-id',
'@executable_path/Frameworks/bar.dylib',
'/build/native_assets/ios/bar.dylib',
],
),
const FakeCommand(
command: <Pattern>[
'codesign',
'--force',
'--sign',
'-',
'--timestamp=none',
'/build/native_assets/ios/bar.dylib',
],
),
],
),
}, () async {
if (const LocalPlatform().isWindows) {
return; // Backslashes in commands, but we will never run these commands on Windows.
}
final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json');
await packageConfig.parent.create();
await packageConfig.create();
await buildNativeAssetsIOS(
darwinArchs: <DarwinArch>[DarwinArch.arm64],
environmentType: EnvironmentType.simulator,
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.macOSArm64,
path: AssetAbsolutePath(Uri.file('bar.dylib')),
),
],
),
),
);
expect(
environment.buildDir.childFile('native_assets.yaml'),
exists,
);
});
}

View File

@ -0,0 +1,385 @@
// 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:flutter_tools/src/artifacts.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:flutter_tools/src/macos/native_assets.dart';
import 'package:flutter_tools/src/native_assets.dart';
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_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 dryRunNativeAssetsMacOS(
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 buildNativeAssetsMacOS(
darwinArchs: <DarwinArch>[DarwinArch.arm64],
projectUri: projectUri,
buildMode: BuildMode.debug,
fileSystem: fileSystem,
buildRunner: FakeNativeAssetsBuildRunner(
hasPackageConfigResult: false,
),
);
expect(
(globals.logger as BufferLogger).traceText,
contains('No package config found. Skipping native assets compilation.'),
);
});
testUsingContext('dry run for multiple OSes with no package config', overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.empty(),
}, () async {
await dryRunNativeAssetsMultipeOSes(
projectUri: projectUri,
fileSystem: fileSystem,
targetPlatforms: <TargetPlatform>[
TargetPlatform.darwin,
TargetPlatform.ios,
],
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(
() => dryRunNativeAssetsMacOS(
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 dryRunNativeAssetsMacOS(
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('bar.dylib')),
),
Asset(
id: 'package:bar/bar.dart',
linkMode: LinkMode.dynamic,
target: native_assets_cli.Target.macOSX64,
path: AssetAbsolutePath(Uri.file('bar.dylib')),
),
],
),
),
);
expect(
nativeAssetsYaml,
projectUri.resolve('build/native_assets/macos/native_assets.yaml'),
);
expect(
await fileSystem.file(nativeAssetsYaml).readAsString(),
contains('package:bar/bar.dart'),
);
});
testUsingContext('build 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(
() => buildNativeAssetsMacOS(
darwinArchs: <DarwinArch>[DarwinArch.arm64],
projectUri: projectUri,
buildMode: BuildMode.debug,
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('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();
final (Uri? nativeAssetsYaml, _) = await buildNativeAssetsMacOS(
darwinArchs: <DarwinArch>[DarwinArch.arm64],
projectUri: projectUri,
buildMode: BuildMode.debug,
fileSystem: fileSystem,
buildRunner: FakeNativeAssetsBuildRunner(
packagesWithNativeAssetsResult: <Package>[
Package('bar', projectUri),
],
),
);
expect(
nativeAssetsYaml,
projectUri.resolve('build/native_assets/macos/native_assets.yaml'),
);
expect(
await fileSystem.file(nativeAssetsYaml).readAsString(),
isNot(contains('package:bar/bar.dart')),
);
});
for (final bool flutterTester in <bool>[false, true]) {
String testName = '';
if (flutterTester) {
testName += ' flutter tester';
}
testUsingContext('build with assets$testName', overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true),
ProcessManager: () => FakeProcessManager.list(
<FakeCommand>[
const FakeCommand(
command: <Pattern>[
'lipo',
'-create',
'-output',
'/build/native_assets/macos/bar.dylib',
'bar.dylib',
],
),
const FakeCommand(
command: <Pattern>[
'install_name_tool',
'-id',
'@executable_path/Frameworks/bar.dylib',
'/build/native_assets/macos/bar.dylib',
],
),
const FakeCommand(
command: <Pattern>[
'codesign',
'--force',
'--sign',
'-',
'--timestamp=none',
'/build/native_assets/macos/bar.dylib',
],
),
],
),
}, () async {
if (const LocalPlatform().isWindows) {
return; // Backslashes in commands, but we will never run these commands on Windows.
}
final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json');
await packageConfig.parent.create();
await packageConfig.create();
final (Uri? nativeAssetsYaml, _) = await buildNativeAssetsMacOS(
darwinArchs: <DarwinArch>[DarwinArch.arm64],
projectUri: projectUri,
buildMode: BuildMode.debug,
fileSystem: fileSystem,
flutterTester: flutterTester,
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.macOSArm64,
path: AssetAbsolutePath(Uri.file('bar.dylib')),
),
],
),
),
);
expect(
nativeAssetsYaml,
projectUri.resolve('build/native_assets/macos/native_assets.yaml'),
);
expect(
await fileSystem.file(nativeAssetsYaml).readAsString(),
stringContainsInOrder(<String>[
'package:bar/bar.dart',
if (flutterTester)
// Tests run on host system, so the have the full path on the system.
'- ${projectUri.resolve('/build/native_assets/macos/bar.dylib').toFilePath()}'
else
// Apps are a bundle with the dylibs on their dlopen path.
'- bar.dylib',
]),
);
});
}
testUsingContext('static libs not supported', 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(
() => dryRunNativeAssetsMacOS(
projectUri: projectUri,
fileSystem: fileSystem,
buildRunner: FakeNativeAssetsBuildRunner(
packagesWithNativeAssetsResult: <Package>[
Package('bar', projectUri),
],
dryRunResult: FakeNativeAssetsBuilderResult(
assets: <Asset>[
Asset(
id: 'package:bar/bar.dart',
linkMode: LinkMode.static,
target: native_assets_cli.Target.macOSArm64,
path: AssetAbsolutePath(Uri.file('bar.a')),
),
Asset(
id: 'package:bar/bar.dart',
linkMode: LinkMode.static,
target: native_assets_cli.Target.macOSX64,
path: AssetAbsolutePath(Uri.file('bar.a')),
),
],
),
),
),
throwsToolExit(
message: 'Native asset(s) package:bar/bar.dart have their link mode set to '
'static, but this is not yet supported. '
'For more info see https://github.com/dart-lang/sdk/issues/49418.',
),
);
});
// This logic is mocked in the other tests to avoid having test order
// randomization causing issues with what processes are invoked.
// Exercise the parsing of the process output in this separate test.
testUsingContext('NativeAssetsBuildRunnerImpl.cCompilerConfig', overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true),
ProcessManager: () => FakeProcessManager.list(
<FakeCommand>[
const FakeCommand(
command: <Pattern>['xcrun', 'clang', '--version'],
stdout: '''
Apple clang version 14.0.0 (clang-1400.0.29.202)
Target: arm64-apple-darwin22.6.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin''',
)
],
),
}, () async {
if (!const LocalPlatform().isMacOS) {
// TODO(dacoharkes): Implement other OSes. https://github.com/flutter/flutter/issues/129757
return;
}
final NativeAssetsBuildRunner runner = NativeAssetsBuildRunnerImpl(projectUri, fileSystem, logger);
final CCompilerConfig result = await runner.cCompilerConfig;
expect(
result.cc,
Uri.file(
'/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang',
),
);
});
}

View File

@ -99,6 +99,7 @@ class FakeBundleBuilder extends Fake implements BundleBuilder {
String? applicationKernelFilePath,
String? depfilePath,
String? assetDirPath,
Uri? nativeAssets,
@visibleForTesting BuildSystem? buildSystem
}) async {
final Directory assetDirectory = fileSystem

View File

@ -18,6 +18,7 @@ import 'package:flutter_tools/src/base/io.dart' as io;
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/build_system/targets/scene_importer.dart';
import 'package:flutter_tools/src/build_system/targets/shader_compiler.dart';
import 'package:flutter_tools/src/compile.dart';
@ -25,6 +26,7 @@ import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/device_port_forwarder.dart';
import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/reporting/reporting.dart';
@ -34,6 +36,9 @@ import 'package:flutter_tools/src/run_cold.dart';
import 'package:flutter_tools/src/run_hot.dart';
import 'package:flutter_tools/src/version.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' as native_assets_cli;
import 'package:package_config/package_config.dart';
import 'package:test/fake.dart';
import 'package:vm_service/vm_service.dart' as vm_service;
@ -43,6 +48,7 @@ import '../src/context.dart';
import '../src/fake_vm_services.dart';
import '../src/fakes.dart';
import '../src/testbed.dart';
import 'fake_native_assets_build_runner.dart';
final vm_service.Event fakeUnpausedEvent = vm_service.Event(
kind: vm_service.EventKind.kResume,
@ -2322,6 +2328,82 @@ flutter:
expect(flutterDevice.devFS!.hasSetAssetDirectory, true);
expect(fakeVmServiceHost!.hasRemainingExpectations, false);
}));
testUsingContext(
'native assets',
() => testbed.run(() async {
final FileSystem fileSystem = globals.fs;
final Environment environment = Environment.test(
fileSystem.currentDirectory,
inputs: <String, String>{},
artifacts: Artifacts.test(),
processManager: FakeProcessManager.empty(),
fileSystem: fileSystem,
logger: BufferLogger.test(),
);
final Uri projectUri = environment.projectDir.uri;
final FakeDevice device = FakeDevice(
targetPlatform: TargetPlatform.darwin,
sdkNameAndVersion: 'Macos',
);
final FakeFlutterDevice flutterDevice = FakeFlutterDevice()
..testUri = testUri
..vmServiceHost = (() => fakeVmServiceHost)
..device = device
.._devFS = devFS
..targetPlatform = TargetPlatform.darwin;
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
listViews,
listViews,
]);
globals.fs
.file(globals.fs.path.join('lib', 'main.dart'))
.createSync(recursive: true);
final FakeNativeAssetsBuildRunner 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('bar.dylib')),
),
],
),
);
residentRunner = HotRunner(
<FlutterDevice>[
flutterDevice,
],
stayResident: false,
debuggingOptions: DebuggingOptions.enabled(const BuildInfo(
BuildMode.debug,
'',
treeShakeIcons: false,
trackWidgetCreation: true,
)),
target: 'main.dart',
devtoolsHandler: createNoOpHandler,
buildRunner: buildRunner,
);
final int? result = await residentRunner.run();
expect(result, 0);
expect(buildRunner.buildInvocations, 0);
expect(buildRunner.dryRunInvocations, 1);
expect(buildRunner.hasPackageConfigInvocations, 1);
expect(buildRunner.packagesWithNativeAssetsInvocations, 0);
}),
overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.any(),
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true, isMacOSEnabled: true),
});
}
// This implements [dds.DartDevelopmentService], not the [DartDevelopmentService]
@ -2386,7 +2468,7 @@ class FakeFlutterDevice extends Fake implements FlutterDevice {
DevelopmentShaderCompiler get developmentShaderCompiler => const FakeShaderCompiler();
@override
TargetPlatform get targetPlatform => TargetPlatform.android;
TargetPlatform targetPlatform = TargetPlatform.android;
@override
Stream<Uri?> get vmServiceUris => Stream<Uri?>.value(testUri);
@ -2521,6 +2603,7 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler {
bool suppressErrors = false,
bool checkDartPluginRegistry = false,
File? dartPluginRegistrant,
Uri? nativeAssetsYaml,
}) async {
didSuppressErrors = suppressErrors;
return nextOutput ?? const CompilerOutput('foo.dill', 0, <Uri>[]);

View File

@ -1444,6 +1444,7 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler {
bool suppressErrors = false,
bool checkDartPluginRegistry = false,
File? dartPluginRegistrant,
Uri? nativeAssetsYaml,
}) async {
return const CompilerOutput('foo.dill', 0, <Uri>[]);
}

View File

@ -234,6 +234,7 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler {
bool suppressErrors = false,
bool checkDartPluginRegistry = false,
File? dartPluginRegistrant,
Uri? nativeAssetsYaml,
}) async {
if (compilerOutput != null) {
fileSystem!.file(compilerOutput!.outputFilename).createSync(recursive: true);

View File

@ -98,7 +98,6 @@ void main() {
artifacts: Artifacts.test(),
logger: BufferLogger.test(),
flutterVersion: FakeFlutterVersion(),
operatingSystemUtils: FakeOperatingSystemUtils(),
);
logLines = <String>[];
device.getLogReader().logLines.listen(logLines.add);
@ -213,7 +212,6 @@ FlutterTesterDevices setUpFlutterTesterDevices() {
processManager: FakeProcessManager.any(),
fileSystem: MemoryFileSystem.test(),
flutterVersion: FakeFlutterVersion(),
operatingSystemUtils: FakeOperatingSystemUtils(),
);
}

View File

@ -1190,6 +1190,7 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler {
bool suppressErrors = false,
bool checkDartPluginRegistry = false,
File? dartPluginRegistrant,
Uri? nativeAssetsYaml,
}) async {
return output;
}

View File

@ -254,6 +254,7 @@ void main() {
'VERBOSE_SCRIPT_LOGGING': '1',
'FLUTTER_BUILD_MODE': 'release',
'ACTION': 'install',
'FLUTTER_BUILD_DIR': 'build',
// Skip bitcode stripping since we just checked that above.
},
);

View File

@ -0,0 +1,360 @@
// 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.
// This test exercises the embedding of the native assets mapping in dill files.
// An initial dill file is created by `flutter assemble` and used for running
// the application. This dill must contain the mapping.
// When doing hot reload, this mapping must stay in place.
// When doing a hot restart, a new dill file is pushed. This dill file must also
// contain the native assets mapping.
// When doing a hot reload, this mapping must stay in place.
@Timeout(Duration(minutes: 10))
library;
import 'dart:io';
import 'package:file/file.dart';
import 'package:file_testing/file_testing.dart';
import '../src/common.dart';
import 'test_utils.dart' show fileSystem, platform;
import 'transition_test_utils.dart';
final String hostOs = platform.operatingSystem;
final List<String> devices = <String>[
'flutter-tester',
hostOs,
];
final List<String> buildSubcommands = <String>[
hostOs,
if (hostOs == 'macos') 'ios',
];
final List<String> add2appBuildSubcommands = <String>[
if (hostOs == 'macos') ...<String>[
'macos-framework',
'ios-framework',
],
];
/// The build modes to target for each flutter command that supports passing
/// a build mode.
///
/// The flow of compiling kernel as well as bundling dylibs can differ based on
/// build mode, so we should cover this.
const List<String> buildModes = <String>[
'debug',
'profile',
'release',
];
const String packageName = 'package_with_native_assets';
const String exampleAppName = '${packageName}_example';
const String dylibName = 'lib$packageName.dylib';
void main() {
if (!platform.isMacOS) {
// TODO(dacoharkes): Implement other OSes. https://github.com/flutter/flutter/issues/129757
return;
}
setUpAll(() {
processManager.runSync(<String>[
flutterBin,
'config',
'--enable-native-assets',
]);
});
for (final String device in devices) {
for (final String buildMode in buildModes) {
if (device == 'flutter-tester' && buildMode != 'debug') {
continue;
}
final String hotReload = buildMode == 'debug' ? ' hot reload and hot restart' : '';
testWithoutContext('flutter run$hotReload with native assets $device $buildMode', () async {
await inTempDir((Directory tempDirectory) async {
final Directory packageDirectory = await createTestProject(packageName, tempDirectory);
final Directory exampleDirectory = packageDirectory.childDirectory('example');
final ProcessTestResult result = await runFlutter(
<String>[
'run',
'-d$device',
'--$buildMode',
],
exampleDirectory.path,
<Transition>[
Multiple(<Pattern>[
'Flutter run key commands.',
], handler: (String line) {
if (buildMode == 'debug') {
// Do a hot reload diff on the initial dill file.
return 'r';
} else {
// No hot reload and hot restart in release mode.
return 'q';
}
}),
if (buildMode == 'debug') ...<Transition>[
Barrier(
'Performing hot reload...'.padRight(progressMessageWidth),
logging: true,
),
Multiple(<Pattern>[
RegExp('Reloaded .*'),
], handler: (String line) {
// Do a hot restart, pushing a new complete dill file.
return 'R';
}),
Barrier('Performing hot restart...'.padRight(progressMessageWidth)),
Multiple(<Pattern>[
RegExp('Restarted application .*'),
], handler: (String line) {
// Do another hot reload, pushing a diff to the second dill file.
return 'r';
}),
Barrier(
'Performing hot reload...'.padRight(progressMessageWidth),
logging: true,
),
Multiple(<Pattern>[
RegExp('Reloaded .*'),
], handler: (String line) {
return 'q';
}),
],
const Barrier('Application finished.'),
],
logging: false,
);
if (result.exitCode != 0) {
throw Exception('flutter run failed: ${result.exitCode}\n${result.stderr}\n${result.stdout}');
}
final String stdout = result.stdout.join('\n');
// Check that we did not fail to resolve the native function in the
// dynamic library.
expect(stdout, isNot(contains("Invalid argument(s): Couldn't resolve native function 'sum'")));
// And also check that we did not have any other exceptions that might
// shadow the exception we would have gotten.
expect(stdout, isNot(contains('EXCEPTION CAUGHT BY WIDGETS LIBRARY')));
if (device == 'macos') {
expectDylibIsBundledMacOS(exampleDirectory, buildMode);
}
if (device == hostOs) {
expectCCompilerIsConfigured(exampleDirectory);
}
});
});
}
}
testWithoutContext('flutter test with native assets', () async {
await inTempDir((Directory tempDirectory) async {
final Directory packageDirectory = await createTestProject(packageName, tempDirectory);
final ProcessTestResult result = await runFlutter(
<String>[
'test',
],
packageDirectory.path,
<Transition>[
Barrier(RegExp('.* All tests passed!')),
],
logging: false,
);
if (result.exitCode != 0) {
throw Exception('flutter test failed: ${result.exitCode}\n${result.stderr}\n${result.stdout}');
}
});
});
for (final String buildSubcommand in buildSubcommands) {
for (final String buildMode in buildModes) {
testWithoutContext('flutter build $buildSubcommand with native assets $buildMode', () async {
await inTempDir((Directory tempDirectory) async {
final Directory packageDirectory = await createTestProject(packageName, tempDirectory);
final Directory exampleDirectory = packageDirectory.childDirectory('example');
final ProcessResult result = processManager.runSync(
<String>[
flutterBin,
'build',
buildSubcommand,
'--$buildMode',
if (buildSubcommand == 'ios') '--no-codesign',
],
workingDirectory: exampleDirectory.path,
);
if (result.exitCode != 0) {
throw Exception('flutter build failed: ${result.exitCode}\n${result.stderr}\n${result.stdout}');
}
if (buildSubcommand == 'macos') {
expectDylibIsBundledMacOS(exampleDirectory, buildMode);
} else if (buildSubcommand == 'ios') {
expectDylibIsBundledIos(exampleDirectory, buildMode);
}
expectCCompilerIsConfigured(exampleDirectory);
});
});
}
// This could be an hermetic unit test if the native_assets_builder
// could mock process runs and file system.
// https://github.com/dart-lang/native/issues/90.
testWithoutContext('flutter build $buildSubcommand error on static libraries', () async {
await inTempDir((Directory tempDirectory) async {
final Directory packageDirectory = await createTestProject(packageName, tempDirectory);
final File buildDotDart = packageDirectory.childFile('build.dart');
final String buildDotDartContents = await buildDotDart.readAsString();
// Overrides the build to output static libraries.
final String buildDotDartContentsNew = buildDotDartContents.replaceFirst(
'final buildConfig = await BuildConfig.fromArgs(args);',
r'''
final buildConfig = await BuildConfig.fromArgs([
'-D${LinkModePreference.configKey}=${LinkModePreference.static}',
...args,
]);
''',
);
expect(buildDotDartContentsNew, isNot(buildDotDartContents));
await buildDotDart.writeAsString(buildDotDartContentsNew);
final Directory exampleDirectory = packageDirectory.childDirectory('example');
final ProcessResult result = processManager.runSync(
<String>[
flutterBin,
'build',
buildSubcommand,
if (buildSubcommand == 'ios') '--no-codesign',
],
workingDirectory: exampleDirectory.path,
);
expect(result.exitCode, isNot(0));
expect(result.stderr, contains('link mode set to static, but this is not yet supported'));
});
});
}
for (final String add2appBuildSubcommand in add2appBuildSubcommands) {
testWithoutContext('flutter build $add2appBuildSubcommand with native assets', () async {
await inTempDir((Directory tempDirectory) async {
final Directory packageDirectory = await createTestProject(packageName, tempDirectory);
final Directory exampleDirectory = packageDirectory.childDirectory('example');
final ProcessResult result = processManager.runSync(
<String>[
flutterBin,
'build',
add2appBuildSubcommand,
],
workingDirectory: exampleDirectory.path,
);
if (result.exitCode != 0) {
throw Exception('flutter build failed: ${result.exitCode}\n${result.stderr}\n${result.stdout}');
}
for (final String buildMode in buildModes) {
expectDylibIsBundledWithFrameworks(exampleDirectory, buildMode, add2appBuildSubcommand.replaceAll('-framework', ''));
}
expectCCompilerIsConfigured(exampleDirectory);
});
});
}
}
/// For `flutter build` we can't easily test whether running the app works.
/// Check that we have the dylibs in the app.
void expectDylibIsBundledMacOS(Directory appDirectory, String buildMode) {
final Directory appBundle = appDirectory.childDirectory('build/$hostOs/Build/Products/${buildMode.upperCaseFirst()}/$exampleAppName.app');
expect(appBundle, exists);
final Directory dylibsFolder = appBundle.childDirectory('Contents/Frameworks');
expect(dylibsFolder, exists);
final File dylib = dylibsFolder.childFile(dylibName);
expect(dylib, exists);
}
void expectDylibIsBundledIos(Directory appDirectory, String buildMode) {
final Directory appBundle = appDirectory.childDirectory('build/ios/${buildMode.upperCaseFirst()}-iphoneos/Runner.app');
expect(appBundle, exists);
final Directory dylibsFolder = appBundle.childDirectory('Frameworks');
expect(dylibsFolder, exists);
final File dylib = dylibsFolder.childFile(dylibName);
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) {
final Directory frameworksFolder = appDirectory.childDirectory('build/$os/framework/${buildMode.upperCaseFirst()}');
expect(frameworksFolder, exists);
final File dylib = frameworksFolder.childFile(dylibName);
expect(dylib, exists);
}
/// Check that the native assets are built with the C Compiler that Flutter uses.
///
/// This inspects the build configuration to see if the C compiler was configured.
void expectCCompilerIsConfigured(Directory appDirectory) {
final Directory nativeAssetsBuilderDir = appDirectory.childDirectory('.dart_tool/native_assets_builder/');
for (final Directory subDir in nativeAssetsBuilderDir.listSync().whereType<Directory>()) {
final File config = subDir.childFile('config.yaml');
expect(config, exists);
final String contents = config.readAsStringSync();
// Dry run does not pass compiler info.
if (contents.contains('dry_run: true')) {
continue;
}
expect(contents, contains('cc: '));
}
}
extension on String {
String upperCaseFirst() {
return replaceFirst(this[0], this[0].toUpperCase());
}
}
Future<Directory> createTestProject(String packageName, Directory tempDirectory) async {
final ProcessResult result = processManager.runSync(
<String>[
flutterBin,
'create',
'--template=package_ffi',
packageName,
],
workingDirectory: tempDirectory.path,
);
if (result.exitCode != 0) {
throw Exception('flutter create failed: ${result.exitCode}\n${result.stderr}\n${result.stdout}');
}
final Directory packageDirectory = tempDirectory.childDirectory(packageName);
// No platform-specific boilerplate files.
expect(packageDirectory.childDirectory('android/'), isNot(exists));
expect(packageDirectory.childDirectory('ios/'), isNot(exists));
expect(packageDirectory.childDirectory('linux/'), isNot(exists));
expect(packageDirectory.childDirectory('macos/'), isNot(exists));
expect(packageDirectory.childDirectory('windows/'), isNot(exists));
return packageDirectory;
}
Future<void> inTempDir(Future<void> Function(Directory tempDirectory) fun) async {
final Directory tempDirectory = fileSystem.directory(fileSystem.systemTempDirectory.createTempSync().resolveSymbolicLinksSync());
try {
await fun(tempDirectory);
} finally {
tryToDelete(tempDirectory);
}
}

View File

@ -26,310 +26,11 @@
@Tags(<String>['no-shuffle'])
library;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import '../src/common.dart';
import 'test_utils.dart' show fileSystem;
const ProcessManager processManager = LocalProcessManager();
final String flutterRoot = getFlutterRoot();
final String flutterBin = fileSystem.path.join(flutterRoot, 'bin', 'flutter');
void debugPrint(String message) {
// This is called to intentionally print debugging output when a test is
// either taking too long or has failed.
// ignore: avoid_print
print(message);
}
typedef LineHandler = String? Function(String line);
abstract class Transition {
const Transition({this.handler, this.logging});
/// Callback that is invoked when the transition matches.
///
/// This should not throw, even if the test is failing. (For example, don't use "expect"
/// in these callbacks.) Throwing here would prevent the [runFlutter] function from running
/// to completion, which would leave zombie `flutter` processes around.
final LineHandler? handler;
/// Whether to enable or disable logging when this transition is matched.
///
/// The default value, null, leaves the logging state unaffected.
final bool? logging;
bool matches(String line);
@protected
bool lineMatchesPattern(String line, Pattern pattern) {
if (pattern is String) {
return line == pattern;
}
return line.contains(pattern);
}
@protected
String describe(Pattern pattern) {
if (pattern is String) {
return '"$pattern"';
}
if (pattern is RegExp) {
return '/${pattern.pattern}/';
}
return '$pattern';
}
}
class Barrier extends Transition {
const Barrier(this.pattern, {super.handler, super.logging});
final Pattern pattern;
@override
bool matches(String line) => lineMatchesPattern(line, pattern);
@override
String toString() => describe(pattern);
}
class Multiple extends Transition {
Multiple(List<Pattern> patterns, {
super.handler,
super.logging,
}) : _originalPatterns = patterns,
patterns = patterns.toList();
final List<Pattern> _originalPatterns;
final List<Pattern> patterns;
@override
bool matches(String line) {
for (int index = 0; index < patterns.length; index += 1) {
if (lineMatchesPattern(line, patterns[index])) {
patterns.removeAt(index);
break;
}
}
return patterns.isEmpty;
}
@override
String toString() {
if (patterns.isEmpty) {
return '${_originalPatterns.map(describe).join(', ')} (all matched)';
}
return '${_originalPatterns.map(describe).join(', ')} (matched ${_originalPatterns.length - patterns.length} so far)';
}
}
class LogLine {
const LogLine(this.channel, this.stamp, this.message);
final String channel;
final String stamp;
final String message;
bool get couldBeCrash => message.contains('Oops; flutter has exited unexpectedly:');
@override
String toString() => '$stamp $channel: $message';
void printClearly() {
debugPrint('$stamp $channel: ${clarify(message)}');
}
static String clarify(String line) {
return line.runes.map<String>((int rune) {
if (rune >= 0x20 && rune <= 0x7F) {
return String.fromCharCode(rune);
}
switch (rune) {
case 0x00: return '<NUL>';
case 0x07: return '<BEL>';
case 0x08: return '<TAB>';
case 0x09: return '<BS>';
case 0x0A: return '<LF>';
case 0x0D: return '<CR>';
}
return '<${rune.toRadixString(16).padLeft(rune <= 0xFF ? 2 : rune <= 0xFFFF ? 4 : 5, '0')}>';
}).join();
}
}
class ProcessTestResult {
const ProcessTestResult(this.exitCode, this.logs);
final int exitCode;
final List<LogLine> logs;
List<String> get stdout {
return logs
.where((LogLine log) => log.channel == 'stdout')
.map<String>((LogLine log) => log.message)
.toList();
}
List<String> get stderr {
return logs
.where((LogLine log) => log.channel == 'stderr')
.map<String>((LogLine log) => log.message)
.toList();
}
@override
String toString() => 'exit code $exitCode\nlogs:\n ${logs.join('\n ')}\n';
}
Future<ProcessTestResult> runFlutter(
List<String> arguments,
String workingDirectory,
List<Transition> transitions, {
bool debug = false,
bool logging = true,
Duration expectedMaxDuration = const Duration(minutes: 10), // must be less than test timeout of 15 minutes! See ../../dart_test.yaml.
}) async {
final Stopwatch clock = Stopwatch()..start();
final Process process = await processManager.start(
<String>[flutterBin, ...arguments],
workingDirectory: workingDirectory,
);
final List<LogLine> logs = <LogLine>[];
int nextTransition = 0;
void describeStatus() {
if (transitions.isNotEmpty) {
debugPrint('Expected state transitions:');
for (int index = 0; index < transitions.length; index += 1) {
debugPrint(
'${index.toString().padLeft(5)} '
'${index < nextTransition ? 'ALREADY MATCHED ' :
index == nextTransition ? 'NOW WAITING FOR>' :
' '} ${transitions[index]}');
}
}
if (logs.isEmpty) {
debugPrint('So far nothing has been logged${ debug ? "" : "; use debug:true to print all output" }.');
} else {
debugPrint('Log${ debug ? "" : " (only contains logged lines; use debug:true to print all output)" }:');
for (final LogLine log in logs) {
log.printClearly();
}
}
}
bool streamingLogs = false;
Timer? timeout;
void processTimeout() {
if (!streamingLogs) {
streamingLogs = true;
if (!debug) {
debugPrint('Test is taking a long time (${clock.elapsed.inSeconds} seconds so far).');
}
describeStatus();
debugPrint('(streaming all logs from this point on...)');
} else {
debugPrint('(taking a long time...)');
}
}
String stamp() => '[${(clock.elapsed.inMilliseconds / 1000.0).toStringAsFixed(1).padLeft(5)}s]';
void processStdout(String line) {
final LogLine log = LogLine('stdout', stamp(), line);
if (logging) {
logs.add(log);
}
if (streamingLogs) {
log.printClearly();
}
if (nextTransition < transitions.length && transitions[nextTransition].matches(line)) {
if (streamingLogs) {
debugPrint('(matched ${transitions[nextTransition]})');
}
if (transitions[nextTransition].logging != null) {
if (!logging && transitions[nextTransition].logging!) {
logs.add(log);
}
logging = transitions[nextTransition].logging!;
if (streamingLogs) {
if (logging) {
debugPrint('(enabled logging)');
} else {
debugPrint('(disabled logging)');
}
}
}
if (transitions[nextTransition].handler != null) {
final String? command = transitions[nextTransition].handler!(line);
if (command != null) {
final LogLine inLog = LogLine('stdin', stamp(), command);
logs.add(inLog);
if (streamingLogs) {
inLog.printClearly();
}
process.stdin.write(command);
}
}
nextTransition += 1;
timeout?.cancel();
timeout = Timer(expectedMaxDuration ~/ 5, processTimeout); // This is not a failure timeout, just when to start logging verbosely to help debugging.
}
}
void processStderr(String line) {
final LogLine log = LogLine('stdout', stamp(), line);
logs.add(log);
if (streamingLogs) {
log.printClearly();
}
}
if (debug) {
processTimeout();
} else {
timeout = Timer(expectedMaxDuration ~/ 2, processTimeout); // This is not a failure timeout, just when to start logging verbosely to help debugging.
}
process.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(processStdout);
process.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(processStderr);
unawaited(process.exitCode.timeout(expectedMaxDuration, onTimeout: () { // This is a failure timeout, must not be short.
debugPrint('${stamp()} (process is not quitting, trying to send a "q" just in case that helps)');
debugPrint('(a functional test should never reach this point)');
final LogLine inLog = LogLine('stdin', stamp(), 'q');
logs.add(inLog);
if (streamingLogs) {
inLog.printClearly();
}
process.stdin.write('q');
return -1; // discarded
}).then(
(int i) => i,
onError: (Object error) {
// ignore errors here, they will be reported on the next line
return -1; // discarded
},
));
final int exitCode = await process.exitCode;
if (streamingLogs) {
debugPrint('${stamp()} (process terminated with exit code $exitCode)');
}
timeout?.cancel();
if (nextTransition < transitions.length) {
debugPrint('The subprocess terminated before all the expected transitions had been matched.');
if (logs.any((LogLine line) => line.couldBeCrash)) {
debugPrint('The subprocess may in fact have crashed. Check the stderr logs below.');
}
debugPrint('The transition that we were hoping to see next but that we never saw was:');
debugPrint('${nextTransition.toString().padLeft(5)} NOW WAITING FOR> ${transitions[nextTransition]}');
if (!streamingLogs) {
describeStatus();
debugPrint('(process terminated with exit code $exitCode)');
}
throw TestFailure('Missed some expected transitions.');
}
if (streamingLogs) {
debugPrint('${stamp()} (completed execution successfully!)');
}
return ProcessTestResult(exitCode, logs);
}
const int progressMessageWidth = 64;
import 'transition_test_utils.dart';
void main() {
testWithoutContext('flutter run writes and clears pidfile appropriately', () async {

View File

@ -0,0 +1,338 @@
// 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 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import '../src/common.dart';
import 'test_utils.dart' show fileSystem;
const ProcessManager processManager = LocalProcessManager();
final String flutterRoot = getFlutterRoot();
final String flutterBin = fileSystem.path.join(flutterRoot, 'bin', 'flutter');
void debugPrint(String message) {
// This is called to intentionally print debugging output when a test is
// either taking too long or has failed.
// ignore: avoid_print
print(message);
}
typedef LineHandler = String? Function(String line);
abstract class Transition {
const Transition({this.handler, this.logging});
/// Callback that is invoked when the transition matches.
///
/// This should not throw, even if the test is failing. (For example, don't use "expect"
/// in these callbacks.) Throwing here would prevent the [runFlutter] function from running
/// to completion, which would leave zombie `flutter` processes around.
final LineHandler? handler;
/// Whether to enable or disable logging when this transition is matched.
///
/// The default value, null, leaves the logging state unaffected.
final bool? logging;
bool matches(String line);
@protected
bool lineMatchesPattern(String line, Pattern pattern) {
if (pattern is String) {
return line == pattern;
}
return line.contains(pattern);
}
@protected
String describe(Pattern pattern) {
if (pattern is String) {
return '"$pattern"';
}
if (pattern is RegExp) {
return '/${pattern.pattern}/';
}
return '$pattern';
}
}
class Barrier extends Transition {
const Barrier(this.pattern, {super.handler, super.logging});
final Pattern pattern;
@override
bool matches(String line) => lineMatchesPattern(line, pattern);
@override
String toString() => describe(pattern);
}
class Multiple extends Transition {
Multiple(
List<Pattern> patterns, {
super.handler,
super.logging,
}) : _originalPatterns = patterns,
patterns = patterns.toList();
final List<Pattern> _originalPatterns;
final List<Pattern> patterns;
@override
bool matches(String line) {
for (int index = 0; index < patterns.length; index += 1) {
if (lineMatchesPattern(line, patterns[index])) {
patterns.removeAt(index);
break;
}
}
return patterns.isEmpty;
}
@override
String toString() {
if (patterns.isEmpty) {
return '${_originalPatterns.map(describe).join(', ')} (all matched)';
}
return '${_originalPatterns.map(describe).join(', ')} (matched ${_originalPatterns.length - patterns.length} so far)';
}
}
class LogLine {
const LogLine(this.channel, this.stamp, this.message);
final String channel;
final String stamp;
final String message;
bool get couldBeCrash =>
message.contains('Oops; flutter has exited unexpectedly:');
@override
String toString() => '$stamp $channel: $message';
void printClearly() {
debugPrint('$stamp $channel: ${clarify(message)}');
}
static String clarify(String line) {
return line.runes.map<String>((int rune) {
if (rune >= 0x20 && rune <= 0x7F) {
return String.fromCharCode(rune);
}
switch (rune) {
case 0x00:
return '<NUL>';
case 0x07:
return '<BEL>';
case 0x08:
return '<TAB>';
case 0x09:
return '<BS>';
case 0x0A:
return '<LF>';
case 0x0D:
return '<CR>';
}
return '<${rune.toRadixString(16).padLeft(rune <= 0xFF ? 2 : rune <= 0xFFFF ? 4 : 5, '0')}>';
}).join();
}
}
class ProcessTestResult {
const ProcessTestResult(this.exitCode, this.logs);
final int exitCode;
final List<LogLine> logs;
List<String> get stdout {
return logs
.where((LogLine log) => log.channel == 'stdout')
.map<String>((LogLine log) => log.message)
.toList();
}
List<String> get stderr {
return logs
.where((LogLine log) => log.channel == 'stderr')
.map<String>((LogLine log) => log.message)
.toList();
}
@override
String toString() => 'exit code $exitCode\nlogs:\n ${logs.join('\n ')}\n';
}
Future<ProcessTestResult> runFlutter(
List<String> arguments,
String workingDirectory,
List<Transition> transitions, {
bool debug = false,
bool logging = true,
Duration expectedMaxDuration = const Duration(
minutes:
10), // must be less than test timeout of 15 minutes! See ../../dart_test.yaml.
}) async {
final Stopwatch clock = Stopwatch()..start();
final Process process = await processManager.start(
<String>[flutterBin, ...arguments],
workingDirectory: workingDirectory,
);
final List<LogLine> logs = <LogLine>[];
int nextTransition = 0;
void describeStatus() {
if (transitions.isNotEmpty) {
debugPrint('Expected state transitions:');
for (int index = 0; index < transitions.length; index += 1) {
debugPrint('${index.toString().padLeft(5)} '
'${index < nextTransition ? 'ALREADY MATCHED ' : index == nextTransition ? 'NOW WAITING FOR>' : ' '} ${transitions[index]}');
}
}
if (logs.isEmpty) {
debugPrint(
'So far nothing has been logged${debug ? "" : "; use debug:true to print all output"}.');
} else {
debugPrint(
'Log${debug ? "" : " (only contains logged lines; use debug:true to print all output)"}:');
for (final LogLine log in logs) {
log.printClearly();
}
}
}
bool streamingLogs = false;
Timer? timeout;
void processTimeout() {
if (!streamingLogs) {
streamingLogs = true;
if (!debug) {
debugPrint(
'Test is taking a long time (${clock.elapsed.inSeconds} seconds so far).');
}
describeStatus();
debugPrint('(streaming all logs from this point on...)');
} else {
debugPrint('(taking a long time...)');
}
}
String stamp() =>
'[${(clock.elapsed.inMilliseconds / 1000.0).toStringAsFixed(1).padLeft(5)}s]';
void processStdout(String line) {
final LogLine log = LogLine('stdout', stamp(), line);
if (logging) {
logs.add(log);
}
if (streamingLogs) {
log.printClearly();
}
if (nextTransition < transitions.length &&
transitions[nextTransition].matches(line)) {
if (streamingLogs) {
debugPrint('(matched ${transitions[nextTransition]})');
}
if (transitions[nextTransition].logging != null) {
if (!logging && transitions[nextTransition].logging!) {
logs.add(log);
}
logging = transitions[nextTransition].logging!;
if (streamingLogs) {
if (logging) {
debugPrint('(enabled logging)');
} else {
debugPrint('(disabled logging)');
}
}
}
if (transitions[nextTransition].handler != null) {
final String? command = transitions[nextTransition].handler!(line);
if (command != null) {
final LogLine inLog = LogLine('stdin', stamp(), command);
logs.add(inLog);
if (streamingLogs) {
inLog.printClearly();
}
process.stdin.write(command);
}
}
nextTransition += 1;
timeout?.cancel();
timeout = Timer(expectedMaxDuration ~/ 5,
processTimeout); // This is not a failure timeout, just when to start logging verbosely to help debugging.
}
}
void processStderr(String line) {
final LogLine log = LogLine('stdout', stamp(), line);
logs.add(log);
if (streamingLogs) {
log.printClearly();
}
}
if (debug) {
processTimeout();
} else {
timeout = Timer(expectedMaxDuration ~/ 2,
processTimeout); // This is not a failure timeout, just when to start logging verbosely to help debugging.
}
process.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen(processStdout);
process.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen(processStderr);
unawaited(process.exitCode.timeout(expectedMaxDuration, onTimeout: () {
// This is a failure timeout, must not be short.
debugPrint(
'${stamp()} (process is not quitting, trying to send a "q" just in case that helps)');
debugPrint('(a functional test should never reach this point)');
final LogLine inLog = LogLine('stdin', stamp(), 'q');
logs.add(inLog);
if (streamingLogs) {
inLog.printClearly();
}
process.stdin.write('q');
return -1; // discarded
}).then(
(int i) => i,
onError: (Object error) {
// ignore errors here, they will be reported on the next line
return -1; // discarded
},
));
final int exitCode = await process.exitCode;
if (streamingLogs) {
debugPrint('${stamp()} (process terminated with exit code $exitCode)');
}
timeout?.cancel();
if (nextTransition < transitions.length) {
debugPrint(
'The subprocess terminated before all the expected transitions had been matched.');
if (logs.any((LogLine line) => line.couldBeCrash)) {
debugPrint(
'The subprocess may in fact have crashed. Check the stderr logs below.');
}
debugPrint(
'The transition that we were hoping to see next but that we never saw was:');
debugPrint(
'${nextTransition.toString().padLeft(5)} NOW WAITING FOR> ${transitions[nextTransition]}');
if (!streamingLogs) {
describeStatus();
debugPrint('(process terminated with exit code $exitCode)');
}
throw TestFailure('Missed some expected transitions.');
}
if (streamingLogs) {
debugPrint('${stamp()} (completed execution successfully!)');
}
return ProcessTestResult(exitCode, logs);
}
const int progressMessageWidth = 64;

View File

@ -449,6 +449,7 @@ class TestFeatureFlags implements FeatureFlags {
this.areCustomDevicesEnabled = false,
this.isFlutterWebWasmEnabled = false,
this.isCliAnimationEnabled = true,
this.isNativeAssetsEnabled = false,
});
@override
@ -481,6 +482,9 @@ class TestFeatureFlags implements FeatureFlags {
@override
final bool isCliAnimationEnabled;
@override
final bool isNativeAssetsEnabled;
@override
bool isEnabled(Feature feature) {
switch (feature) {
@ -502,6 +506,8 @@ class TestFeatureFlags implements FeatureFlags {
return areCustomDevicesEnabled;
case cliAnimation:
return isCliAnimationEnabled;
case nativeAssets:
return isNativeAssetsEnabled;
}
return false;
}