Unverified Commit aa36db1d authored by Daco Harkes's avatar Daco Harkes Committed by GitHub

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.
parent 690800bc
...@@ -4069,6 +4069,26 @@ targets: ...@@ -4069,6 +4069,26 @@ targets:
["devicelab", "ios", "mac"] ["devicelab", "ios", "mac"]
task_name: microbenchmarks_ios 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 - name: Mac_ios native_platform_view_ui_tests_ios
recipe: devicelab/devicelab_drone recipe: devicelab/devicelab_drone
presubmit: false presubmit: false
......
...@@ -199,6 +199,8 @@ ...@@ -199,6 +199,8 @@
/dev/devicelab/bin/tasks/large_image_changer_perf_ios.dart @zanderso @flutter/engine /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/macos_chrome_dev_mode.dart @zanderso @flutter/tool
/dev/devicelab/bin/tasks/microbenchmarks_ios.dart @cyanglaz @flutter/engine /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/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_ios__transition_perf.dart @zanderso @flutter/engine
/dev/devicelab/bin/tasks/new_gallery_skia_ios__transition_perf.dart @zanderso @flutter/engine /dev/devicelab/bin/tasks/new_gallery_skia_ios__transition_perf.dart @zanderso @flutter/engine
......
...@@ -59,6 +59,36 @@ Future<void> main() async { ...@@ -59,6 +59,36 @@ Future<void> main() async {
final File marquee = File(path.join(flutterModuleLibSource.path, 'marquee')); final File marquee = File(path.join(flutterModuleLibSource.path, 'marquee'));
marquee.copySync(path.join(flutterModuleLibDestination.path, 'marquee.dart')); 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'); section('Build ephemeral host app in release mode without CocoaPods');
await inDirectory(projectDir, () async { await inDirectory(projectDir, () async {
...@@ -162,10 +192,8 @@ Future<void> main() async { ...@@ -162,10 +192,8 @@ Future<void> main() async {
section('Add plugins'); section('Add plugins');
final File pubspec = File(path.join(projectDir.path, 'pubspec.yaml'));
String content = await pubspec.readAsString();
content = content.replaceFirst( content = content.replaceFirst(
'\ndependencies:\n', 'dependencies:\n',
// One framework, one Dart-only, one that does not support iOS, and one with a resource bundle. // One framework, one Dart-only, one that does not support iOS, and one with a resource bundle.
''' '''
dependencies: dependencies:
...@@ -221,6 +249,11 @@ dependencies: ...@@ -221,6 +249,11 @@ dependencies:
// Dart-only, no embedded framework. // Dart-only, no embedded framework.
checkDirectoryNotExists(path.join(ephemeralIOSHostApp.path, 'Frameworks', '$dartPluginName.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'); section('Clean and pub get module');
await inDirectory(projectDir, () async { await inDirectory(projectDir, () async {
...@@ -350,6 +383,11 @@ end ...@@ -350,6 +383,11 @@ end
'isolate_snapshot_data', 'isolate_snapshot_data',
)); ));
checkFileExists(path.join(
hostFrameworksDirectory,
libFfiPackageDylib,
));
section('Check the NOTICE file is correct'); section('Check the NOTICE file is correct');
final String licenseFilePath = path.join( final String licenseFilePath = path.join(
...@@ -449,6 +487,13 @@ end ...@@ -449,6 +487,13 @@ end
throw TaskResult.failure('Unexpected armv7 architecture slice in $builtAppBinary'); 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 // The host app example builds plugins statically, url_launcher_ios.framework
// should not exist. // should not exist.
checkDirectoryNotExists(path.join( checkDirectoryNotExists(path.join(
...@@ -685,3 +730,17 @@ class $dartPluginClass { ...@@ -685,3 +730,17 @@ class $dartPluginClass {
// Remove the native plugin code. // Remove the native plugin code.
await Directory(path.join(pluginDir, 'ios')).delete(recursive: true); 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,
],
);
});
}
// 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()();
});
}
// 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);
});
}
// 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);
}
}
...@@ -7,6 +7,7 @@ import 'dart:ui' as ui; ...@@ -7,6 +7,7 @@ import 'dart:ui' as ui;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:ffi_package/ffi_package.dart';
import 'marquee.dart'; import 'marquee.dart';
...@@ -116,10 +117,15 @@ class _MyHomePageState extends State<MyHomePage> { ...@@ -116,10 +117,15 @@ class _MyHomePageState extends State<MyHomePage> {
// button on the Flutter page has been tapped. // button on the Flutter page has been tapped.
int _counter = 0; int _counter = 0;
late int sumResult;
late Future<int> sumAsyncResult;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_platform.setMessageHandler(_handlePlatformIncrement); _platform.setMessageHandler(_handlePlatformIncrement);
sumResult = sum(1, 2);
sumAsyncResult = sumAsync(3, 4);
} }
/// Directly increments our internal counter and rebuilds the UI. /// Directly increments our internal counter and rebuilds the UI.
......
...@@ -144,8 +144,8 @@ BuildApp() { ...@@ -144,8 +144,8 @@ BuildApp() {
RunCommand "${flutter_args[@]}" RunCommand "${flutter_args[@]}"
} }
# Adds the App.framework as an embedded binary and the flutter_assets as # Adds the App.framework as an embedded binary, the flutter_assets as
# resources. # resources, and the native assets.
EmbedFrameworks() { EmbedFrameworks() {
# Embed App.framework from Flutter into the app (after creating the Frameworks directory # Embed App.framework from Flutter into the app (after creating the Frameworks directory
# if it doesn't already exist). # if it doesn't already exist).
...@@ -164,6 +164,17 @@ EmbedFrameworks() { ...@@ -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}/App.framework/App"
RunCommand codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${xcode_frameworks_dir}/FlutterMacOS.framework/FlutterMacOS" RunCommand codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${xcode_frameworks_dir}/FlutterMacOS.framework/FlutterMacOS"
fi 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. # Main entry point.
......
...@@ -171,6 +171,32 @@ class Context { ...@@ -171,6 +171,32 @@ class Context {
exitApp(-1); 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 // Adds the App.framework as an embedded binary and the flutter_assets as
// resources. // resources.
void embedFlutterFrameworks() { void embedFlutterFrameworks() {
...@@ -185,33 +211,46 @@ class Context { ...@@ -185,33 +211,46 @@ class Context {
xcodeFrameworksDir, xcodeFrameworksDir,
] ]
); );
runSync( runRsync(
'rsync', delete: true,
<String>[ '${environment['BUILT_PRODUCTS_DIR']}/App.framework',
'-8', // Avoid mangling filenames with encodings that do not match the current locale. xcodeFrameworksDir,
'-av',
'--delete',
'--filter',
'- .DS_Store',
'${environment['BUILT_PRODUCTS_DIR']}/App.framework',
xcodeFrameworksDir,
],
); );
// Embed the actual Flutter.framework that the Flutter app expects to run against, // 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. // which could be a local build or an arch/type specific build.
runSync( runRsync(
'rsync', delete: true,
<String>[ '${environment['BUILT_PRODUCTS_DIR']}/Flutter.framework',
'-av', '$xcodeFrameworksDir/',
'--delete',
'--filter',
'- .DS_Store',
'${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(); addVmServiceBonjourService();
} }
......
...@@ -777,6 +777,8 @@ TargetPlatform getTargetPlatformForName(String platform) { ...@@ -777,6 +777,8 @@ TargetPlatform getTargetPlatformForName(String platform) {
return TargetPlatform.windows_x64; return TargetPlatform.windows_x64;
case 'web-javascript': case 'web-javascript':
return TargetPlatform.web_javascript; return TargetPlatform.web_javascript;
case 'flutter-tester':
return TargetPlatform.tester;
} }
throw Exception('Unsupported platform name "$platform"'); throw Exception('Unsupported platform name "$platform"');
} }
......
...@@ -6,6 +6,7 @@ import 'package:package_config/package_config.dart'; ...@@ -6,6 +6,7 @@ import 'package:package_config/package_config.dart';
import '../../artifacts.dart'; import '../../artifacts.dart';
import '../../base/build.dart'; import '../../base/build.dart';
import '../../base/common.dart';
import '../../base/file_system.dart'; import '../../base/file_system.dart';
import '../../base/io.dart'; import '../../base/io.dart';
import '../../build_info.dart'; import '../../build_info.dart';
...@@ -19,6 +20,7 @@ import 'assets.dart'; ...@@ -19,6 +20,7 @@ import 'assets.dart';
import 'dart_plugin_registrant.dart'; import 'dart_plugin_registrant.dart';
import 'icon_tree_shaker.dart'; import 'icon_tree_shaker.dart';
import 'localizations.dart'; import 'localizations.dart';
import 'native_assets.dart';
import 'shader_compiler.dart'; import 'shader_compiler.dart';
/// Copies the pre-built flutter bundle. /// Copies the pre-built flutter bundle.
...@@ -125,6 +127,7 @@ class KernelSnapshot extends Target { ...@@ -125,6 +127,7 @@ class KernelSnapshot extends Target {
@override @override
List<Source> get inputs => const <Source>[ List<Source> get inputs => const <Source>[
Source.pattern('{BUILD_DIR}/native_assets.yaml'),
Source.pattern('{PROJECT_DIR}/.dart_tool/package_config_subset'), Source.pattern('{PROJECT_DIR}/.dart_tool/package_config_subset'),
Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/common.dart'), Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/common.dart'),
Source.artifact(Artifact.platformKernelDill), Source.artifact(Artifact.platformKernelDill),
...@@ -142,6 +145,7 @@ class KernelSnapshot extends Target { ...@@ -142,6 +145,7 @@ class KernelSnapshot extends Target {
@override @override
List<Target> get dependencies => const <Target>[ List<Target> get dependencies => const <Target>[
NativeAssets(),
GenerateLocalizationsTarget(), GenerateLocalizationsTarget(),
DartPluginRegistrantTarget(), DartPluginRegistrantTarget(),
]; ];
...@@ -178,6 +182,13 @@ class KernelSnapshot extends Target { ...@@ -178,6 +182,13 @@ class KernelSnapshot extends Target {
final List<String>? fileSystemRoots = environment.defines[kFileSystemRoots]?.split(','); final List<String>? fileSystemRoots = environment.defines[kFileSystemRoots]?.split(',');
final String? fileSystemScheme = environment.defines[kFileSystemScheme]; 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; TargetModel targetModel = TargetModel.flutter;
if (targetPlatform == TargetPlatform.fuchsia_x64 || if (targetPlatform == TargetPlatform.fuchsia_x64 ||
targetPlatform == TargetPlatform.fuchsia_arm64) { targetPlatform == TargetPlatform.fuchsia_arm64) {
...@@ -251,6 +262,7 @@ class KernelSnapshot extends Target { ...@@ -251,6 +262,7 @@ class KernelSnapshot extends Target {
buildDir: environment.buildDir, buildDir: environment.buildDir,
targetOS: targetOS, targetOS: targetOS,
checkDartPluginRegistry: environment.generateDartPluginRegistry, checkDartPluginRegistry: environment.generateDartPluginRegistry,
nativeAssets: nativeAssets,
); );
if (output == null || output.errorCount != 0) { if (output == null || output.errorCount != 0) {
throw Exception(); throw Exception();
......
// 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'),
];
}
...@@ -270,6 +270,28 @@ class BuildIOSFrameworkCommand extends BuildFrameworkCommand { ...@@ -270,6 +270,28 @@ class BuildIOSFrameworkCommand extends BuildFrameworkCommand {
final Status status = globals.logger.startProgress( final Status status = globals.logger.startProgress(
' └─Moving to ${globals.fs.path.relative(modeDirectory.path)}'); ' └─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 { try {
// Delete the intermediaries since they would have been copied into our // Delete the intermediaries since they would have been copied into our
// output frameworks. // output frameworks.
......
...@@ -6,6 +6,7 @@ import 'package:meta/meta.dart'; ...@@ -6,6 +6,7 @@ import 'package:meta/meta.dart';
import '../base/common.dart'; import '../base/common.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart'; import '../base/logger.dart';
import '../base/process.dart'; import '../base/process.dart';
import '../base/utils.dart'; import '../base/utils.dart';
...@@ -96,6 +97,26 @@ class BuildMacOSFrameworkCommand extends BuildFrameworkCommand { ...@@ -96,6 +97,26 @@ class BuildMacOSFrameworkCommand extends BuildFrameworkCommand {
globals.logger.printStatus(' └─Moving to ${globals.fs.path.relative(modeDirectory.path)}'); 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 // Delete the intermediaries since they would have been copied into our
// output frameworks. // output frameworks.
if (buildOutput.existsSync()) { if (buildOutput.existsSync()) {
......
...@@ -36,10 +36,11 @@ class CreateCommand extends CreateBase { ...@@ -36,10 +36,11 @@ class CreateCommand extends CreateBase {
argParser.addOption( argParser.addOption(
'template', 'template',
abbr: 't', 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.', help: 'Specify the type of project to create.',
valueHelp: 'type', valueHelp: 'type',
allowedHelp: CliEnum.allowedHelp(FlutterProjectType.values), allowedHelp: CliEnum.allowedHelp(FlutterProjectType.enabledValues),
); );
argParser.addOption( argParser.addOption(
'sample', 'sample',
...@@ -206,12 +207,14 @@ class CreateCommand extends CreateBase { ...@@ -206,12 +207,14 @@ class CreateCommand extends CreateBase {
final FlutterProjectType template = _getProjectType(projectDir); final FlutterProjectType template = _getProjectType(projectDir);
final bool generateModule = template == FlutterProjectType.module; final bool generateModule = template == FlutterProjectType.module;
final bool generateMethodChannelsPlugin = template == FlutterProjectType.plugin; final bool generateMethodChannelsPlugin = template == FlutterProjectType.plugin;
final bool generateFfiPackage = template == FlutterProjectType.packageFfi;
final bool generateFfiPlugin = template == FlutterProjectType.pluginFfi; final bool generateFfiPlugin = template == FlutterProjectType.pluginFfi;
final bool generateFfi = generateFfiPlugin || generateFfiPackage;
final bool generatePackage = template == FlutterProjectType.package; final bool generatePackage = template == FlutterProjectType.package;
final List<String> platforms = stringsArg('platforms'); final List<String> platforms = stringsArg('platforms');
// `--platforms` does not support module or package. // `--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'; final String template = generateModule ? 'module' : 'package';
throwToolExit( throwToolExit(
'The "--platforms" argument is not supported in $template template.', 'The "--platforms" argument is not supported in $template template.',
...@@ -225,15 +228,15 @@ class CreateCommand extends CreateBase { ...@@ -225,15 +228,15 @@ class CreateCommand extends CreateBase {
'The web platform is not supported in plugin_ffi template.', 'The web platform is not supported in plugin_ffi template.',
exitCode: 2, exitCode: 2,
); );
} else if (generateFfiPlugin && argResults!.wasParsed('ios-language')) { } else if (generateFfi && argResults!.wasParsed('ios-language')) {
throwToolExit( 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++.', 'template: the language will always be C or C++.',
exitCode: 2, exitCode: 2,
); );
} else if (generateFfiPlugin && argResults!.wasParsed('android-language')) { } else if (generateFfi && argResults!.wasParsed('android-language')) {
throwToolExit( 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++.', 'template: the language will always be C or C++.',
exitCode: 2, exitCode: 2,
); );
...@@ -306,6 +309,7 @@ class CreateCommand extends CreateBase { ...@@ -306,6 +309,7 @@ class CreateCommand extends CreateBase {
flutterRoot: flutterRoot, flutterRoot: flutterRoot,
withPlatformChannelPluginHook: generateMethodChannelsPlugin, withPlatformChannelPluginHook: generateMethodChannelsPlugin,
withFfiPluginHook: generateFfiPlugin, withFfiPluginHook: generateFfiPlugin,
withFfiPackage: generateFfiPackage,
withEmptyMain: emptyArgument, withEmptyMain: emptyArgument,
androidLanguage: stringArg('android-language'), androidLanguage: stringArg('android-language'),
iosLanguage: stringArg('ios-language'), iosLanguage: stringArg('ios-language'),
...@@ -393,6 +397,15 @@ class CreateCommand extends CreateBase { ...@@ -393,6 +397,15 @@ class CreateCommand extends CreateBase {
projectType: template, projectType: template,
); );
pubContext = PubContext.createPlugin; pubContext = PubContext.createPlugin;
case FlutterProjectType.packageFfi:
generatedFileCount += await _generateFfiPackage(
relativeDir,
templateContext,
overwrite: overwrite,
printStatusWhenWriting: !creatingNewProject,
projectType: template,
);
pubContext = PubContext.createPackage;
} }
if (boolArg('pub')) { if (boolArg('pub')) {
...@@ -403,14 +416,21 @@ class CreateCommand extends CreateBase { ...@@ -403,14 +416,21 @@ class CreateCommand extends CreateBase {
offline: boolArg('offline'), offline: boolArg('offline'),
outputMode: PubOutputMode.summaryOnly, outputMode: PubOutputMode.summaryOnly,
); );
await project.ensureReadyForPlatformSpecificTooling( // Setting `includeIos` etc to false as with FlutterProjectType.package
androidPlatform: includeAndroid, // causes the example sub directory to not get os sub directories.
iosPlatform: includeIos, // This will lead to `flutter build ios` to fail in the example.
linuxPlatform: includeLinux, // TODO(dacoharkes): Uncouple the app and parent project platforms. https://github.com/flutter/flutter/issues/133874
macOSPlatform: includeMacos, // Then this if can be removed.
windowsPlatform: includeWindows, if (!generateFfiPackage) {
webPlatform: includeWeb, await project.ensureReadyForPlatformSpecificTooling(
); androidPlatform: includeAndroid,
iosPlatform: includeIos,
linuxPlatform: includeLinux,
macOSPlatform: includeMacos,
windowsPlatform: includeWindows,
webPlatform: includeWeb,
);
}
} }
if (sampleCode != null) { if (sampleCode != null) {
_applySample(relativeDir, sampleCode); _applySample(relativeDir, sampleCode);
...@@ -663,6 +683,48 @@ Your $application code is in $relativeAppMain. ...@@ -663,6 +683,48 @@ Your $application code is in $relativeAppMain.
return generatedCount; 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 // Takes an application template and replaces the main.dart with one from the
// documentation website in sampleCode. Returns the difference in the number // documentation website in sampleCode. Returns the difference in the number
// of files after applying the sample, since it also deletes the application's // of files after applying the sample, since it also deletes the application's
......
...@@ -352,6 +352,7 @@ abstract class CreateBase extends FlutterCommand { ...@@ -352,6 +352,7 @@ abstract class CreateBase extends FlutterCommand {
String? gradleVersion, String? gradleVersion,
bool withPlatformChannelPluginHook = false, bool withPlatformChannelPluginHook = false,
bool withFfiPluginHook = false, bool withFfiPluginHook = false,
bool withFfiPackage = false,
bool withEmptyMain = false, bool withEmptyMain = false,
bool ios = false, bool ios = false,
bool android = false, bool android = false,
...@@ -399,9 +400,11 @@ abstract class CreateBase extends FlutterCommand { ...@@ -399,9 +400,11 @@ abstract class CreateBase extends FlutterCommand {
'pluginClassCapitalSnakeCase': pluginClassCapitalSnakeCase, 'pluginClassCapitalSnakeCase': pluginClassCapitalSnakeCase,
'pluginDartClass': pluginDartClass, 'pluginDartClass': pluginDartClass,
'pluginProjectUUID': const Uuid().v4().toUpperCase(), 'pluginProjectUUID': const Uuid().v4().toUpperCase(),
'withFfi': withFfiPluginHook || withFfiPackage,
'withFfiPackage': withFfiPackage,
'withFfiPluginHook': withFfiPluginHook, 'withFfiPluginHook': withFfiPluginHook,
'withPlatformChannelPluginHook': withPlatformChannelPluginHook, 'withPlatformChannelPluginHook': withPlatformChannelPluginHook,
'withPluginHook': withFfiPluginHook || withPlatformChannelPluginHook, 'withPluginHook': withFfiPluginHook || withFfiPackage || withPlatformChannelPluginHook,
'withEmptyMain': withEmptyMain, 'withEmptyMain': withEmptyMain,
'androidLanguage': androidLanguage, 'androidLanguage': androidLanguage,
'iosLanguage': iosLanguage, 'iosLanguage': iosLanguage,
......
...@@ -240,6 +240,7 @@ class KernelCompiler { ...@@ -240,6 +240,7 @@ class KernelCompiler {
required bool trackWidgetCreation, required bool trackWidgetCreation,
required List<String> dartDefines, required List<String> dartDefines,
required PackageConfig packageConfig, required PackageConfig packageConfig,
String? nativeAssets,
}) async { }) async {
final TargetPlatform? platform = targetModel == TargetModel.dartdevc ? TargetPlatform.web_javascript : null; final TargetPlatform? platform = targetModel == TargetModel.dartdevc ? TargetPlatform.web_javascript : null;
final String frontendServer = _artifacts.getArtifactPath( final String frontendServer = _artifacts.getArtifactPath(
...@@ -337,6 +338,10 @@ class KernelCompiler { ...@@ -337,6 +338,10 @@ class KernelCompiler {
'package:flutter/src/dart_plugin_registrant.dart', 'package:flutter/src/dart_plugin_registrant.dart',
'-Dflutter.dart_plugin_registrant=$dartPluginRegistrantUri', '-Dflutter.dart_plugin_registrant=$dartPluginRegistrantUri',
], ],
if (nativeAssets != null) ...<String>[
'--native-assets',
nativeAssets,
],
// See: https://github.com/flutter/flutter/issues/103994 // See: https://github.com/flutter/flutter/issues/103994
'--verbosity=error', '--verbosity=error',
...?extraFrontEndOptions, ...?extraFrontEndOptions,
...@@ -381,9 +386,10 @@ class _RecompileRequest extends _CompilationRequest { ...@@ -381,9 +386,10 @@ class _RecompileRequest extends _CompilationRequest {
this.invalidatedFiles, this.invalidatedFiles,
this.outputPath, this.outputPath,
this.packageConfig, this.packageConfig,
this.suppressErrors, this.suppressErrors, {
{this.additionalSourceUri} this.additionalSourceUri,
); this.nativeAssetsYamlUri,
});
Uri mainUri; Uri mainUri;
List<Uri>? invalidatedFiles; List<Uri>? invalidatedFiles;
...@@ -391,6 +397,7 @@ class _RecompileRequest extends _CompilationRequest { ...@@ -391,6 +397,7 @@ class _RecompileRequest extends _CompilationRequest {
PackageConfig packageConfig; PackageConfig packageConfig;
bool suppressErrors; bool suppressErrors;
final Uri? additionalSourceUri; final Uri? additionalSourceUri;
final Uri? nativeAssetsYamlUri;
@override @override
Future<CompilerOutput?> _run(DefaultResidentCompiler compiler) async => Future<CompilerOutput?> _run(DefaultResidentCompiler compiler) async =>
...@@ -515,6 +522,7 @@ abstract class ResidentCompiler { ...@@ -515,6 +522,7 @@ abstract class ResidentCompiler {
bool suppressErrors = false, bool suppressErrors = false,
bool checkDartPluginRegistry = false, bool checkDartPluginRegistry = false,
File? dartPluginRegistrant, File? dartPluginRegistrant,
Uri? nativeAssetsYaml,
}); });
Future<CompilerOutput?> compileExpression( Future<CompilerOutput?> compileExpression(
...@@ -663,6 +671,7 @@ class DefaultResidentCompiler implements ResidentCompiler { ...@@ -663,6 +671,7 @@ class DefaultResidentCompiler implements ResidentCompiler {
File? dartPluginRegistrant, File? dartPluginRegistrant,
String? projectRootPath, String? projectRootPath,
FileSystem? fs, FileSystem? fs,
Uri? nativeAssetsYaml,
}) async { }) async {
if (!_controller.hasListener) { if (!_controller.hasListener) {
_controller.stream.listen(_handleCompilationRequest); _controller.stream.listen(_handleCompilationRequest);
...@@ -681,6 +690,7 @@ class DefaultResidentCompiler implements ResidentCompiler { ...@@ -681,6 +690,7 @@ class DefaultResidentCompiler implements ResidentCompiler {
packageConfig, packageConfig,
suppressErrors, suppressErrors,
additionalSourceUri: additionalSourceUri, additionalSourceUri: additionalSourceUri,
nativeAssetsYamlUri: nativeAssetsYaml,
)); ));
return completer.future; return completer.future;
} }
...@@ -699,12 +709,22 @@ class DefaultResidentCompiler implements ResidentCompiler { ...@@ -699,12 +709,22 @@ class DefaultResidentCompiler implements ResidentCompiler {
toMultiRootPath(request.additionalSourceUri!, fileSystemScheme, fileSystemRoots, _platform.isWindows); toMultiRootPath(request.additionalSourceUri!, fileSystemScheme, fileSystemRoots, _platform.isWindows);
} }
final String? nativeAssets = request.nativeAssetsYamlUri?.toString();
final Process? server = _server; final Process? server = _server;
if (server == null) { if (server == null) {
return _compile(mainUri, request.outputPath, additionalSourceUri: additionalSourceUri); return _compile(
mainUri,
request.outputPath,
additionalSourceUri: additionalSourceUri,
nativeAssetsUri: nativeAssets,
);
} }
final String inputKey = Uuid().generateV4(); 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'); server.stdin.writeln('recompile $mainUri $inputKey');
_logger.printTrace('<- recompile $mainUri $inputKey'); _logger.printTrace('<- recompile $mainUri $inputKey');
final List<Uri>? invalidatedFiles = request.invalidatedFiles; final List<Uri>? invalidatedFiles = request.invalidatedFiles;
...@@ -746,9 +766,10 @@ class DefaultResidentCompiler implements ResidentCompiler { ...@@ -746,9 +766,10 @@ class DefaultResidentCompiler implements ResidentCompiler {
Future<CompilerOutput?> _compile( Future<CompilerOutput?> _compile(
String scriptUri, String scriptUri,
String? outputPath, String? outputPath, {
{String? additionalSourceUri} String? additionalSourceUri,
) async { String? nativeAssetsUri,
}) async {
final TargetPlatform? platform = (targetModel == TargetModel.dartdevc) ? TargetPlatform.web_javascript : null; final TargetPlatform? platform = (targetModel == TargetModel.dartdevc) ? TargetPlatform.web_javascript : null;
final String frontendServer = artifacts.getArtifactPath( final String frontendServer = artifacts.getArtifactPath(
Artifact.frontendServerSnapshotForEngineDartSdk, Artifact.frontendServerSnapshotForEngineDartSdk,
...@@ -806,6 +827,10 @@ class DefaultResidentCompiler implements ResidentCompiler { ...@@ -806,6 +827,10 @@ class DefaultResidentCompiler implements ResidentCompiler {
'package:flutter/src/dart_plugin_registrant.dart', 'package:flutter/src/dart_plugin_registrant.dart',
'-Dflutter.dart_plugin_registrant=$additionalSourceUri', '-Dflutter.dart_plugin_registrant=$additionalSourceUri',
], ],
if (nativeAssetsUri != null) ...<String>[
'--native-assets',
nativeAssetsUri,
],
if (platformDill != null) ...<String>[ if (platformDill != null) ...<String>[
'--platform', '--platform',
platformDill!, platformDill!,
...@@ -842,6 +867,11 @@ class DefaultResidentCompiler implements ResidentCompiler { ...@@ -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'); _server?.stdin.writeln('compile $scriptUri');
_logger.printTrace('<- compile $scriptUri'); _logger.printTrace('<- compile $scriptUri');
......
...@@ -50,6 +50,9 @@ abstract class FeatureFlags { ...@@ -50,6 +50,9 @@ abstract class FeatureFlags {
/// Whether animations are used in the command line interface. /// Whether animations are used in the command line interface.
bool get isCliAnimationEnabled => true; 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. /// Whether a particular feature is enabled for the current channel.
/// ///
/// Prefer using one of the specific getters above instead of this API. /// Prefer using one of the specific getters above instead of this API.
...@@ -68,6 +71,7 @@ const List<Feature> allFeatures = <Feature>[ ...@@ -68,6 +71,7 @@ const List<Feature> allFeatures = <Feature>[
flutterCustomDevicesFeature, flutterCustomDevicesFeature,
flutterWebWasm, flutterWebWasm,
cliAnimation, cliAnimation,
nativeAssets,
]; ];
/// All current Flutter feature flags that can be configured. /// All current Flutter feature flags that can be configured.
...@@ -158,6 +162,16 @@ const Feature cliAnimation = Feature.fullyEnabled( ...@@ -158,6 +162,16 @@ const Feature cliAnimation = Feature.fullyEnabled(
configSetting: 'cli-animations', 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. /// A [Feature] is a process for conditionally enabling tool features.
/// ///
/// All settings are optional, and if not provided will generally default to /// All settings are optional, and if not provided will generally default to
......
...@@ -87,7 +87,6 @@ class FlutterDeviceManager extends DeviceManager { ...@@ -87,7 +87,6 @@ class FlutterDeviceManager extends DeviceManager {
processManager: processManager, processManager: processManager,
logger: logger, logger: logger,
artifacts: artifacts, artifacts: artifacts,
operatingSystemUtils: operatingSystemUtils,
), ),
MacOSDevices( MacOSDevices(
processManager: processManager, processManager: processManager,
......
...@@ -55,6 +55,9 @@ class FlutterFeatureFlags implements FeatureFlags { ...@@ -55,6 +55,9 @@ class FlutterFeatureFlags implements FeatureFlags {
return isEnabled(cliAnimation); return isEnabled(cliAnimation);
} }
@override
bool get isNativeAssetsEnabled => isEnabled(nativeAssets);
@override @override
bool isEnabled(Feature feature) { bool isEnabled(Feature feature) {
final String currentChannel = _flutterVersion.channel; final String currentChannel = _flutterVersion.channel;
......
...@@ -970,7 +970,7 @@ Future<void> _writeWebPluginRegistrant(FlutterProject project, List<Plugin> plug ...@@ -970,7 +970,7 @@ Future<void> _writeWebPluginRegistrant(FlutterProject project, List<Plugin> plug
/// be created only if missing. /// be created only if missing.
/// ///
/// This uses [project.flutterPluginsDependenciesFile], so it should only be /// 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}) { void createPluginSymlinks(FlutterProject project, {bool force = false, @visibleForTesting FeatureFlags? featureFlagsOverride}) {
final FeatureFlags localFeatureFlags = featureFlagsOverride ?? featureFlags; final FeatureFlags localFeatureFlags = featureFlagsOverride ?? featureFlags;
Map<String, Object?>? platformPlugins; Map<String, Object?>? platformPlugins;
......
...@@ -7,6 +7,7 @@ import 'package:yaml/yaml.dart'; ...@@ -7,6 +7,7 @@ import 'package:yaml/yaml.dart';
import 'base/file_system.dart'; import 'base/file_system.dart';
import 'base/logger.dart'; import 'base/logger.dart';
import 'base/utils.dart'; import 'base/utils.dart';
import 'features.dart';
import 'project.dart'; import 'project.dart';
import 'template.dart'; import 'template.dart';
import 'version.dart'; import 'version.dart';
...@@ -28,6 +29,9 @@ enum FlutterProjectType implements CliEnum { ...@@ -28,6 +29,9 @@ enum FlutterProjectType implements CliEnum {
/// components, only Dart. /// components, only Dart.
package, package,
/// This is a Dart package project with external builds for native components.
packageFfi,
/// This is a native plugin project. /// This is a native plugin project.
plugin, plugin,
...@@ -52,6 +56,10 @@ enum FlutterProjectType implements CliEnum { ...@@ -52,6 +56,10 @@ enum FlutterProjectType implements CliEnum {
'Generate a shareable Flutter project containing an API ' 'Generate a shareable Flutter project containing an API '
'in Dart code with a platform-specific implementation through dart:ffi for Android, iOS, ' 'in Dart code with a platform-specific implementation through dart:ffi for Android, iOS, '
'Linux, macOS, Windows, or any combination of these.', '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 => FlutterProjectType.module =>
'Generate a project to add a Flutter module to an existing Android or iOS application.', 'Generate a project to add a Flutter module to an existing Android or iOS application.',
}; };
...@@ -64,6 +72,16 @@ enum FlutterProjectType implements CliEnum { ...@@ -64,6 +72,16 @@ enum FlutterProjectType implements CliEnum {
} }
return null; 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. /// Verifies the expected yaml keys are present in the file.
......
// 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');
}
// 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');
}
// 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'),
);
}
This diff is collapsed.
...@@ -21,6 +21,7 @@ import 'dart/package_map.dart'; ...@@ -21,6 +21,7 @@ import 'dart/package_map.dart';
import 'devfs.dart'; import 'devfs.dart';
import 'device.dart'; import 'device.dart';
import 'globals.dart' as globals; import 'globals.dart' as globals;
import 'native_assets.dart';
import 'project.dart'; import 'project.dart';
import 'reporting/reporting.dart'; import 'reporting/reporting.dart';
import 'resident_runner.dart'; import 'resident_runner.dart';
...@@ -92,9 +93,11 @@ class HotRunner extends ResidentRunner { ...@@ -92,9 +93,11 @@ class HotRunner extends ResidentRunner {
StopwatchFactory stopwatchFactory = const StopwatchFactory(), StopwatchFactory stopwatchFactory = const StopwatchFactory(),
ReloadSourcesHelper reloadSourcesHelper = defaultReloadSourcesHelper, ReloadSourcesHelper reloadSourcesHelper = defaultReloadSourcesHelper,
ReassembleHelper reassembleHelper = _defaultReassembleHelper, ReassembleHelper reassembleHelper = _defaultReassembleHelper,
NativeAssetsBuildRunner? buildRunner,
}) : _stopwatchFactory = stopwatchFactory, }) : _stopwatchFactory = stopwatchFactory,
_reloadSourcesHelper = reloadSourcesHelper, _reloadSourcesHelper = reloadSourcesHelper,
_reassembleHelper = reassembleHelper, _reassembleHelper = reassembleHelper,
_buildRunner = buildRunner,
super( super(
hotMode: true, hotMode: true,
); );
...@@ -132,6 +135,8 @@ class HotRunner extends ResidentRunner { ...@@ -132,6 +135,8 @@ class HotRunner extends ResidentRunner {
String? _sdkName; String? _sdkName;
bool? _emulator; bool? _emulator;
NativeAssetsBuildRunner? _buildRunner;
Future<void> _calculateTargetPlatform() async { Future<void> _calculateTargetPlatform() async {
if (_targetPlatform != null) { if (_targetPlatform != null) {
return; return;
...@@ -360,6 +365,15 @@ class HotRunner extends ResidentRunner { ...@@ -360,6 +365,15 @@ class HotRunner extends ResidentRunner {
}) async { }) async {
await _calculateTargetPlatform(); 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 Stopwatch appStartedTimer = Stopwatch()..start();
final File mainFile = globals.fs.file(mainPath); final File mainFile = globals.fs.file(mainPath);
firstBuildTime = DateTime.now(); firstBuildTime = DateTime.now();
...@@ -391,6 +405,7 @@ class HotRunner extends ResidentRunner { ...@@ -391,6 +405,7 @@ class HotRunner extends ResidentRunner {
packageConfig: debuggingOptions.buildInfo.packageConfig, packageConfig: debuggingOptions.buildInfo.packageConfig,
projectRootPath: FlutterProject.current().directory.absolute.path, projectRootPath: FlutterProject.current().directory.absolute.path,
fs: globals.fs, fs: globals.fs,
nativeAssetsYaml: nativeAssetsYaml,
).then((CompilerOutput? output) { ).then((CompilerOutput? output) {
compileTimer.stop(); compileTimer.stop();
totalCompileTime += compileTimer.elapsed; totalCompileTime += compileTimer.elapsed;
......
...@@ -10,11 +10,14 @@ import 'package:meta/meta.dart'; ...@@ -10,11 +10,14 @@ import 'package:meta/meta.dart';
import '../artifacts.dart'; import '../artifacts.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/platform.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../bundle.dart'; import '../bundle.dart';
import '../compile.dart'; import '../compile.dart';
import '../flutter_plugins.dart'; import '../flutter_plugins.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
import '../macos/native_assets.dart';
import '../native_assets.dart';
import '../project.dart'; import '../project.dart';
import 'test_time_recorder.dart'; import 'test_time_recorder.dart';
...@@ -163,6 +166,26 @@ class TestCompiler { ...@@ -163,6 +166,26 @@ class TestCompiler {
invalidatedRegistrantFiles.add(flutterProject!.dartPluginRegistrant.absolute.uri); 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( final CompilerOutput? compilerOutput = await compiler!.recompile(
request.mainUri, request.mainUri,
<Uri>[request.mainUri, ...invalidatedRegistrantFiles], <Uri>[request.mainUri, ...invalidatedRegistrantFiles],
...@@ -171,6 +194,7 @@ class TestCompiler { ...@@ -171,6 +194,7 @@ class TestCompiler {
projectRootPath: flutterProject?.directory.absolute.path, projectRootPath: flutterProject?.directory.absolute.path,
checkDartPluginRegistry: true, checkDartPluginRegistry: true,
fs: globals.fs, fs: globals.fs,
nativeAssetsYaml: nativeAssetsYaml,
); );
final String? outputPath = compilerOutput?.outputFilename; final String? outputPath = compilerOutput?.outputFilename;
......
...@@ -11,7 +11,6 @@ import '../artifacts.dart'; ...@@ -11,7 +11,6 @@ import '../artifacts.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/io.dart'; import '../base/io.dart';
import '../base/logger.dart'; import '../base/logger.dart';
import '../base/os.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../bundle.dart'; import '../bundle.dart';
import '../bundle_builder.dart'; import '../bundle_builder.dart';
...@@ -50,13 +49,11 @@ class FlutterTesterDevice extends Device { ...@@ -50,13 +49,11 @@ class FlutterTesterDevice extends Device {
required Logger logger, required Logger logger,
required FileSystem fileSystem, required FileSystem fileSystem,
required Artifacts artifacts, required Artifacts artifacts,
required OperatingSystemUtils operatingSystemUtils,
}) : _processManager = processManager, }) : _processManager = processManager,
_flutterVersion = flutterVersion, _flutterVersion = flutterVersion,
_logger = logger, _logger = logger,
_fileSystem = fileSystem, _fileSystem = fileSystem,
_artifacts = artifacts, _artifacts = artifacts,
_operatingSystemUtils = operatingSystemUtils,
super( super(
platformType: null, platformType: null,
category: null, category: null,
...@@ -68,7 +65,6 @@ class FlutterTesterDevice extends Device { ...@@ -68,7 +65,6 @@ class FlutterTesterDevice extends Device {
final Logger _logger; final Logger _logger;
final FileSystem _fileSystem; final FileSystem _fileSystem;
final Artifacts _artifacts; final Artifacts _artifacts;
final OperatingSystemUtils _operatingSystemUtils;
Process? _process; Process? _process;
final DevicePortForwarder _portForwarder = const NoOpDevicePortForwarder(); final DevicePortForwarder _portForwarder = const NoOpDevicePortForwarder();
...@@ -157,7 +153,7 @@ class FlutterTesterDevice extends Device { ...@@ -157,7 +153,7 @@ class FlutterTesterDevice extends Device {
buildInfo: buildInfo, buildInfo: buildInfo,
mainPath: mainPath, mainPath: mainPath,
applicationKernelFilePath: applicationKernelFilePath, applicationKernelFilePath: applicationKernelFilePath,
platform: getTargetPlatformForName(getNameForHostPlatform(_operatingSystemUtils.hostPlatform)), platform: TargetPlatform.tester,
assetDirPath: assetDirectory.path, assetDirPath: assetDirectory.path,
); );
...@@ -258,15 +254,13 @@ class FlutterTesterDevices extends PollingDeviceDiscovery { ...@@ -258,15 +254,13 @@ class FlutterTesterDevices extends PollingDeviceDiscovery {
required ProcessManager processManager, required ProcessManager processManager,
required Logger logger, required Logger logger,
required FlutterVersion flutterVersion, required FlutterVersion flutterVersion,
required OperatingSystemUtils operatingSystemUtils,
}) : _testerDevice = FlutterTesterDevice( }) : _testerDevice = FlutterTesterDevice(
kTesterDeviceId, kTesterDeviceId,
fileSystem: fileSystem, fileSystem: fileSystem,
artifacts: artifacts, artifacts: artifacts,
processManager: processManager, processManager: processManager,
logger: logger, logger: logger,
flutterVersion: flutterVersion, flutterVersion: flutterVersion,
operatingSystemUtils: operatingSystemUtils,
), ),
super('Flutter tester'); super('Flutter tester');
......
...@@ -50,6 +50,11 @@ dependencies: ...@@ -50,6 +50,11 @@ dependencies:
async: 2.11.0 async: 2.11.0
unified_analytics: 3.0.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 # We depend on very specific internal implementation details of the
# 'test' package, which change between versions, so when upgrading # 'test' package, which change between versions, so when upgrading
# this, make sure the tests are still running correctly. # this, make sure the tests are still running correctly.
...@@ -107,4 +112,4 @@ dartdoc: ...@@ -107,4 +112,4 @@ dartdoc:
# Exclude this package from the hosted API docs. # Exclude this package from the hosted API docs.
nodoc: true nodoc: true
# PUBSPEC CHECKSUM: a4aa # PUBSPEC CHECKSUM: 284b
...@@ -27,11 +27,11 @@ import 'dart:async'; ...@@ -27,11 +27,11 @@ import 'dart:async';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:{{pluginProjectName}}/{{pluginProjectName}}.dart'; import 'package:{{pluginProjectName}}/{{pluginProjectName}}.dart';
{{/withPlatformChannelPluginHook}} {{/withPlatformChannelPluginHook}}
{{#withFfiPluginHook}} {{#withFfi}}
import 'dart:async'; import 'dart:async';
import 'package:{{pluginProjectName}}/{{pluginProjectName}}.dart' as {{pluginProjectName}}; import 'package:{{pluginProjectName}}/{{pluginProjectName}}.dart' as {{pluginProjectName}};
{{/withFfiPluginHook}} {{/withFfi}}
void main() { void main() {
runApp(const MyApp()); runApp(const MyApp());
...@@ -213,7 +213,7 @@ class _MyAppState extends State<MyApp> { ...@@ -213,7 +213,7 @@ class _MyAppState extends State<MyApp> {
} }
} }
{{/withPlatformChannelPluginHook}} {{/withPlatformChannelPluginHook}}
{{#withFfiPluginHook}} {{#withFfi}}
class MyApp extends StatefulWidget { class MyApp extends StatefulWidget {
const MyApp({super.key}); const MyApp({super.key});
...@@ -279,5 +279,5 @@ class _MyAppState extends State<MyApp> { ...@@ -279,5 +279,5 @@ class _MyAppState extends State<MyApp> {
); );
} }
} }
{{/withFfiPluginHook}} {{/withFfi}}
{{/withEmptyMain}} {{/withEmptyMain}}
...@@ -8,9 +8,9 @@ ...@@ -8,9 +8,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
{{^withFfiPluginHook}} {{^withFfi}}
import 'package:{{projectName}}/main.dart'; import 'package:{{projectName}}/main.dart';
{{/withFfiPluginHook}} {{/withFfi}}
{{^withPluginHook}} {{^withPluginHook}}
void main() { void main() {
......
# 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/
# 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
# {{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.
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
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);
}
# 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
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;
}();
// 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,
);
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
#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;
}
#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);
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);
});
}
...@@ -10,6 +10,9 @@ version: ...@@ -10,6 +10,9 @@ version:
{{#withFfiPluginHook}} {{#withFfiPluginHook}}
project_type: plugin_ffi project_type: plugin_ffi
{{/withFfiPluginHook}} {{/withFfiPluginHook}}
{{#withFfiPackage}}
project_type: package_ffi
{{/withFfiPackage}}
{{#withPlatformChannelPluginHook}} {{#withPlatformChannelPluginHook}}
project_type: plugin project_type: plugin
{{/withPlatformChannelPluginHook}} {{/withPlatformChannelPluginHook}}
...@@ -17,10 +17,10 @@ dependencies: ...@@ -17,10 +17,10 @@ dependencies:
plugin_platform_interface: ^2.0.2 plugin_platform_interface: ^2.0.2
dev_dependencies: dev_dependencies:
{{#withFfiPluginHook}} {{#withFfi}}
ffi: ^2.0.1 ffi: ^2.0.2
ffigen: ^6.1.2 ffigen: ^9.0.0
{{/withFfiPluginHook}} {{/withFfi}}
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^2.0.0 flutter_lints: ^2.0.0
......
...@@ -248,6 +248,21 @@ ...@@ -248,6 +248,21 @@
"templates/package/README.md.tmpl", "templates/package/README.md.tmpl",
"templates/package/test/projectName_test.dart.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/build.gradle.tmpl",
"templates/plugin/android-java.tmpl/projectName_android.iml.tmpl", "templates/plugin/android-java.tmpl/projectName_android.iml.tmpl",
"templates/plugin/android-java.tmpl/src/main/java/androidIdentifier/pluginClass.java.tmpl", "templates/plugin/android-java.tmpl/src/main/java/androidIdentifier/pluginClass.java.tmpl",
......
...@@ -10,11 +10,14 @@ import 'package:flutter_tools/src/convert.dart'; ...@@ -10,11 +10,14 @@ import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/dart/pub.dart'; import 'package:flutter_tools/src/dart/pub.dart';
import 'package:flutter_tools/src/doctor.dart'; import 'package:flutter_tools/src/doctor.dart';
import 'package:flutter_tools/src/doctor_validator.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/globals.dart' as globals;
import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/project.dart';
import 'package:test/fake.dart'; import 'package:test/fake.dart';
import '../../src/common.dart';
import '../../src/context.dart'; import '../../src/context.dart';
import '../../src/fakes.dart';
import '../../src/test_flutter_command_runner.dart'; import '../../src/test_flutter_command_runner.dart';
import '../../src/testbed.dart'; import '../../src/testbed.dart';
...@@ -79,6 +82,7 @@ void main() { ...@@ -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', 'skeleton'),
globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'module', 'common'), 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'),
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'),
globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'plugin_ffi'), globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'plugin_ffi'),
globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'plugin_shared'), globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'plugin_shared'),
...@@ -109,6 +113,7 @@ void main() { ...@@ -109,6 +113,7 @@ void main() {
flutterManifest.writeAsStringSync('{"files":[]}'); flutterManifest.writeAsStringSync('{"files":[]}');
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(), DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(),
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true),
}); });
}); });
...@@ -133,6 +138,9 @@ void main() { ...@@ -133,6 +138,9 @@ void main() {
await runner.run(<String>['create', '--no-pub', '--template=plugin_ffi', 'testy5']); await runner.run(<String>['create', '--no-pub', '--template=plugin_ffi', 'testy5']);
expect((await command.usageValues).commandCreateProjectType, 'plugin_ffi'); 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 { testUsingContext('set iOS host language type as usage value', () => testbed.run(() async {
...@@ -183,6 +191,29 @@ void main() { ...@@ -183,6 +191,29 @@ void main() {
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Pub: () => fakePub, 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.
),
});
}); });
} }
......
...@@ -856,6 +856,7 @@ class FakeBundleBuilder extends Fake implements BundleBuilder { ...@@ -856,6 +856,7 @@ class FakeBundleBuilder extends Fake implements BundleBuilder {
String? applicationKernelFilePath, String? applicationKernelFilePath,
String? depfilePath, String? depfilePath,
String? assetDirPath, String? assetDirPath,
Uri? nativeAssets,
@visibleForTesting BuildSystem? buildSystem, @visibleForTesting BuildSystem? buildSystem,
}) async {} }) async {}
} }
...@@ -2610,6 +2610,18 @@ void main() { ...@@ -2610,6 +2610,18 @@ void main() {
, throwsToolExit(message: 'The "--platforms" argument is not supported', exitCode: 2)); , 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 { testUsingContext('create a plugin with android, delete then re-create folders', () async {
Cache.flutterRoot = '../..'; Cache.flutterRoot = '../..';
...@@ -3315,43 +3327,49 @@ void main() { ...@@ -3315,43 +3327,49 @@ void main() {
FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true), FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
}); });
testUsingContext('FFI plugins error android language', () async { for (final String template in <String>['package_ffi', 'plugin_ffi']) {
final CreateCommand command = CreateCommand(); testUsingContext('$template error android language', () async {
final CommandRunner<void> runner = createTestCommandRunner(command); final CreateCommand command = CreateCommand();
final List<String> args = <String>[ final CommandRunner<void> runner = createTestCommandRunner(command);
'create', final List<String> args = <String>[
'--no-pub', 'create',
'--template=plugin_ffi', '--no-pub',
'-a', '--template=$template',
'kotlin', '-a',
'--platforms=android', 'kotlin',
projectDir.path, if (template == 'plugin_ffi') '--platforms=android',
]; projectDir.path,
];
await expectLater(
runner.run(args), await expectLater(
throwsToolExit(message: 'The "android-language" option is not supported with the plugin_ffi template: the language will always be C or C++.'), 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>{
testUsingContext('FFI plugins error ios language', () async { FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true),
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,
];
await expectLater( testUsingContext('$template error ios language', () async {
runner.run(args), final CreateCommand command = CreateCommand();
throwsToolExit(message: 'The "ios-language" option is not supported with the plugin_ffi template: the language will always be C or C++.'), 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 $template template: the language will always be C or C++.'),
);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true),
});
}
testUsingContext('FFI plugins error web platform', () async { testUsingContext('FFI plugins error web platform', () async {
final CreateCommand command = CreateCommand(); final CreateCommand command = CreateCommand();
......
...@@ -71,10 +71,21 @@ void main() { ...@@ -71,10 +71,21 @@ void main() {
throwsA(isA<MissingDefineException>())); throwsA(isA<MissingDefineException>()));
}); });
const String emptyNativeAssets = '''
format-version:
- 1
- 0
- 0
native-assets: {}
''';
testWithoutContext('KernelSnapshot handles null result from kernel compilation', () async { testWithoutContext('KernelSnapshot handles null result from kernel compilation', () async {
fileSystem.file('.dart_tool/package_config.json') fileSystem.file('.dart_tool/package_config.json')
..createSync(recursive: true) ..createSync(recursive: true)
..writeAsStringSync('{"configVersion": 2, "packages":[]}'); ..writeAsStringSync('{"configVersion": 2, "packages":[]}');
androidEnvironment.buildDir.childFile('native_assets.yaml')
..createSync(recursive: true)
..writeAsStringSync(emptyNativeAssets);
final String build = androidEnvironment.buildDir.path; final String build = androidEnvironment.buildDir.path;
final String flutterPatchedSdkPath = artifacts.getArtifactPath( final String flutterPatchedSdkPath = artifacts.getArtifactPath(
Artifact.flutterPatchedSdkPath, Artifact.flutterPatchedSdkPath,
...@@ -102,6 +113,8 @@ void main() { ...@@ -102,6 +113,8 @@ void main() {
'$build/app.dill', '$build/app.dill',
'--depfile', '--depfile',
'$build/kernel_snapshot.d', '$build/kernel_snapshot.d',
'--native-assets',
'$build/native_assets.yaml',
'--verbosity=error', '--verbosity=error',
'file:///lib/main.dart', 'file:///lib/main.dart',
], exitCode: 1), ], exitCode: 1),
...@@ -115,6 +128,9 @@ void main() { ...@@ -115,6 +128,9 @@ void main() {
fileSystem.file('.dart_tool/package_config.json') fileSystem.file('.dart_tool/package_config.json')
..createSync(recursive: true) ..createSync(recursive: true)
..writeAsStringSync('{"configVersion": 2, "packages":[]}'); ..writeAsStringSync('{"configVersion": 2, "packages":[]}');
androidEnvironment.buildDir.childFile('native_assets.yaml')
..createSync(recursive: true)
..writeAsStringSync(emptyNativeAssets);
final String build = androidEnvironment.buildDir.path; final String build = androidEnvironment.buildDir.path;
final String flutterPatchedSdkPath = artifacts.getArtifactPath( final String flutterPatchedSdkPath = artifacts.getArtifactPath(
Artifact.flutterPatchedSdkPath, Artifact.flutterPatchedSdkPath,
...@@ -142,6 +158,8 @@ void main() { ...@@ -142,6 +158,8 @@ void main() {
'$build/app.dill', '$build/app.dill',
'--depfile', '--depfile',
'$build/kernel_snapshot.d', '$build/kernel_snapshot.d',
'--native-assets',
'$build/native_assets.yaml',
'--verbosity=error', '--verbosity=error',
'file:///lib/main.dart', 'file:///lib/main.dart',
], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n'), ], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n'),
...@@ -156,6 +174,9 @@ void main() { ...@@ -156,6 +174,9 @@ void main() {
fileSystem.file('.dart_tool/package_config.json') fileSystem.file('.dart_tool/package_config.json')
..createSync(recursive: true) ..createSync(recursive: true)
..writeAsStringSync('{"configVersion": 2, "packages":[]}'); ..writeAsStringSync('{"configVersion": 2, "packages":[]}');
androidEnvironment.buildDir.childFile('native_assets.yaml')
..createSync(recursive: true)
..writeAsStringSync(emptyNativeAssets);
final String build = androidEnvironment.buildDir.path; final String build = androidEnvironment.buildDir.path;
final String flutterPatchedSdkPath = artifacts.getArtifactPath( final String flutterPatchedSdkPath = artifacts.getArtifactPath(
Artifact.flutterPatchedSdkPath, Artifact.flutterPatchedSdkPath,
...@@ -183,6 +204,8 @@ void main() { ...@@ -183,6 +204,8 @@ void main() {
'$build/app.dill', '$build/app.dill',
'--depfile', '--depfile',
'$build/kernel_snapshot.d', '$build/kernel_snapshot.d',
'--native-assets',
'$build/native_assets.yaml',
'--verbosity=error', '--verbosity=error',
'file:///lib/main.dart', 'file:///lib/main.dart',
], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n'), ], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n'),
...@@ -198,6 +221,9 @@ void main() { ...@@ -198,6 +221,9 @@ void main() {
fileSystem.file('.dart_tool/package_config.json') fileSystem.file('.dart_tool/package_config.json')
..createSync(recursive: true) ..createSync(recursive: true)
..writeAsStringSync('{"configVersion": 2, "packages":[]}'); ..writeAsStringSync('{"configVersion": 2, "packages":[]}');
androidEnvironment.buildDir.childFile('native_assets.yaml')
..createSync(recursive: true)
..writeAsStringSync(emptyNativeAssets);
final String build = androidEnvironment.buildDir.path; final String build = androidEnvironment.buildDir.path;
final String flutterPatchedSdkPath = artifacts.getArtifactPath( final String flutterPatchedSdkPath = artifacts.getArtifactPath(
Artifact.flutterPatchedSdkPath, Artifact.flutterPatchedSdkPath,
...@@ -225,6 +251,8 @@ void main() { ...@@ -225,6 +251,8 @@ void main() {
'$build/app.dill', '$build/app.dill',
'--depfile', '--depfile',
'$build/kernel_snapshot.d', '$build/kernel_snapshot.d',
'--native-assets',
'$build/native_assets.yaml',
'--verbosity=error', '--verbosity=error',
'foo', 'foo',
'bar', 'bar',
...@@ -242,6 +270,9 @@ void main() { ...@@ -242,6 +270,9 @@ void main() {
fileSystem.file('.dart_tool/package_config.json') fileSystem.file('.dart_tool/package_config.json')
..createSync(recursive: true) ..createSync(recursive: true)
..writeAsStringSync('{"configVersion": 2, "packages":[]}'); ..writeAsStringSync('{"configVersion": 2, "packages":[]}');
androidEnvironment.buildDir.childFile('native_assets.yaml')
..createSync(recursive: true)
..writeAsStringSync(emptyNativeAssets);
final String build = androidEnvironment.buildDir.path; final String build = androidEnvironment.buildDir.path;
final String flutterPatchedSdkPath = artifacts.getArtifactPath( final String flutterPatchedSdkPath = artifacts.getArtifactPath(
Artifact.flutterPatchedSdkPath, Artifact.flutterPatchedSdkPath,
...@@ -268,6 +299,8 @@ void main() { ...@@ -268,6 +299,8 @@ void main() {
'--incremental', '--incremental',
'--initialize-from-dill', '--initialize-from-dill',
'$build/app.dill', '$build/app.dill',
'--native-assets',
'$build/native_assets.yaml',
'--verbosity=error', '--verbosity=error',
'file:///lib/main.dart', 'file:///lib/main.dart',
], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n'), ], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n'),
...@@ -284,6 +317,9 @@ void main() { ...@@ -284,6 +317,9 @@ void main() {
fileSystem.file('.dart_tool/package_config.json') fileSystem.file('.dart_tool/package_config.json')
..createSync(recursive: true) ..createSync(recursive: true)
..writeAsStringSync('{"configVersion": 2, "packages":[]}'); ..writeAsStringSync('{"configVersion": 2, "packages":[]}');
androidEnvironment.buildDir.childFile('native_assets.yaml')
..createSync(recursive: true)
..writeAsStringSync(emptyNativeAssets);
final String build = androidEnvironment.buildDir.path; final String build = androidEnvironment.buildDir.path;
final String flutterPatchedSdkPath = artifacts.getArtifactPath( final String flutterPatchedSdkPath = artifacts.getArtifactPath(
Artifact.flutterPatchedSdkPath, Artifact.flutterPatchedSdkPath,
...@@ -309,6 +345,8 @@ void main() { ...@@ -309,6 +345,8 @@ void main() {
'--incremental', '--incremental',
'--initialize-from-dill', '--initialize-from-dill',
'$build/app.dill', '$build/app.dill',
'--native-assets',
'$build/native_assets.yaml',
'--verbosity=error', '--verbosity=error',
'file:///lib/main.dart', 'file:///lib/main.dart',
], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n'), ], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n'),
...@@ -338,6 +376,9 @@ void main() { ...@@ -338,6 +376,9 @@ void main() {
fileSystem: fileSystem, fileSystem: fileSystem,
logger: logger, logger: logger,
); );
testEnvironment.buildDir.childFile('native_assets.yaml')
..createSync(recursive: true)
..writeAsStringSync(emptyNativeAssets);
final String build = testEnvironment.buildDir.path; final String build = testEnvironment.buildDir.path;
final String flutterPatchedSdkPath = artifacts.getArtifactPath( final String flutterPatchedSdkPath = artifacts.getArtifactPath(
Artifact.flutterPatchedSdkPath, Artifact.flutterPatchedSdkPath,
...@@ -365,6 +406,8 @@ void main() { ...@@ -365,6 +406,8 @@ void main() {
'--incremental', '--incremental',
'--initialize-from-dill', '--initialize-from-dill',
'$build/app.dill', '$build/app.dill',
'--native-assets',
'$build/native_assets.yaml',
'--verbosity=error', '--verbosity=error',
'file:///lib/main.dart', 'file:///lib/main.dart',
], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey /build/653e11a8e6908714056a57cd6b4f602a/app.dill 0\n'), ], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey /build/653e11a8e6908714056a57cd6b4f602a/app.dill 0\n'),
......
// 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',
]),
);
},
);
}
...@@ -435,4 +435,55 @@ void main() { ...@@ -435,4 +435,55 @@ void main() {
completer.complete(); completer.complete();
await output; 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, '');
});
} }
...@@ -644,6 +644,7 @@ class FakeBundleBuilder extends Fake implements BundleBuilder { ...@@ -644,6 +644,7 @@ class FakeBundleBuilder extends Fake implements BundleBuilder {
String? applicationKernelFilePath, String? applicationKernelFilePath,
String? depfilePath, String? depfilePath,
String? assetDirPath, String? assetDirPath,
Uri? nativeAssets,
@visibleForTesting BuildSystem? buildSystem @visibleForTesting BuildSystem? buildSystem
}) async {} }) async {}
} }
...@@ -702,7 +702,18 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler { ...@@ -702,7 +702,18 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler {
Future<CompilerOutput> Function(Uri mainUri, List<Uri>? invalidatedFiles)? onRecompile; Future<CompilerOutput> Function(Uri mainUri, List<Uri>? invalidatedFiles)? onRecompile;
@override @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) return onRecompile?.call(mainUri, invalidatedFiles)
?? Future<CompilerOutput>.value(const CompilerOutput('', 1, <Uri>[])); ?? Future<CompilerOutput>.value(const CompilerOutput('', 1, <Uri>[]));
} }
......
// 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;
}
...@@ -400,5 +400,13 @@ void main() { ...@@ -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);
});
}); });
} }
...@@ -5,10 +5,13 @@ ...@@ -5,10 +5,13 @@
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.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/flutter_project_metadata.dart';
import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/project.dart';
import '../src/common.dart'; import '../src/common.dart';
import '../src/context.dart';
import '../src/fakes.dart';
void main() { void main() {
late FileSystem fileSystem; late FileSystem fileSystem;
...@@ -184,4 +187,16 @@ migration: ...@@ -184,4 +187,16 @@ migration:
expect(logger.traceText, contains('The key `create_revision` was not found')); 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),
});
} }
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:flutter_tools/src/application_package.dart'; import 'package:flutter_tools/src/application_package.dart';
import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/artifacts.dart';
...@@ -16,12 +14,15 @@ import 'package:flutter_tools/src/build_system/targets/shader_compiler.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/compile.dart';
import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/device.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/project.dart';
import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:flutter_tools/src/resident_devtools_handler.dart'; import 'package:flutter_tools/src/resident_devtools_handler.dart';
import 'package:flutter_tools/src/resident_runner.dart'; import 'package:flutter_tools/src/resident_runner.dart';
import 'package:flutter_tools/src/run_hot.dart'; import 'package:flutter_tools/src/run_hot.dart';
import 'package:flutter_tools/src/vmservice.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:package_config/package_config.dart';
import 'package:test/fake.dart'; import 'package:test/fake.dart';
import 'package:vm_service/vm_service.dart' as vm_service; import 'package:vm_service/vm_service.dart' as vm_service;
...@@ -29,6 +30,7 @@ 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/common.dart';
import '../src/context.dart'; import '../src/context.dart';
import '../src/fakes.dart'; import '../src/fakes.dart';
import 'fake_native_assets_build_runner.dart';
void main() { void main() {
group('validateReloadReport', () { group('validateReloadReport', () {
...@@ -548,6 +550,134 @@ void main() { ...@@ -548,6 +550,134 @@ void main() {
expect(flutterDevice2.stoppedEchoingDeviceLog, true); 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 { class FakeDevFs extends Fake implements DevFS {
...@@ -580,6 +710,12 @@ 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. // Until we fix that, we have to also ignore related lints here.
// ignore: avoid_implementing_value_types // ignore: avoid_implementing_value_types
class FakeDevice extends Fake implements Device { class FakeDevice extends Fake implements Device {
FakeDevice({
TargetPlatform targetPlatform = TargetPlatform.tester,
}) : _targetPlatform = targetPlatform;
final TargetPlatform _targetPlatform;
bool disposed = false; bool disposed = false;
@override @override
...@@ -595,7 +731,7 @@ class FakeDevice extends Fake implements Device { ...@@ -595,7 +731,7 @@ class FakeDevice extends Fake implements Device {
bool supportsFlutterExit = true; bool supportsFlutterExit = true;
@override @override
Future<TargetPlatform> get targetPlatform async => TargetPlatform.tester; Future<TargetPlatform> get targetPlatform async => _targetPlatform;
@override @override
Future<String> get sdkNameAndVersion async => 'Tester'; Future<String> get sdkNameAndVersion async => 'Tester';
...@@ -658,6 +794,9 @@ class FakeFlutterDevice extends Fake implements FlutterDevice { ...@@ -658,6 +794,9 @@ class FakeFlutterDevice extends Fake implements FlutterDevice {
required List<Uri> invalidatedFiles, required List<Uri> invalidatedFiles,
required PackageConfig packageConfig, required PackageConfig packageConfig,
}) => updateDevFSReportCallback(); }) => updateDevFSReportCallback();
@override
TargetPlatform? get targetPlatform => device._targetPlatform;
} }
class TestFlutterDevice extends FlutterDevice { class TestFlutterDevice extends FlutterDevice {
......
// 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,
);
});
}
...@@ -99,6 +99,7 @@ class FakeBundleBuilder extends Fake implements BundleBuilder { ...@@ -99,6 +99,7 @@ class FakeBundleBuilder extends Fake implements BundleBuilder {
String? applicationKernelFilePath, String? applicationKernelFilePath,
String? depfilePath, String? depfilePath,
String? assetDirPath, String? assetDirPath,
Uri? nativeAssets,
@visibleForTesting BuildSystem? buildSystem @visibleForTesting BuildSystem? buildSystem
}) async { }) async {
final Directory assetDirectory = fileSystem final Directory assetDirectory = fileSystem
......
...@@ -18,6 +18,7 @@ import 'package:flutter_tools/src/base/io.dart' as io; ...@@ -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/logger.dart';
import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/build_info.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/scene_importer.dart';
import 'package:flutter_tools/src/build_system/targets/shader_compiler.dart'; import 'package:flutter_tools/src/build_system/targets/shader_compiler.dart';
import 'package:flutter_tools/src/compile.dart'; import 'package:flutter_tools/src/compile.dart';
...@@ -25,6 +26,7 @@ import 'package:flutter_tools/src/convert.dart'; ...@@ -25,6 +26,7 @@ import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/device_port_forwarder.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/globals.dart' as globals;
import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:flutter_tools/src/reporting/reporting.dart';
...@@ -34,6 +36,9 @@ import 'package:flutter_tools/src/run_cold.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/run_hot.dart';
import 'package:flutter_tools/src/version.dart'; import 'package:flutter_tools/src/version.dart';
import 'package:flutter_tools/src/vmservice.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:package_config/package_config.dart';
import 'package:test/fake.dart'; import 'package:test/fake.dart';
import 'package:vm_service/vm_service.dart' as vm_service; import 'package:vm_service/vm_service.dart' as vm_service;
...@@ -43,6 +48,7 @@ import '../src/context.dart'; ...@@ -43,6 +48,7 @@ import '../src/context.dart';
import '../src/fake_vm_services.dart'; import '../src/fake_vm_services.dart';
import '../src/fakes.dart'; import '../src/fakes.dart';
import '../src/testbed.dart'; import '../src/testbed.dart';
import 'fake_native_assets_build_runner.dart';
final vm_service.Event fakeUnpausedEvent = vm_service.Event( final vm_service.Event fakeUnpausedEvent = vm_service.Event(
kind: vm_service.EventKind.kResume, kind: vm_service.EventKind.kResume,
...@@ -2322,6 +2328,82 @@ flutter: ...@@ -2322,6 +2328,82 @@ flutter:
expect(flutterDevice.devFS!.hasSetAssetDirectory, true); expect(flutterDevice.devFS!.hasSetAssetDirectory, true);
expect(fakeVmServiceHost!.hasRemainingExpectations, false); 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] // This implements [dds.DartDevelopmentService], not the [DartDevelopmentService]
...@@ -2386,7 +2468,7 @@ class FakeFlutterDevice extends Fake implements FlutterDevice { ...@@ -2386,7 +2468,7 @@ class FakeFlutterDevice extends Fake implements FlutterDevice {
DevelopmentShaderCompiler get developmentShaderCompiler => const FakeShaderCompiler(); DevelopmentShaderCompiler get developmentShaderCompiler => const FakeShaderCompiler();
@override @override
TargetPlatform get targetPlatform => TargetPlatform.android; TargetPlatform targetPlatform = TargetPlatform.android;
@override @override
Stream<Uri?> get vmServiceUris => Stream<Uri?>.value(testUri); Stream<Uri?> get vmServiceUris => Stream<Uri?>.value(testUri);
...@@ -2521,6 +2603,7 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler { ...@@ -2521,6 +2603,7 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler {
bool suppressErrors = false, bool suppressErrors = false,
bool checkDartPluginRegistry = false, bool checkDartPluginRegistry = false,
File? dartPluginRegistrant, File? dartPluginRegistrant,
Uri? nativeAssetsYaml,
}) async { }) async {
didSuppressErrors = suppressErrors; didSuppressErrors = suppressErrors;
return nextOutput ?? const CompilerOutput('foo.dill', 0, <Uri>[]); return nextOutput ?? const CompilerOutput('foo.dill', 0, <Uri>[]);
......
...@@ -1444,6 +1444,7 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler { ...@@ -1444,6 +1444,7 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler {
bool suppressErrors = false, bool suppressErrors = false,
bool checkDartPluginRegistry = false, bool checkDartPluginRegistry = false,
File? dartPluginRegistrant, File? dartPluginRegistrant,
Uri? nativeAssetsYaml,
}) async { }) async {
return const CompilerOutput('foo.dill', 0, <Uri>[]); return const CompilerOutput('foo.dill', 0, <Uri>[]);
} }
......
...@@ -234,6 +234,7 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler { ...@@ -234,6 +234,7 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler {
bool suppressErrors = false, bool suppressErrors = false,
bool checkDartPluginRegistry = false, bool checkDartPluginRegistry = false,
File? dartPluginRegistrant, File? dartPluginRegistrant,
Uri? nativeAssetsYaml,
}) async { }) async {
if (compilerOutput != null) { if (compilerOutput != null) {
fileSystem!.file(compilerOutput!.outputFilename).createSync(recursive: true); fileSystem!.file(compilerOutput!.outputFilename).createSync(recursive: true);
......
...@@ -98,7 +98,6 @@ void main() { ...@@ -98,7 +98,6 @@ void main() {
artifacts: Artifacts.test(), artifacts: Artifacts.test(),
logger: BufferLogger.test(), logger: BufferLogger.test(),
flutterVersion: FakeFlutterVersion(), flutterVersion: FakeFlutterVersion(),
operatingSystemUtils: FakeOperatingSystemUtils(),
); );
logLines = <String>[]; logLines = <String>[];
device.getLogReader().logLines.listen(logLines.add); device.getLogReader().logLines.listen(logLines.add);
...@@ -213,7 +212,6 @@ FlutterTesterDevices setUpFlutterTesterDevices() { ...@@ -213,7 +212,6 @@ FlutterTesterDevices setUpFlutterTesterDevices() {
processManager: FakeProcessManager.any(), processManager: FakeProcessManager.any(),
fileSystem: MemoryFileSystem.test(), fileSystem: MemoryFileSystem.test(),
flutterVersion: FakeFlutterVersion(), flutterVersion: FakeFlutterVersion(),
operatingSystemUtils: FakeOperatingSystemUtils(),
); );
} }
......
...@@ -1190,6 +1190,7 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler { ...@@ -1190,6 +1190,7 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler {
bool suppressErrors = false, bool suppressErrors = false,
bool checkDartPluginRegistry = false, bool checkDartPluginRegistry = false,
File? dartPluginRegistrant, File? dartPluginRegistrant,
Uri? nativeAssetsYaml,
}) async { }) async {
return output; return output;
} }
......
...@@ -254,6 +254,7 @@ void main() { ...@@ -254,6 +254,7 @@ void main() {
'VERBOSE_SCRIPT_LOGGING': '1', 'VERBOSE_SCRIPT_LOGGING': '1',
'FLUTTER_BUILD_MODE': 'release', 'FLUTTER_BUILD_MODE': 'release',
'ACTION': 'install', 'ACTION': 'install',
'FLUTTER_BUILD_DIR': 'build',
// Skip bitcode stripping since we just checked that above. // Skip bitcode stripping since we just checked that above.
}, },
); );
......
...@@ -449,6 +449,7 @@ class TestFeatureFlags implements FeatureFlags { ...@@ -449,6 +449,7 @@ class TestFeatureFlags implements FeatureFlags {
this.areCustomDevicesEnabled = false, this.areCustomDevicesEnabled = false,
this.isFlutterWebWasmEnabled = false, this.isFlutterWebWasmEnabled = false,
this.isCliAnimationEnabled = true, this.isCliAnimationEnabled = true,
this.isNativeAssetsEnabled = false,
}); });
@override @override
...@@ -481,6 +482,9 @@ class TestFeatureFlags implements FeatureFlags { ...@@ -481,6 +482,9 @@ class TestFeatureFlags implements FeatureFlags {
@override @override
final bool isCliAnimationEnabled; final bool isCliAnimationEnabled;
@override
final bool isNativeAssetsEnabled;
@override @override
bool isEnabled(Feature feature) { bool isEnabled(Feature feature) {
switch (feature) { switch (feature) {
...@@ -502,6 +506,8 @@ class TestFeatureFlags implements FeatureFlags { ...@@ -502,6 +506,8 @@ class TestFeatureFlags implements FeatureFlags {
return areCustomDevicesEnabled; return areCustomDevicesEnabled;
case cliAnimation: case cliAnimation:
return isCliAnimationEnabled; return isCliAnimationEnabled;
case nativeAssets:
return isNativeAssetsEnabled;
} }
return false; return false;
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment