From aa36db1d29eb53b74dd1dda5a12e37800c628358 Mon Sep 17 00:00:00 2001 From: Daco Harkes Date: Sun, 10 Sep 2023 10:07:13 +0200 Subject: [PATCH] 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. --- .ci.yaml | 20 + TESTOWNERS | 2 + dev/devicelab/bin/tasks/module_test_ios.dart | 65 ++- .../bin/tasks/native_assets_ios.dart | 14 + .../tasks/native_assets_ios_simulator.dart | 31 ++ .../lib/tasks/native_assets_test.dart | 191 +++++++++ .../ios_host_app/flutterapp/lib/main | 6 + packages/flutter_tools/bin/macos_assemble.sh | 15 +- packages/flutter_tools/bin/xcode_backend.dart | 81 +++- .../flutter_tools/lib/src/build_info.dart | 2 + .../lib/src/build_system/targets/common.dart | 12 + .../build_system/targets/native_assets.dart | 183 +++++++++ .../lib/src/commands/build_ios_framework.dart | 22 + .../src/commands/build_macos_framework.dart | 21 + .../lib/src/commands/create.dart | 92 ++++- .../lib/src/commands/create_base.dart | 5 +- packages/flutter_tools/lib/src/compile.dart | 44 +- packages/flutter_tools/lib/src/features.dart | 14 + .../lib/src/flutter_device_manager.dart | 1 - .../lib/src/flutter_features.dart | 3 + .../lib/src/flutter_plugins.dart | 2 +- .../lib/src/flutter_project_metadata.dart | 18 + .../lib/src/ios/native_assets.dart | 171 ++++++++ .../lib/src/macos/native_assets.dart | 162 ++++++++ .../lib/src/macos/native_assets_host.dart | 141 +++++++ .../flutter_tools/lib/src/native_assets.dart | 378 +++++++++++++++++ packages/flutter_tools/lib/src/run_hot.dart | 15 + .../lib/src/test/test_compiler.dart | 24 ++ .../lib/src/tester/flutter_tester.dart | 12 +- packages/flutter_tools/pubspec.yaml | 7 +- .../templates/app/lib/main.dart.tmpl | 8 +- .../module/common/test/widget_test.dart.tmpl | 4 +- .../templates/package_ffi/.gitignore.tmpl | 30 ++ .../templates/package_ffi/.metadata.tmpl | 10 + .../templates/package_ffi/CHANGELOG.md.tmpl | 3 + .../templates/package_ffi/LICENSE.tmpl | 1 + .../templates/package_ffi/README.md.tmpl | 49 +++ .../package_ffi/analysis_options.yaml.tmpl | 4 + .../templates/package_ffi/build.dart.tmpl | 24 ++ .../templates/package_ffi/ffigen.yaml.tmpl | 20 + .../package_ffi/lib/projectName.dart.tmpl | 108 +++++ .../projectName_bindings_generated.dart.tmpl | 30 ++ .../templates/package_ffi/pubspec.yaml.tmpl | 19 + .../package_ffi/src.tmpl/projectName.c.tmpl | 29 ++ .../package_ffi/src.tmpl/projectName.h.tmpl | 30 ++ .../test/projectName_test.dart.tmpl | 14 + .../templates/plugin_shared/.metadata.tmpl | 3 + .../templates/plugin_shared/pubspec.yaml.tmpl | 8 +- .../templates/template_manifest.json | 15 + .../hermetic/create_usage_test.dart | 31 ++ .../permeable/build_bundle_test.dart | 1 + .../commands.shard/permeable/create_test.dart | 86 ++-- .../build_system/targets/common_test.dart | 43 ++ .../targets/native_assets_test.dart | 140 +++++++ .../general.shard/compile_batch_test.dart | 51 +++ .../custom_devices/custom_device_test.dart | 1 + .../test/general.shard/devfs_test.dart | 13 +- .../fake_native_assets_build_runner.dart | 91 +++++ .../test/general.shard/features_test.dart | 8 + .../flutter_project_metadata_test.dart | 15 + .../test/general.shard/hot_test.dart | 145 ++++++- .../general.shard/ios/native_assets_test.dart | 274 +++++++++++++ .../macos/native_assets_test.dart | 385 ++++++++++++++++++ .../general.shard/preview_device_test.dart | 1 + .../general.shard/resident_runner_test.dart | 85 +++- .../resident_web_runner_test.dart | 1 + .../test/test_compiler_test.dart | 1 + .../tester/flutter_tester_test.dart | 2 - .../general.shard/web/devfs_web_test.dart | 1 + .../ios_content_validation_test.dart | 1 + .../integration.shard/native_assets_test.dart | 360 ++++++++++++++++ .../overall_experience_test.dart | 301 +------------- .../transition_test_utils.dart | 338 +++++++++++++++ packages/flutter_tools/test/src/fakes.dart | 6 + 74 files changed, 4132 insertions(+), 412 deletions(-) create mode 100644 dev/devicelab/bin/tasks/native_assets_ios.dart create mode 100644 dev/devicelab/bin/tasks/native_assets_ios_simulator.dart create mode 100644 dev/devicelab/lib/tasks/native_assets_test.dart create mode 100644 packages/flutter_tools/lib/src/build_system/targets/native_assets.dart create mode 100644 packages/flutter_tools/lib/src/ios/native_assets.dart create mode 100644 packages/flutter_tools/lib/src/macos/native_assets.dart create mode 100644 packages/flutter_tools/lib/src/macos/native_assets_host.dart create mode 100644 packages/flutter_tools/lib/src/native_assets.dart create mode 100644 packages/flutter_tools/templates/package_ffi/.gitignore.tmpl create mode 100644 packages/flutter_tools/templates/package_ffi/.metadata.tmpl create mode 100644 packages/flutter_tools/templates/package_ffi/CHANGELOG.md.tmpl create mode 100644 packages/flutter_tools/templates/package_ffi/LICENSE.tmpl create mode 100644 packages/flutter_tools/templates/package_ffi/README.md.tmpl create mode 100644 packages/flutter_tools/templates/package_ffi/analysis_options.yaml.tmpl create mode 100644 packages/flutter_tools/templates/package_ffi/build.dart.tmpl create mode 100644 packages/flutter_tools/templates/package_ffi/ffigen.yaml.tmpl create mode 100644 packages/flutter_tools/templates/package_ffi/lib/projectName.dart.tmpl create mode 100644 packages/flutter_tools/templates/package_ffi/lib/projectName_bindings_generated.dart.tmpl create mode 100644 packages/flutter_tools/templates/package_ffi/pubspec.yaml.tmpl create mode 100644 packages/flutter_tools/templates/package_ffi/src.tmpl/projectName.c.tmpl create mode 100644 packages/flutter_tools/templates/package_ffi/src.tmpl/projectName.h.tmpl create mode 100644 packages/flutter_tools/templates/package_ffi/test/projectName_test.dart.tmpl create mode 100644 packages/flutter_tools/test/general.shard/build_system/targets/native_assets_test.dart create mode 100644 packages/flutter_tools/test/general.shard/fake_native_assets_build_runner.dart create mode 100644 packages/flutter_tools/test/general.shard/ios/native_assets_test.dart create mode 100644 packages/flutter_tools/test/general.shard/macos/native_assets_test.dart create mode 100644 packages/flutter_tools/test/integration.shard/native_assets_test.dart create mode 100644 packages/flutter_tools/test/integration.shard/transition_test_utils.dart diff --git a/.ci.yaml b/.ci.yaml index ba31dfac145..07fd6f54df8 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -4069,6 +4069,26 @@ targets: ["devicelab", "ios", "mac"] task_name: microbenchmarks_ios + - name: Mac_ios native_assets_ios_simulator + recipe: devicelab/devicelab_drone + presubmit: false + bringup: true # TODO(dacoharkes): Set to false in follow up PR and check that test works on CI. + timeout: 60 + properties: + tags: > + ["devicelab", "ios", "mac"] + task_name: native_assets_ios_simulator + + - name: Mac_ios native_assets_ios + recipe: devicelab/devicelab_drone + presubmit: false + bringup: true # TODO(dacoharkes): Set to false in follow up PR and check that test works on CI. + timeout: 60 + properties: + tags: > + ["devicelab", "ios", "mac"] + task_name: native_assets_ios + - name: Mac_ios native_platform_view_ui_tests_ios recipe: devicelab/devicelab_drone presubmit: false diff --git a/TESTOWNERS b/TESTOWNERS index 7da41d661c6..635215f5230 100644 --- a/TESTOWNERS +++ b/TESTOWNERS @@ -199,6 +199,8 @@ /dev/devicelab/bin/tasks/large_image_changer_perf_ios.dart @zanderso @flutter/engine /dev/devicelab/bin/tasks/macos_chrome_dev_mode.dart @zanderso @flutter/tool /dev/devicelab/bin/tasks/microbenchmarks_ios.dart @cyanglaz @flutter/engine +/dev/devicelab/bin/tasks/native_assets_ios_simulator.dart @dacoharkes @flutter/ios +/dev/devicelab/bin/tasks/native_assets_ios.dart @dacoharkes @flutter/ios /dev/devicelab/bin/tasks/native_platform_view_ui_tests_ios.dart @hellohuanlin @flutter/ios /dev/devicelab/bin/tasks/new_gallery_ios__transition_perf.dart @zanderso @flutter/engine /dev/devicelab/bin/tasks/new_gallery_skia_ios__transition_perf.dart @zanderso @flutter/engine diff --git a/dev/devicelab/bin/tasks/module_test_ios.dart b/dev/devicelab/bin/tasks/module_test_ios.dart index a213bbee26f..cd6218e4517 100644 --- a/dev/devicelab/bin/tasks/module_test_ios.dart +++ b/dev/devicelab/bin/tasks/module_test_ios.dart @@ -59,6 +59,36 @@ Future main() async { final File marquee = File(path.join(flutterModuleLibSource.path, 'marquee')); marquee.copySync(path.join(flutterModuleLibDestination.path, 'marquee.dart')); + section('Create package with native assets'); + + await flutter( + 'config', + options: ['--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: ['get'], + ); + }); + section('Build ephemeral host app in release mode without CocoaPods'); await inDirectory(projectDir, () async { @@ -162,10 +192,8 @@ Future main() async { section('Add plugins'); - final File pubspec = File(path.join(projectDir.path, 'pubspec.yaml')); - String content = await pubspec.readAsString(); content = content.replaceFirst( - '\ndependencies:\n', + 'dependencies:\n', // One framework, one Dart-only, one that does not support iOS, and one with a resource bundle. ''' dependencies: @@ -221,6 +249,11 @@ dependencies: // Dart-only, no embedded framework. checkDirectoryNotExists(path.join(ephemeralIOSHostApp.path, 'Frameworks', '$dartPluginName.framework')); + // Native assets embedded, no embedded framework. + const String libFfiPackageDylib = 'lib$ffiPackageName.dylib'; + checkFileExists(path.join(ephemeralIOSHostApp.path, 'Frameworks', libFfiPackageDylib)); + checkDirectoryNotExists(path.join(ephemeralIOSHostApp.path, 'Frameworks', '$ffiPackageName.framework')); + section('Clean and pub get module'); await inDirectory(projectDir, () async { @@ -350,6 +383,11 @@ end 'isolate_snapshot_data', )); + checkFileExists(path.join( + hostFrameworksDirectory, + libFfiPackageDylib, + )); + section('Check the NOTICE file is correct'); final String licenseFilePath = path.join( @@ -449,6 +487,13 @@ end throw TaskResult.failure('Unexpected armv7 architecture slice in $builtAppBinary'); } + // Check native assets are bundled. + checkFileExists(path.join( + archivedAppPath, + 'Frameworks', + libFfiPackageDylib, + )); + // The host app example builds plugins statically, url_launcher_ios.framework // should not exist. checkDirectoryNotExists(path.join( @@ -685,3 +730,17 @@ class $dartPluginClass { // Remove the native plugin code. await Directory(path.join(pluginDir, 'ios')).delete(recursive: true); } + +Future _createFfiPackage(String name, Directory parent) async { + await inDirectory(parent, () async { + await flutter( + 'create', + options: [ + '--org', + 'io.flutter.devicelab', + '--template=package_ffi', + name, + ], + ); + }); +} diff --git a/dev/devicelab/bin/tasks/native_assets_ios.dart b/dev/devicelab/bin/tasks/native_assets_ios.dart new file mode 100644 index 00000000000..9db75bf3c86 --- /dev/null +++ b/dev/devicelab/bin/tasks/native_assets_ios.dart @@ -0,0 +1,14 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_devicelab/framework/devices.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/tasks/native_assets_test.dart'; + +Future main() async { + await task(() async { + deviceOperatingSystem = DeviceOperatingSystem.ios; + return createNativeAssetsTest()(); + }); +} diff --git a/dev/devicelab/bin/tasks/native_assets_ios_simulator.dart b/dev/devicelab/bin/tasks/native_assets_ios_simulator.dart new file mode 100644 index 00000000000..73579452434 --- /dev/null +++ b/dev/devicelab/bin/tasks/native_assets_ios_simulator.dart @@ -0,0 +1,31 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_devicelab/framework/devices.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/framework/ios.dart'; +import 'package:flutter_devicelab/framework/task_result.dart'; +import 'package:flutter_devicelab/tasks/native_assets_test.dart'; + +Future 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); + }); +} diff --git a/dev/devicelab/lib/tasks/native_assets_test.dart b/dev/devicelab/lib/tasks/native_assets_test.dart new file mode 100644 index 00000000000..7d93272e7eb --- /dev/null +++ b/dev/devicelab/lib/tasks/native_assets_test.dart @@ -0,0 +1,191 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import '../framework/devices.dart'; +import '../framework/framework.dart'; +import '../framework/task_result.dart'; +import '../framework/utils.dart'; + +const String _packageName = 'package_with_native_assets'; + +const List _buildModes = [ + '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 options = [ + '-d', + deviceIdOverride!, + '--no-android-gradle-daemon', + '--no-publish-port', + '--verbose', + '--uninstall-first', + '--$buildMode', + ]; + int transitionCount = 0; + bool done = false; + + await inDirectory(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 runFlutter({ + required List options, + required void Function(String, Process) onLine, +}) async { + final Process process = await startFlutter( + 'run', + options: options, + ); + + final Completer stdoutDone = Completer(); + final Completer stderrDone = Completer(); + process.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen((String line) { + onLine(line, process); + print('stdout: $line'); + }, onDone: stdoutDone.complete); + + process.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen( + (String line) => print('stderr: $line'), + onDone: stderrDone.complete, + ); + + await Future.wait(>[stdoutDone.future, stderrDone.future]); + final int exitCode = await process.exitCode; + return exitCode; +} + +final String _flutterBin = path.join(flutterDirectory.path, 'bin', 'flutter'); + +Future enableNativeAssets() async { + print('Enabling configs for native assets...'); + final int configResult = await exec( + _flutterBin, + [ + 'config', + '-v', + '--enable-native-assets', + ], + canFail: true); + if (configResult != 0) { + print('Failed to enable configuration, tasks may not run.'); + } +} + +Future createTestProject( + String packageName, + Directory tempDirectory, +) async { + final int createResult = await exec( + _flutterBin, + [ + '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 inTempDir(Future Function(Directory tempDirectory) fun) async { + final Directory tempDirectory = dir(Directory.systemTemp.createTempSync().resolveSymbolicLinksSync()); + try { + return await fun(tempDirectory); + } finally { + tempDirectory.deleteSync(recursive: true); + } +} diff --git a/dev/integration_tests/ios_host_app/flutterapp/lib/main b/dev/integration_tests/ios_host_app/flutterapp/lib/main index 2f5c09d11b7..78ea99b18b8 100644 --- a/dev/integration_tests/ios_host_app/flutterapp/lib/main +++ b/dev/integration_tests/ios_host_app/flutterapp/lib/main @@ -7,6 +7,7 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:ffi_package/ffi_package.dart'; import 'marquee.dart'; @@ -116,10 +117,15 @@ class _MyHomePageState extends State { // button on the Flutter page has been tapped. int _counter = 0; + late int sumResult; + late Future sumAsyncResult; + @override void initState() { super.initState(); _platform.setMessageHandler(_handlePlatformIncrement); + sumResult = sum(1, 2); + sumAsyncResult = sumAsync(3, 4); } /// Directly increments our internal counter and rebuilds the UI. diff --git a/packages/flutter_tools/bin/macos_assemble.sh b/packages/flutter_tools/bin/macos_assemble.sh index 50f85fd227c..3f21624e7c9 100755 --- a/packages/flutter_tools/bin/macos_assemble.sh +++ b/packages/flutter_tools/bin/macos_assemble.sh @@ -144,8 +144,8 @@ BuildApp() { RunCommand "${flutter_args[@]}" } -# Adds the App.framework as an embedded binary and the flutter_assets as -# resources. +# Adds the App.framework as an embedded binary, the flutter_assets as +# resources, and the native assets. EmbedFrameworks() { # Embed App.framework from Flutter into the app (after creating the Frameworks directory # if it doesn't already exist). @@ -164,6 +164,17 @@ EmbedFrameworks() { RunCommand codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${xcode_frameworks_dir}/App.framework/App" RunCommand codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${xcode_frameworks_dir}/FlutterMacOS.framework/FlutterMacOS" fi + + # Copy the native assets. These do not have to be codesigned here because, + # they are already codesigned in buildNativeAssetsMacOS. + local project_path="${SOURCE_ROOT}/.." + if [[ -n "$FLUTTER_APPLICATION_PATH" ]]; then + project_path="${FLUTTER_APPLICATION_PATH}" + fi + local native_assets_path="${project_path}/${FLUTTER_BUILD_DIR}/native_assets/macos/" + if [[ -d "$native_assets_path" ]]; then + RunCommand rsync -av --filter "- .DS_Store" --filter "- native_assets.yaml" "${native_assets_path}" "${xcode_frameworks_dir}" + fi } # Main entry point. diff --git a/packages/flutter_tools/bin/xcode_backend.dart b/packages/flutter_tools/bin/xcode_backend.dart index 15da6b525ed..87d1c22313a 100644 --- a/packages/flutter_tools/bin/xcode_backend.dart +++ b/packages/flutter_tools/bin/xcode_backend.dart @@ -171,6 +171,32 @@ class Context { exitApp(-1); } + /// Copies all files from [source] to [destination]. + /// + /// Does not copy `.DS_Store`. + /// + /// If [delete], delete extraneous files from [destination]. + void runRsync( + String source, + String destination, { + List extraArgs = const [], + bool delete = false, + }) { + runSync( + 'rsync', + [ + '-8', // Avoid mangling filenames with encodings that do not match the current locale. + '-av', + if (delete) '--delete', + '--filter', + '- .DS_Store', + ...extraArgs, + source, + destination, + ], + ); + } + // Adds the App.framework as an embedded binary and the flutter_assets as // resources. void embedFlutterFrameworks() { @@ -185,33 +211,46 @@ class Context { xcodeFrameworksDir, ] ); - runSync( - 'rsync', - [ - '-8', // Avoid mangling filenames with encodings that do not match the current locale. - '-av', - '--delete', - '--filter', - '- .DS_Store', - '${environment['BUILT_PRODUCTS_DIR']}/App.framework', - xcodeFrameworksDir, - ], + runRsync( + delete: true, + '${environment['BUILT_PRODUCTS_DIR']}/App.framework', + xcodeFrameworksDir, ); // Embed the actual Flutter.framework that the Flutter app expects to run against, // which could be a local build or an arch/type specific build. - runSync( - 'rsync', - [ - '-av', - '--delete', - '--filter', - '- .DS_Store', - '${environment['BUILT_PRODUCTS_DIR']}/Flutter.framework', - '$xcodeFrameworksDir/', - ], + runRsync( + delete: true, + '${environment['BUILT_PRODUCTS_DIR']}/Flutter.framework', + '$xcodeFrameworksDir/', ); + // Copy the native assets. These do not have to be codesigned here because, + // they are already codesigned in buildNativeAssetsMacOS. + final String sourceRoot = environment['SOURCE_ROOT'] ?? ''; + String projectPath = '$sourceRoot/..'; + if (environment['FLUTTER_APPLICATION_PATH'] != null) { + projectPath = environment['FLUTTER_APPLICATION_PATH']!; + } + final String flutterBuildDir = environment['FLUTTER_BUILD_DIR']!; + final String nativeAssetsPath = '$projectPath/$flutterBuildDir/native_assets/ios/'; + final bool verbose = (environment['VERBOSE_SCRIPT_LOGGING'] ?? '').isNotEmpty; + if (Directory(nativeAssetsPath).existsSync()) { + if (verbose) { + print('♦ Copying native assets from $nativeAssetsPath.'); + } + runRsync( + extraArgs: [ + '--filter', + '- native_assets.yaml', + ], + nativeAssetsPath, + xcodeFrameworksDir, + ); + } else if (verbose) { + print("♦ No native assets to bundle. $nativeAssetsPath doesn't exist."); + } + addVmServiceBonjourService(); } diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart index 7c3455d1163..fea9da5c415 100644 --- a/packages/flutter_tools/lib/src/build_info.dart +++ b/packages/flutter_tools/lib/src/build_info.dart @@ -777,6 +777,8 @@ TargetPlatform getTargetPlatformForName(String platform) { return TargetPlatform.windows_x64; case 'web-javascript': return TargetPlatform.web_javascript; + case 'flutter-tester': + return TargetPlatform.tester; } throw Exception('Unsupported platform name "$platform"'); } diff --git a/packages/flutter_tools/lib/src/build_system/targets/common.dart b/packages/flutter_tools/lib/src/build_system/targets/common.dart index 039bb5a7fdd..951febd5b47 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/common.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/common.dart @@ -6,6 +6,7 @@ import 'package:package_config/package_config.dart'; import '../../artifacts.dart'; import '../../base/build.dart'; +import '../../base/common.dart'; import '../../base/file_system.dart'; import '../../base/io.dart'; import '../../build_info.dart'; @@ -19,6 +20,7 @@ import 'assets.dart'; import 'dart_plugin_registrant.dart'; import 'icon_tree_shaker.dart'; import 'localizations.dart'; +import 'native_assets.dart'; import 'shader_compiler.dart'; /// Copies the pre-built flutter bundle. @@ -125,6 +127,7 @@ class KernelSnapshot extends Target { @override List get inputs => const [ + Source.pattern('{BUILD_DIR}/native_assets.yaml'), Source.pattern('{PROJECT_DIR}/.dart_tool/package_config_subset'), Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/common.dart'), Source.artifact(Artifact.platformKernelDill), @@ -142,6 +145,7 @@ class KernelSnapshot extends Target { @override List get dependencies => const [ + NativeAssets(), GenerateLocalizationsTarget(), DartPluginRegistrantTarget(), ]; @@ -178,6 +182,13 @@ class KernelSnapshot extends Target { final List? fileSystemRoots = environment.defines[kFileSystemRoots]?.split(','); final String? fileSystemScheme = environment.defines[kFileSystemScheme]; + final File nativeAssetsFile = environment.buildDir.childFile('native_assets.yaml'); + final String nativeAssets = nativeAssetsFile.path; + if (!await nativeAssetsFile.exists()) { + throwToolExit("$nativeAssets doesn't exist."); + } + environment.logger.printTrace('Embedding native assets mapping $nativeAssets in kernel.'); + TargetModel targetModel = TargetModel.flutter; if (targetPlatform == TargetPlatform.fuchsia_x64 || targetPlatform == TargetPlatform.fuchsia_arm64) { @@ -251,6 +262,7 @@ class KernelSnapshot extends Target { buildDir: environment.buildDir, targetOS: targetOS, checkDartPluginRegistry: environment.generateDartPluginRegistry, + nativeAssets: nativeAssets, ); if (output == null || output.errorCount != 0) { throw Exception(); diff --git a/packages/flutter_tools/lib/src/build_system/targets/native_assets.dart b/packages/flutter_tools/lib/src/build_system/targets/native_assets.dart new file mode 100644 index 00000000000..073b5863833 --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/targets/native_assets.dart @@ -0,0 +1,183 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:meta/meta.dart'; +import 'package:native_assets_cli/native_assets_cli.dart' show Asset; + +import '../../base/common.dart'; +import '../../base/file_system.dart'; +import '../../base/platform.dart'; +import '../../build_info.dart'; +import '../../ios/native_assets.dart'; +import '../../macos/native_assets.dart'; +import '../../macos/xcode.dart'; +import '../../native_assets.dart'; +import '../build_system.dart'; +import '../depfile.dart'; +import '../exceptions.dart'; +import 'common.dart'; + +/// Builds the right native assets for a Flutter app. +/// +/// The build mode and target architecture can be changed from the +/// native build project (Xcode etc.), so only `flutter assemble` has the +/// information about build-mode and target architecture. +/// Invocations of flutter_tools other than `flutter assemble` are dry runs. +/// +/// This step needs to be consistent with the dry run invocations in `flutter +/// run`s so that the kernel mapping of asset id to dylib lines up after hot +/// restart. +/// +/// [KernelSnapshot] depends on this target. We produce a native_assets.yaml +/// here, and embed that mapping inside the kernel snapshot. +/// +/// The build always produces a valid native_assets.yaml and a native_assets.d +/// even if there are no native assets. This way the caching logic won't try to +/// rebuild. +class NativeAssets extends Target { + const NativeAssets({ + @visibleForTesting NativeAssetsBuildRunner? buildRunner, + }) : _buildRunner = buildRunner; + + final NativeAssetsBuildRunner? _buildRunner; + + @override + Future 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 dependencies; + switch (targetPlatform) { + case TargetPlatform.ios: + final String? iosArchsEnvironment = environment.defines[kIosArchs]; + if (iosArchsEnvironment == null) { + throw MissingDefineException(kIosArchs, name); + } + final List 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 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([], environment.buildDir.uri, fileSystem); + dependencies = []; + } + 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([], environment.buildDir.uri, fileSystem); + dependencies = []; + } + + final File nativeAssetsFile = environment.buildDir.childFile('native_assets.yaml'); + final Depfile depfile = Depfile( + [ + for (final Uri dependency in dependencies) fileSystem.file(dependency), + ], + [ + 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 get depfiles => [ + 'native_assets.d', + ]; + + @override + List get dependencies => []; + + @override + List get inputs => const [ + 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 get outputs => const [ + Source.pattern('{BUILD_DIR}/native_assets.yaml'), + ]; +} diff --git a/packages/flutter_tools/lib/src/commands/build_ios_framework.dart b/packages/flutter_tools/lib/src/commands/build_ios_framework.dart index ea3c50bbfa9..66eea554289 100644 --- a/packages/flutter_tools/lib/src/commands/build_ios_framework.dart +++ b/packages/flutter_tools/lib/src/commands/build_ios_framework.dart @@ -270,6 +270,28 @@ class BuildIOSFrameworkCommand extends BuildFrameworkCommand { final Status status = globals.logger.startProgress( ' └─Moving to ${globals.fs.path.relative(modeDirectory.path)}'); + + // Copy the native assets. The native assets have already been signed in + // buildNativeAssetsMacOS. + final Directory nativeAssetsDirectory = globals.fs + .directory(getBuildDirectory()) + .childDirectory('native_assets/ios/'); + if (await nativeAssetsDirectory.exists()) { + final ProcessResult rsyncResult = await globals.processManager.run([ + 'rsync', + '-av', + '--filter', + '- .DS_Store', + '--filter', + '- native_assets.yaml', + nativeAssetsDirectory.path, + modeDirectory.path, + ]); + if (rsyncResult.exitCode != 0) { + throwToolExit('Failed to copy native assets:\n${rsyncResult.stderr}'); + } + } + try { // Delete the intermediaries since they would have been copied into our // output frameworks. diff --git a/packages/flutter_tools/lib/src/commands/build_macos_framework.dart b/packages/flutter_tools/lib/src/commands/build_macos_framework.dart index 77a02511dc0..19c1e17ed50 100644 --- a/packages/flutter_tools/lib/src/commands/build_macos_framework.dart +++ b/packages/flutter_tools/lib/src/commands/build_macos_framework.dart @@ -6,6 +6,7 @@ import 'package:meta/meta.dart'; import '../base/common.dart'; import '../base/file_system.dart'; +import '../base/io.dart'; import '../base/logger.dart'; import '../base/process.dart'; import '../base/utils.dart'; @@ -96,6 +97,26 @@ class BuildMacOSFrameworkCommand extends BuildFrameworkCommand { globals.logger.printStatus(' └─Moving to ${globals.fs.path.relative(modeDirectory.path)}'); + // Copy the native assets. + final Directory nativeAssetsDirectory = globals.fs + .directory(getBuildDirectory()) + .childDirectory('native_assets/macos/'); + if (await nativeAssetsDirectory.exists()) { + final ProcessResult rsyncResult = await globals.processManager.run([ + 'rsync', + '-av', + '--filter', + '- .DS_Store', + '--filter', + '- native_assets.yaml', + nativeAssetsDirectory.path, + modeDirectory.path, + ]); + if (rsyncResult.exitCode != 0) { + throwToolExit('Failed to copy native assets:\n${rsyncResult.stderr}'); + } + } + // Delete the intermediaries since they would have been copied into our // output frameworks. if (buildOutput.existsSync()) { diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart index 66a9e1127e6..0c6ca3a02cd 100644 --- a/packages/flutter_tools/lib/src/commands/create.dart +++ b/packages/flutter_tools/lib/src/commands/create.dart @@ -36,10 +36,11 @@ class CreateCommand extends CreateBase { argParser.addOption( 'template', abbr: 't', - allowed: FlutterProjectType.values.map((FlutterProjectType e) => e.cliName), + allowed: FlutterProjectType.enabledValues + .map((FlutterProjectType e) => e.cliName), help: 'Specify the type of project to create.', valueHelp: 'type', - allowedHelp: CliEnum.allowedHelp(FlutterProjectType.values), + allowedHelp: CliEnum.allowedHelp(FlutterProjectType.enabledValues), ); argParser.addOption( 'sample', @@ -206,12 +207,14 @@ class CreateCommand extends CreateBase { final FlutterProjectType template = _getProjectType(projectDir); final bool generateModule = template == FlutterProjectType.module; final bool generateMethodChannelsPlugin = template == FlutterProjectType.plugin; + final bool generateFfiPackage = template == FlutterProjectType.packageFfi; final bool generateFfiPlugin = template == FlutterProjectType.pluginFfi; + final bool generateFfi = generateFfiPlugin || generateFfiPackage; final bool generatePackage = template == FlutterProjectType.package; final List platforms = stringsArg('platforms'); // `--platforms` does not support module or package. - if (argResults!.wasParsed('platforms') && (generateModule || generatePackage)) { + if (argResults!.wasParsed('platforms') && (generateModule || generatePackage || generateFfiPackage)) { final String template = generateModule ? 'module' : 'package'; throwToolExit( 'The "--platforms" argument is not supported in $template template.', @@ -225,15 +228,15 @@ class CreateCommand extends CreateBase { 'The web platform is not supported in plugin_ffi template.', exitCode: 2, ); - } else if (generateFfiPlugin && argResults!.wasParsed('ios-language')) { + } else if (generateFfi && argResults!.wasParsed('ios-language')) { throwToolExit( - 'The "ios-language" option is not supported with the plugin_ffi ' + 'The "ios-language" option is not supported with the ${template.cliName} ' 'template: the language will always be C or C++.', exitCode: 2, ); - } else if (generateFfiPlugin && argResults!.wasParsed('android-language')) { + } else if (generateFfi && argResults!.wasParsed('android-language')) { throwToolExit( - 'The "android-language" option is not supported with the plugin_ffi ' + 'The "android-language" option is not supported with the ${template.cliName} ' 'template: the language will always be C or C++.', exitCode: 2, ); @@ -306,6 +309,7 @@ class CreateCommand extends CreateBase { flutterRoot: flutterRoot, withPlatformChannelPluginHook: generateMethodChannelsPlugin, withFfiPluginHook: generateFfiPlugin, + withFfiPackage: generateFfiPackage, withEmptyMain: emptyArgument, androidLanguage: stringArg('android-language'), iosLanguage: stringArg('ios-language'), @@ -393,6 +397,15 @@ class CreateCommand extends CreateBase { projectType: template, ); pubContext = PubContext.createPlugin; + case FlutterProjectType.packageFfi: + generatedFileCount += await _generateFfiPackage( + relativeDir, + templateContext, + overwrite: overwrite, + printStatusWhenWriting: !creatingNewProject, + projectType: template, + ); + pubContext = PubContext.createPackage; } if (boolArg('pub')) { @@ -403,14 +416,21 @@ class CreateCommand extends CreateBase { offline: boolArg('offline'), outputMode: PubOutputMode.summaryOnly, ); - await project.ensureReadyForPlatformSpecificTooling( - androidPlatform: includeAndroid, - iosPlatform: includeIos, - linuxPlatform: includeLinux, - macOSPlatform: includeMacos, - windowsPlatform: includeWindows, - webPlatform: includeWeb, - ); + // Setting `includeIos` etc to false as with FlutterProjectType.package + // causes the example sub directory to not get os sub directories. + // This will lead to `flutter build ios` to fail in the example. + // TODO(dacoharkes): Uncouple the app and parent project platforms. https://github.com/flutter/flutter/issues/133874 + // Then this if can be removed. + if (!generateFfiPackage) { + await project.ensureReadyForPlatformSpecificTooling( + androidPlatform: includeAndroid, + iosPlatform: includeIos, + linuxPlatform: includeLinux, + macOSPlatform: includeMacos, + windowsPlatform: includeWindows, + webPlatform: includeWeb, + ); + } } if (sampleCode != null) { _applySample(relativeDir, sampleCode); @@ -663,6 +683,48 @@ Your $application code is in $relativeAppMain. return generatedCount; } + Future _generateFfiPackage( + Directory directory, + Map 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( + [ + '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( + ['app'], + project.example.directory, + templateContext, + overwrite: overwrite, + pluginExampleApp: true, + printStatusWhenWriting: printStatusWhenWriting, + projectType: projectType, + ); + return generatedCount; + } + // Takes an application template and replaces the main.dart with one from the // documentation website in sampleCode. Returns the difference in the number // of files after applying the sample, since it also deletes the application's diff --git a/packages/flutter_tools/lib/src/commands/create_base.dart b/packages/flutter_tools/lib/src/commands/create_base.dart index 4f801acc9b7..2ad3f6b7a5d 100644 --- a/packages/flutter_tools/lib/src/commands/create_base.dart +++ b/packages/flutter_tools/lib/src/commands/create_base.dart @@ -352,6 +352,7 @@ abstract class CreateBase extends FlutterCommand { String? gradleVersion, bool withPlatformChannelPluginHook = false, bool withFfiPluginHook = false, + bool withFfiPackage = false, bool withEmptyMain = false, bool ios = false, bool android = false, @@ -399,9 +400,11 @@ abstract class CreateBase extends FlutterCommand { 'pluginClassCapitalSnakeCase': pluginClassCapitalSnakeCase, 'pluginDartClass': pluginDartClass, 'pluginProjectUUID': const Uuid().v4().toUpperCase(), + 'withFfi': withFfiPluginHook || withFfiPackage, + 'withFfiPackage': withFfiPackage, 'withFfiPluginHook': withFfiPluginHook, 'withPlatformChannelPluginHook': withPlatformChannelPluginHook, - 'withPluginHook': withFfiPluginHook || withPlatformChannelPluginHook, + 'withPluginHook': withFfiPluginHook || withFfiPackage || withPlatformChannelPluginHook, 'withEmptyMain': withEmptyMain, 'androidLanguage': androidLanguage, 'iosLanguage': iosLanguage, diff --git a/packages/flutter_tools/lib/src/compile.dart b/packages/flutter_tools/lib/src/compile.dart index 1329c9fcd1a..8ba7d1e89f3 100644 --- a/packages/flutter_tools/lib/src/compile.dart +++ b/packages/flutter_tools/lib/src/compile.dart @@ -240,6 +240,7 @@ class KernelCompiler { required bool trackWidgetCreation, required List dartDefines, required PackageConfig packageConfig, + String? nativeAssets, }) async { final TargetPlatform? platform = targetModel == TargetModel.dartdevc ? TargetPlatform.web_javascript : null; final String frontendServer = _artifacts.getArtifactPath( @@ -337,6 +338,10 @@ class KernelCompiler { 'package:flutter/src/dart_plugin_registrant.dart', '-Dflutter.dart_plugin_registrant=$dartPluginRegistrantUri', ], + if (nativeAssets != null) ...[ + '--native-assets', + nativeAssets, + ], // See: https://github.com/flutter/flutter/issues/103994 '--verbosity=error', ...?extraFrontEndOptions, @@ -381,9 +386,10 @@ class _RecompileRequest extends _CompilationRequest { this.invalidatedFiles, this.outputPath, this.packageConfig, - this.suppressErrors, - {this.additionalSourceUri} - ); + this.suppressErrors, { + this.additionalSourceUri, + this.nativeAssetsYamlUri, + }); Uri mainUri; List? invalidatedFiles; @@ -391,6 +397,7 @@ class _RecompileRequest extends _CompilationRequest { PackageConfig packageConfig; bool suppressErrors; final Uri? additionalSourceUri; + final Uri? nativeAssetsYamlUri; @override Future _run(DefaultResidentCompiler compiler) async => @@ -515,6 +522,7 @@ abstract class ResidentCompiler { bool suppressErrors = false, bool checkDartPluginRegistry = false, File? dartPluginRegistrant, + Uri? nativeAssetsYaml, }); Future compileExpression( @@ -663,6 +671,7 @@ class DefaultResidentCompiler implements ResidentCompiler { File? dartPluginRegistrant, String? projectRootPath, FileSystem? fs, + Uri? nativeAssetsYaml, }) async { if (!_controller.hasListener) { _controller.stream.listen(_handleCompilationRequest); @@ -681,6 +690,7 @@ class DefaultResidentCompiler implements ResidentCompiler { packageConfig, suppressErrors, additionalSourceUri: additionalSourceUri, + nativeAssetsYamlUri: nativeAssetsYaml, )); return completer.future; } @@ -699,12 +709,22 @@ class DefaultResidentCompiler implements ResidentCompiler { toMultiRootPath(request.additionalSourceUri!, fileSystemScheme, fileSystemRoots, _platform.isWindows); } + final String? nativeAssets = request.nativeAssetsYamlUri?.toString(); final Process? server = _server; if (server == null) { - return _compile(mainUri, request.outputPath, additionalSourceUri: additionalSourceUri); + return _compile( + mainUri, + request.outputPath, + additionalSourceUri: additionalSourceUri, + nativeAssetsUri: nativeAssets, + ); } final String inputKey = Uuid().generateV4(); + if (nativeAssets != null && nativeAssets.isNotEmpty) { + server.stdin.writeln('native-assets $nativeAssets'); + _logger.printTrace('<- native-assets $nativeAssets'); + } server.stdin.writeln('recompile $mainUri $inputKey'); _logger.printTrace('<- recompile $mainUri $inputKey'); final List? invalidatedFiles = request.invalidatedFiles; @@ -746,9 +766,10 @@ class DefaultResidentCompiler implements ResidentCompiler { Future _compile( String scriptUri, - String? outputPath, - {String? additionalSourceUri} - ) async { + String? outputPath, { + String? additionalSourceUri, + String? nativeAssetsUri, + }) async { final TargetPlatform? platform = (targetModel == TargetModel.dartdevc) ? TargetPlatform.web_javascript : null; final String frontendServer = artifacts.getArtifactPath( Artifact.frontendServerSnapshotForEngineDartSdk, @@ -806,6 +827,10 @@ class DefaultResidentCompiler implements ResidentCompiler { 'package:flutter/src/dart_plugin_registrant.dart', '-Dflutter.dart_plugin_registrant=$additionalSourceUri', ], + if (nativeAssetsUri != null) ...[ + '--native-assets', + nativeAssetsUri, + ], if (platformDill != null) ...[ '--platform', platformDill!, @@ -842,6 +867,11 @@ class DefaultResidentCompiler implements ResidentCompiler { } })); + if (nativeAssetsUri != null && nativeAssetsUri.isNotEmpty) { + _server?.stdin.writeln('native-assets $nativeAssetsUri'); + _logger.printTrace('<- native-assets $nativeAssetsUri'); + } + _server?.stdin.writeln('compile $scriptUri'); _logger.printTrace('<- compile $scriptUri'); diff --git a/packages/flutter_tools/lib/src/features.dart b/packages/flutter_tools/lib/src/features.dart index 1e9c303eef1..28441699213 100644 --- a/packages/flutter_tools/lib/src/features.dart +++ b/packages/flutter_tools/lib/src/features.dart @@ -50,6 +50,9 @@ abstract class FeatureFlags { /// Whether animations are used in the command line interface. bool get isCliAnimationEnabled => true; + /// Whether native assets compilation and bundling is enabled. + bool get isNativeAssetsEnabled => false; + /// Whether a particular feature is enabled for the current channel. /// /// Prefer using one of the specific getters above instead of this API. @@ -68,6 +71,7 @@ const List allFeatures = [ flutterCustomDevicesFeature, flutterWebWasm, cliAnimation, + nativeAssets, ]; /// All current Flutter feature flags that can be configured. @@ -158,6 +162,16 @@ const Feature cliAnimation = Feature.fullyEnabled( configSetting: 'cli-animations', ); +/// Enable native assets compilation and bundling. +const Feature nativeAssets = Feature( + name: 'native assets compilation and bundling', + configSetting: 'enable-native-assets', + environmentOverride: 'FLUTTER_NATIVE_ASSETS', + master: FeatureChannelSetting( + available: true, + ), +); + /// A [Feature] is a process for conditionally enabling tool features. /// /// All settings are optional, and if not provided will generally default to diff --git a/packages/flutter_tools/lib/src/flutter_device_manager.dart b/packages/flutter_tools/lib/src/flutter_device_manager.dart index 47411190ca3..dc98b4a7d0d 100644 --- a/packages/flutter_tools/lib/src/flutter_device_manager.dart +++ b/packages/flutter_tools/lib/src/flutter_device_manager.dart @@ -87,7 +87,6 @@ class FlutterDeviceManager extends DeviceManager { processManager: processManager, logger: logger, artifacts: artifacts, - operatingSystemUtils: operatingSystemUtils, ), MacOSDevices( processManager: processManager, diff --git a/packages/flutter_tools/lib/src/flutter_features.dart b/packages/flutter_tools/lib/src/flutter_features.dart index 1f6de014e6f..1418e900096 100644 --- a/packages/flutter_tools/lib/src/flutter_features.dart +++ b/packages/flutter_tools/lib/src/flutter_features.dart @@ -55,6 +55,9 @@ class FlutterFeatureFlags implements FeatureFlags { return isEnabled(cliAnimation); } + @override + bool get isNativeAssetsEnabled => isEnabled(nativeAssets); + @override bool isEnabled(Feature feature) { final String currentChannel = _flutterVersion.channel; diff --git a/packages/flutter_tools/lib/src/flutter_plugins.dart b/packages/flutter_tools/lib/src/flutter_plugins.dart index 27fa5d611a2..50f14c06440 100644 --- a/packages/flutter_tools/lib/src/flutter_plugins.dart +++ b/packages/flutter_tools/lib/src/flutter_plugins.dart @@ -970,7 +970,7 @@ Future _writeWebPluginRegistrant(FlutterProject project, List plug /// be created only if missing. /// /// This uses [project.flutterPluginsDependenciesFile], so it should only be -/// run after refreshPluginList has been run since the last plugin change. +/// run after [refreshPluginsList] has been run since the last plugin change. void createPluginSymlinks(FlutterProject project, {bool force = false, @visibleForTesting FeatureFlags? featureFlagsOverride}) { final FeatureFlags localFeatureFlags = featureFlagsOverride ?? featureFlags; Map? platformPlugins; diff --git a/packages/flutter_tools/lib/src/flutter_project_metadata.dart b/packages/flutter_tools/lib/src/flutter_project_metadata.dart index 708f79fd5c7..c4c1e9146d1 100644 --- a/packages/flutter_tools/lib/src/flutter_project_metadata.dart +++ b/packages/flutter_tools/lib/src/flutter_project_metadata.dart @@ -7,6 +7,7 @@ import 'package:yaml/yaml.dart'; import 'base/file_system.dart'; import 'base/logger.dart'; import 'base/utils.dart'; +import 'features.dart'; import 'project.dart'; import 'template.dart'; import 'version.dart'; @@ -28,6 +29,9 @@ enum FlutterProjectType implements CliEnum { /// components, only Dart. package, + /// This is a Dart package project with external builds for native components. + packageFfi, + /// This is a native plugin project. plugin, @@ -52,6 +56,10 @@ enum FlutterProjectType implements CliEnum { 'Generate a shareable Flutter project containing an API ' 'in Dart code with a platform-specific implementation through dart:ffi for Android, iOS, ' 'Linux, macOS, Windows, or any combination of these.', + FlutterProjectType.packageFfi => + 'Generate a shareable Dart/Flutter project containing an API ' + 'in Dart code with a platform-specific implementation through dart:ffi for Android, iOS, ' + 'Linux, macOS, and Windows.', FlutterProjectType.module => 'Generate a project to add a Flutter module to an existing Android or iOS application.', }; @@ -64,6 +72,16 @@ enum FlutterProjectType implements CliEnum { } return null; } + + static List get enabledValues { + return [ + for (final FlutterProjectType value in values) + if (value == FlutterProjectType.packageFfi) ...[ + if (featureFlags.isNativeAssetsEnabled) value + ] else + value, + ]; + } } /// Verifies the expected yaml keys are present in the file. diff --git a/packages/flutter_tools/lib/src/ios/native_assets.dart b/packages/flutter_tools/lib/src/ios/native_assets.dart new file mode 100644 index 00000000000..3e2d595fba4 --- /dev/null +++ b/packages/flutter_tools/lib/src/ios/native_assets.dart @@ -0,0 +1,171 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:native_assets_builder/native_assets_builder.dart' show BuildResult; +import 'package:native_assets_cli/native_assets_cli.dart' hide BuildMode; +import 'package:native_assets_cli/native_assets_cli.dart' as native_assets_cli; + +import '../base/file_system.dart'; +import '../build_info.dart'; +import '../globals.dart' as globals; + +import '../macos/native_assets_host.dart'; +import '../native_assets.dart'; + +/// Dry run the native builds. +/// +/// This does not build native assets, it only simulates what the final paths +/// of all assets will be so that this can be embedded in the kernel file and +/// the Xcode project. +Future 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 assetTargetLocations = await dryRunNativeAssetsIOSInternal( + fileSystem, + projectUri, + buildRunner, + ); + final Uri nativeAssetsUri = await writeNativeAssetsYaml( + assetTargetLocations, + buildUri_, + fileSystem, + ); + return nativeAssetsUri; +} + +Future> dryRunNativeAssetsIOSInternal( + FileSystem fileSystem, + Uri projectUri, + NativeAssetsBuildRunner buildRunner, +) async { + const OS targetOs = OS.iOS; + globals.logger.printTrace('Dry running native assets for $targetOs.'); + final List 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 assetTargetLocations = _assetTargetLocations(nativeAssets).values; + return assetTargetLocations; +} + +/// Builds native assets. +Future> buildNativeAssetsIOS({ + required NativeAssetsBuildRunner buildRunner, + required List 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([], yamlParentDirectory, fileSystem); + return []; + } + + final List 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 nativeAssets = []; + final Set dependencies = {}; + 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> fatAssetTargetLocations = _fatAssetTargetLocations(nativeAssets); + await copyNativeAssetsMacOSHost( + buildUri_, + fatAssetTargetLocations, + codesignIdentity, + buildMode, + fileSystem, + ); + + final Map 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> _fatAssetTargetLocations(List nativeAssets) { + final Map> result = >{}; + for (final Asset asset in nativeAssets) { + final AssetPath path = _targetLocationIOS(asset).path; + result[path] ??= []; + result[path]!.add(asset); + } + return result; +} + +Map _assetTargetLocations(List nativeAssets) => { + 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'); +} diff --git a/packages/flutter_tools/lib/src/macos/native_assets.dart b/packages/flutter_tools/lib/src/macos/native_assets.dart new file mode 100644 index 00000000000..c62c28fc111 --- /dev/null +++ b/packages/flutter_tools/lib/src/macos/native_assets.dart @@ -0,0 +1,162 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:native_assets_builder/native_assets_builder.dart' show BuildResult; +import 'package:native_assets_cli/native_assets_cli.dart' hide BuildMode; +import 'package:native_assets_cli/native_assets_cli.dart' as native_assets_cli; + +import '../base/file_system.dart'; +import '../build_info.dart'; +import '../globals.dart' as globals; +import '../native_assets.dart'; +import 'native_assets_host.dart'; + +/// Dry run the native builds. +/// +/// This does not build native assets, it only simulates what the final paths +/// of all assets will be so that this can be embedded in the kernel file and +/// the Xcode project. +Future 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 nativeAssetPaths = await dryRunNativeAssetsMacOSInternal(fileSystem, projectUri, flutterTester, buildRunner); + final Uri nativeAssetsUri = await writeNativeAssetsYaml(nativeAssetPaths, buildUri_, fileSystem); + return nativeAssetsUri; +} + +Future> 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 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 assetTargetLocations = _assetTargetLocations(nativeAssets, absolutePath); + final Iterable 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 dependencies)> buildNativeAssetsMacOS({ + required NativeAssetsBuildRunner buildRunner, + List? 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([], yamlParentDirectory ?? buildUri_, fileSystem); + return (nativeAssetsYaml, []); + } + + final List targets = darwinArchs != null ? darwinArchs.map(_getNativeTarget).toList() : [Target.current]; + final native_assets_cli.BuildMode buildModeCli = nativeAssetsBuildMode(buildMode); + + globals.logger.printTrace('Building native assets for $targets $buildModeCli.'); + final List nativeAssets = []; + final Set dependencies = {}; + 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 assetTargetLocations = _assetTargetLocations(nativeAssets, absolutePath); + final Map> 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> _fatAssetTargetLocations(List nativeAssets, Uri? absolutePath) { + final Map> result = >{}; + for (final Asset asset in nativeAssets) { + final AssetPath path = _targetLocationMacOS(asset, absolutePath).path; + result[path] ??= []; + result[path]!.add(asset); + } + return result; +} + +Map _assetTargetLocations(List nativeAssets, Uri? absolutePath) => { + 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'); +} diff --git a/packages/flutter_tools/lib/src/macos/native_assets_host.dart b/packages/flutter_tools/lib/src/macos/native_assets_host.dart new file mode 100644 index 00000000000..107ac9045c1 --- /dev/null +++ b/packages/flutter_tools/lib/src/macos/native_assets_host.dart @@ -0,0 +1,141 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Shared logic between iOS and macOS implementations of native assets. + +import 'package:native_assets_cli/native_assets_cli.dart' hide BuildMode; + +import '../base/common.dart'; +import '../base/file_system.dart'; +import '../base/io.dart'; +import '../build_info.dart'; +import '../convert.dart'; +import '../globals.dart' as globals; + +/// The target location for native assets on macOS. +/// +/// Because we need to have a multi-architecture solution for +/// `flutter run --release`, we use `lipo` to combine all target architectures +/// into a single file. +/// +/// We need to set the install name so that it matches what the place it will +/// be bundled in the final app. +/// +/// Code signing is also done here, so that we don't have to worry about it +/// in xcode_backend.dart and macos_assemble.sh. +Future copyNativeAssetsMacOSHost( + Uri buildUri, + Map> 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> assetMapping in assetTargetLocations.entries) { + final Uri target = (assetMapping.key as AssetAbsolutePath).uri; + final List sources = [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 lipoDylibs(String targetFullPath, List sources) async { + final ProcessResult lipoResult = await globals.processManager.run( + [ + '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 setInstallNameDylib(Uri targetUri) async { + final String fileName = targetUri.pathSegments.last; + final ProcessResult installNameResult = await globals.processManager.run( + [ + '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 codesignDylib( + String? codesignIdentity, + BuildMode buildMode, + String targetFullPath, +) async { + if (codesignIdentity == null || codesignIdentity.isEmpty) { + codesignIdentity = '-'; + } + final List codesignCommand = [ + 'codesign', + '--force', + '--sign', + codesignIdentity, + if (buildMode != BuildMode.release) ...[ + // 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 cCompilerConfigMacOS() async { + final ProcessResult xcrunResult = await globals.processManager.run(['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'), + ); +} diff --git a/packages/flutter_tools/lib/src/native_assets.dart b/packages/flutter_tools/lib/src/native_assets.dart new file mode 100644 index 00000000000..0255a3c4dd4 --- /dev/null +++ b/packages/flutter_tools/lib/src/native_assets.dart @@ -0,0 +1,378 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Logic for native assets shared between all host OSes. + +import 'package:logging/logging.dart' as logging; +import 'package:native_assets_builder/native_assets_builder.dart' as native_assets_builder; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:package_config/package_config_types.dart'; + +import 'base/common.dart'; +import 'base/file_system.dart'; +import 'base/logger.dart'; +import 'base/platform.dart'; +import 'build_info.dart' as build_info; +import 'cache.dart'; +import 'features.dart'; +import 'globals.dart' as globals; +import 'ios/native_assets.dart'; +import 'macos/native_assets.dart'; +import 'macos/native_assets_host.dart'; +import 'resident_runner.dart'; + +/// Programmatic API to be used by Dart launchers to invoke native builds. +/// +/// It enables mocking `package:native_assets_builder` package. +/// It also enables mocking native toolchain discovery via [cCompilerConfig]. +abstract class NativeAssetsBuildRunner { + /// Whether the project has a `.dart_tools/package_config.json`. + /// + /// If there is no package config, [packagesWithNativeAssets], [build], and + /// [dryRun] must not be invoked. + Future hasPackageConfig(); + + /// All packages in the transitive dependencies that have a `build.dart`. + Future> packagesWithNativeAssets(); + + /// Runs all [packagesWithNativeAssets] `build.dart` in dry run. + Future dryRun({ + required bool includeParentEnvironment, + required LinkModePreference linkModePreference, + required OS targetOs, + required Uri workingDirectory, + }); + + /// Runs all [packagesWithNativeAssets] `build.dart`. + Future build({ + required bool includeParentEnvironment, + required BuildMode buildMode, + required LinkModePreference linkModePreference, + required Target target, + required Uri workingDirectory, + CCompilerConfig? cCompilerConfig, + int? targetAndroidNdkApi, + IOSSdk? targetIOSSdk, + }); + + /// The C compiler config to use for compilation. + Future get cCompilerConfig; +} + +/// Uses `package:native_assets_builder` for its implementation. +class NativeAssetsBuildRunnerImpl implements NativeAssetsBuildRunner { + NativeAssetsBuildRunnerImpl(this.projectUri, this.fileSystem, this.logger); + + final Uri projectUri; + final FileSystem fileSystem; + final Logger logger; + + late final logging.Logger _logger = logging.Logger('') + ..onRecord.listen((logging.LogRecord record) { + final int levelValue = record.level.value; + final String message = record.message; + if (levelValue >= logging.Level.SEVERE.value) { + logger.printError(message); + } else if (levelValue >= logging.Level.WARNING.value) { + logger.printWarning(message); + } else if (levelValue >= logging.Level.INFO.value) { + logger.printTrace(message); + } else { + logger.printTrace(message); + } + }); + + late final Uri _dartExecutable = fileSystem.directory(Cache.flutterRoot).uri.resolve('bin/dart'); + + late final native_assets_builder.NativeAssetsBuildRunner _buildRunner = native_assets_builder.NativeAssetsBuildRunner( + logger: _logger, + dartExecutable: _dartExecutable, + ); + + native_assets_builder.PackageLayout? _packageLayout; + + @override + Future hasPackageConfig() { + final File packageConfigJson = fileSystem + .directory(projectUri.toFilePath()) + .childDirectory('.dart_tool') + .childFile('package_config.json'); + return packageConfigJson.exists(); + } + + @override + Future> packagesWithNativeAssets() async { + _packageLayout ??= await native_assets_builder.PackageLayout.fromRootPackageRoot(projectUri); + return _packageLayout!.packagesWithNativeAssets; + } + + @override + Future dryRun({ + required bool includeParentEnvironment, + required LinkModePreference linkModePreference, + required OS targetOs, + required Uri workingDirectory, + }) { + return _buildRunner.dryRun( + includeParentEnvironment: includeParentEnvironment, + linkModePreference: linkModePreference, + targetOs: targetOs, + workingDirectory: workingDirectory, + ); + } + + @override + Future build({ + required bool includeParentEnvironment, + required BuildMode buildMode, + required LinkModePreference linkModePreference, + required Target target, + required Uri workingDirectory, + CCompilerConfig? cCompilerConfig, + int? targetAndroidNdkApi, + IOSSdk? targetIOSSdk, + }) { + return _buildRunner.build( + buildMode: buildMode, + cCompilerConfig: cCompilerConfig, + includeParentEnvironment: includeParentEnvironment, + linkModePreference: linkModePreference, + target: target, + targetAndroidNdkApi: targetAndroidNdkApi, + targetIOSSdk: targetIOSSdk, + workingDirectory: workingDirectory, + ); + } + + @override + late final Future cCompilerConfig = () { + if (globals.platform.isMacOS || globals.platform.isIOS) { + return cCompilerConfigMacOS(); + } + throwToolExit( + 'Native assets feature not yet implemented for Linux, Windows and Android.', + ); + }(); +} + +/// Write [assets] to `native_assets.yaml` in [yamlParentDirectory]. +Future writeNativeAssetsYaml( + Iterable assets, + Uri yamlParentDirectory, + FileSystem fileSystem, +) async { + globals.logger.printTrace('Writing native_assets.yaml.'); + final String nativeAssetsDartContents = assets.toNativeAssetsFile(); + final Directory parentDirectory = fileSystem.directory(yamlParentDirectory); + if (!await parentDirectory.exists()) { + await parentDirectory.create(recursive: true); + } + final File nativeAssetsFile = parentDirectory.childFile('native_assets.yaml'); + await nativeAssetsFile.writeAsString(nativeAssetsDartContents); + globals.logger.printTrace('Writing ${nativeAssetsFile.path} done.'); + return nativeAssetsFile.uri; +} + +/// Select the native asset build mode for a given Flutter build mode. +BuildMode nativeAssetsBuildMode(build_info.BuildMode buildMode) { + switch (buildMode) { + case build_info.BuildMode.debug: + return BuildMode.debug; + case build_info.BuildMode.jitRelease: + case build_info.BuildMode.profile: + case build_info.BuildMode.release: + return BuildMode.release; + } +} + +/// Checks whether this project does not yet have a package config file. +/// +/// A project has no package config when `pub get` has not yet been run. +/// +/// Native asset builds cannot be run without a package config. If there is +/// no package config, leave a logging trace about that. +Future hasNoPackageConfig(NativeAssetsBuildRunner buildRunner) async { + final bool packageConfigExists = await buildRunner.hasPackageConfig(); + if (!packageConfigExists) { + globals.logger.printTrace('No package config found. Skipping native assets compilation.'); + } + return !packageConfigExists; +} + +/// Checks that if native assets is disabled, none of the dependencies declare +/// native assets. +/// +/// If any of the dependencies have native assets, but native assets are +/// disabled, exits the tool. +Future isDisabledAndNoNativeAssets(NativeAssetsBuildRunner buildRunner) async { + if (featureFlags.isNativeAssetsEnabled) { + return false; + } + final List packagesWithNativeAssets = await buildRunner.packagesWithNativeAssets(); + if (packagesWithNativeAssets.isEmpty) { + return true; + } + final String packageNames = packagesWithNativeAssets.map((Package p) => p.name).join(' '); + throwToolExit( + 'Package(s) $packageNames require the native assets feature to be enabled. ' + 'Enable using `flutter config --enable-native-assets`.', + ); +} + +/// Ensures that either this project has no native assets, or that native assets +/// are supported on that operating system. +/// +/// Exits the tool if the above condition is not satisfied. +Future ensureNoNativeAssetsOrOsIsSupported( + Uri workingDirectory, + String os, + FileSystem fileSystem, + NativeAssetsBuildRunner buildRunner, +) async { + if (await hasNoPackageConfig(buildRunner)) { + return; + } + final List packagesWithNativeAssets = await buildRunner.packagesWithNativeAssets(); + if (packagesWithNativeAssets.isEmpty) { + return; + } + final String packageNames = packagesWithNativeAssets.map((Package p) => p.name).join(' '); + throwToolExit( + 'Package(s) $packageNames require the native assets feature. ' + 'This feature has not yet been implemented for `$os`. ' + 'For more info see https://github.com/flutter/flutter/issues/129757.', + ); +} + +/// Ensure all native assets have a linkmode declared to be dynamic loading. +/// +/// In JIT, the link mode must always be dynamic linking. +/// In AOT, the static linking has not yet been implemented in Dart: +/// https://github.com/dart-lang/sdk/issues/49418. +/// +/// Therefore, ensure all `build.dart` scripts return only dynamic libraries. +void ensureNoLinkModeStatic(List nativeAssets) { + final Iterable staticAssets = nativeAssets.whereLinkMode(LinkMode.static); + if (staticAssets.isNotEmpty) { + final String assetIds = staticAssets.map((Asset a) => a.id).toSet().join(', '); + throwToolExit( + 'Native asset(s) $assetIds have their link mode set to static, ' + 'but this is not yet supported. ' + 'For more info see https://github.com/dart-lang/sdk/issues/49418.', + ); + } +} + +/// This should be the same for different archs, debug/release, etc. +/// It should work for all macOS. +Uri nativeAssetsBuildUri(Uri projectUri, OS os) { + final String buildDir = build_info.getBuildDirectory(); + return projectUri.resolve('$buildDir/native_assets/$os/'); +} + +/// Gets the native asset id to dylib mapping to embed in the kernel file. +/// +/// Run hot compiles a kernel file that is pushed to the device after hot +/// restart. We need to embed the native assets mapping in order to access +/// native assets after hot restart. +Future dryRunNativeAssets({ + required Uri projectUri, + required FileSystem fileSystem, + required NativeAssetsBuildRunner buildRunner, + required List flutterDevices, +}) async { + if (flutterDevices.length != 1) { + return dryRunNativeAssetsMultipeOSes( + projectUri: projectUri, + fileSystem: fileSystem, + targetPlatforms: flutterDevices.map((FlutterDevice d) => d.targetPlatform).nonNulls, + buildRunner: buildRunner, + ); + } + final FlutterDevice flutterDevice = flutterDevices.single; + final build_info.TargetPlatform targetPlatform = flutterDevice.targetPlatform!; + + final Uri? nativeAssetsYaml; + switch (targetPlatform) { + case build_info.TargetPlatform.darwin: + nativeAssetsYaml = await dryRunNativeAssetsMacOS( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: buildRunner, + ); + case build_info.TargetPlatform.ios: + nativeAssetsYaml = await dryRunNativeAssetsIOS( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: buildRunner, + ); + case build_info.TargetPlatform.tester: + if (const LocalPlatform().isMacOS) { + nativeAssetsYaml = await dryRunNativeAssetsMacOS( + projectUri: projectUri, + flutterTester: true, + fileSystem: fileSystem, + buildRunner: buildRunner, + ); + } else { + await ensureNoNativeAssetsOrOsIsSupported( + projectUri, + const LocalPlatform().operatingSystem, + fileSystem, + buildRunner, + ); + nativeAssetsYaml = null; + } + case build_info.TargetPlatform.android_arm: + case build_info.TargetPlatform.android_arm64: + case build_info.TargetPlatform.android_x64: + case build_info.TargetPlatform.android_x86: + case build_info.TargetPlatform.android: + case build_info.TargetPlatform.fuchsia_arm64: + case build_info.TargetPlatform.fuchsia_x64: + case build_info.TargetPlatform.linux_arm64: + case build_info.TargetPlatform.linux_x64: + case build_info.TargetPlatform.web_javascript: + case build_info.TargetPlatform.windows_x64: + await ensureNoNativeAssetsOrOsIsSupported( + projectUri, + targetPlatform.toString(), + fileSystem, + buildRunner, + ); + nativeAssetsYaml = null; + } + return nativeAssetsYaml; +} + +/// Dry run the native builds for multiple OSes. +/// +/// Needed for `flutter run -d all`. +Future dryRunNativeAssetsMultipeOSes({ + required NativeAssetsBuildRunner buildRunner, + required Uri projectUri, + required FileSystem fileSystem, + required Iterable targetPlatforms, +}) async { + if (await hasNoPackageConfig(buildRunner) || await isDisabledAndNoNativeAssets(buildRunner)) { + return null; + } + + final Uri buildUri_ = buildUriMultiple(projectUri); + final Iterable nativeAssetPaths = [ + if (targetPlatforms.contains(build_info.TargetPlatform.darwin) || + (targetPlatforms.contains(build_info.TargetPlatform.tester) && OS.current == OS.macOS)) + ...await dryRunNativeAssetsMacOSInternal(fileSystem, projectUri, false, buildRunner), + if (targetPlatforms.contains(build_info.TargetPlatform.ios)) ...await dryRunNativeAssetsIOSInternal(fileSystem, projectUri, buildRunner) + ]; + final Uri nativeAssetsUri = await writeNativeAssetsYaml(nativeAssetPaths, buildUri_, fileSystem); + return nativeAssetsUri; +} + +/// With `flutter run -d all` we need a place to store the native assets +/// mapping for multiple OSes combined. +Uri buildUriMultiple(Uri projectUri) { + final String buildDir = build_info.getBuildDirectory(); + return projectUri.resolve('$buildDir/native_assets/multiple/'); +} diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index fdebdcab731..8665ec322ef 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart @@ -21,6 +21,7 @@ import 'dart/package_map.dart'; import 'devfs.dart'; import 'device.dart'; import 'globals.dart' as globals; +import 'native_assets.dart'; import 'project.dart'; import 'reporting/reporting.dart'; import 'resident_runner.dart'; @@ -92,9 +93,11 @@ class HotRunner extends ResidentRunner { StopwatchFactory stopwatchFactory = const StopwatchFactory(), ReloadSourcesHelper reloadSourcesHelper = defaultReloadSourcesHelper, ReassembleHelper reassembleHelper = _defaultReassembleHelper, + NativeAssetsBuildRunner? buildRunner, }) : _stopwatchFactory = stopwatchFactory, _reloadSourcesHelper = reloadSourcesHelper, _reassembleHelper = reassembleHelper, + _buildRunner = buildRunner, super( hotMode: true, ); @@ -132,6 +135,8 @@ class HotRunner extends ResidentRunner { String? _sdkName; bool? _emulator; + NativeAssetsBuildRunner? _buildRunner; + Future _calculateTargetPlatform() async { if (_targetPlatform != null) { return; @@ -360,6 +365,15 @@ class HotRunner extends ResidentRunner { }) async { await _calculateTargetPlatform(); + final Uri projectUri = Uri.directory(projectRootPath); + _buildRunner ??= NativeAssetsBuildRunnerImpl(projectUri, fileSystem, globals.logger); + final Uri? nativeAssetsYaml = await dryRunNativeAssets( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: _buildRunner!, + flutterDevices: flutterDevices, + ); + final Stopwatch appStartedTimer = Stopwatch()..start(); final File mainFile = globals.fs.file(mainPath); firstBuildTime = DateTime.now(); @@ -391,6 +405,7 @@ class HotRunner extends ResidentRunner { packageConfig: debuggingOptions.buildInfo.packageConfig, projectRootPath: FlutterProject.current().directory.absolute.path, fs: globals.fs, + nativeAssetsYaml: nativeAssetsYaml, ).then((CompilerOutput? output) { compileTimer.stop(); totalCompileTime += compileTimer.elapsed; diff --git a/packages/flutter_tools/lib/src/test/test_compiler.dart b/packages/flutter_tools/lib/src/test/test_compiler.dart index 37db867caa0..067838482ba 100644 --- a/packages/flutter_tools/lib/src/test/test_compiler.dart +++ b/packages/flutter_tools/lib/src/test/test_compiler.dart @@ -10,11 +10,14 @@ import 'package:meta/meta.dart'; import '../artifacts.dart'; import '../base/file_system.dart'; +import '../base/platform.dart'; import '../build_info.dart'; import '../bundle.dart'; import '../compile.dart'; import '../flutter_plugins.dart'; import '../globals.dart' as globals; +import '../macos/native_assets.dart'; +import '../native_assets.dart'; import '../project.dart'; import 'test_time_recorder.dart'; @@ -163,6 +166,26 @@ class TestCompiler { invalidatedRegistrantFiles.add(flutterProject!.dartPluginRegistrant.absolute.uri); } + Uri? nativeAssetsYaml; + final Uri projectUri = FlutterProject.current().directory.uri; + final NativeAssetsBuildRunner buildRunner = NativeAssetsBuildRunnerImpl(projectUri, globals.fs, globals.logger); + if (globals.platform.isMacOS) { + (nativeAssetsYaml, _) = await buildNativeAssetsMacOS( + buildMode: BuildMode.debug, + projectUri: projectUri, + flutterTester: true, + fileSystem: globals.fs, + buildRunner: buildRunner, + ); + } else { + await ensureNoNativeAssetsOrOsIsSupported( + projectUri, + const LocalPlatform().operatingSystem, + globals.fs, + buildRunner, + ); + } + final CompilerOutput? compilerOutput = await compiler!.recompile( request.mainUri, [request.mainUri, ...invalidatedRegistrantFiles], @@ -171,6 +194,7 @@ class TestCompiler { projectRootPath: flutterProject?.directory.absolute.path, checkDartPluginRegistry: true, fs: globals.fs, + nativeAssetsYaml: nativeAssetsYaml, ); final String? outputPath = compilerOutput?.outputFilename; diff --git a/packages/flutter_tools/lib/src/tester/flutter_tester.dart b/packages/flutter_tools/lib/src/tester/flutter_tester.dart index 5b9f6fe1a5d..241b132a75a 100644 --- a/packages/flutter_tools/lib/src/tester/flutter_tester.dart +++ b/packages/flutter_tools/lib/src/tester/flutter_tester.dart @@ -11,7 +11,6 @@ import '../artifacts.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/logger.dart'; -import '../base/os.dart'; import '../build_info.dart'; import '../bundle.dart'; import '../bundle_builder.dart'; @@ -50,13 +49,11 @@ class FlutterTesterDevice extends Device { required Logger logger, required FileSystem fileSystem, required Artifacts artifacts, - required OperatingSystemUtils operatingSystemUtils, }) : _processManager = processManager, _flutterVersion = flutterVersion, _logger = logger, _fileSystem = fileSystem, - _artifacts = artifacts, - _operatingSystemUtils = operatingSystemUtils, + _artifacts = artifacts, super( platformType: null, category: null, @@ -68,7 +65,6 @@ class FlutterTesterDevice extends Device { final Logger _logger; final FileSystem _fileSystem; final Artifacts _artifacts; - final OperatingSystemUtils _operatingSystemUtils; Process? _process; final DevicePortForwarder _portForwarder = const NoOpDevicePortForwarder(); @@ -157,7 +153,7 @@ class FlutterTesterDevice extends Device { buildInfo: buildInfo, mainPath: mainPath, applicationKernelFilePath: applicationKernelFilePath, - platform: getTargetPlatformForName(getNameForHostPlatform(_operatingSystemUtils.hostPlatform)), + platform: TargetPlatform.tester, assetDirPath: assetDirectory.path, ); @@ -258,15 +254,13 @@ class FlutterTesterDevices extends PollingDeviceDiscovery { required ProcessManager processManager, required Logger logger, required FlutterVersion flutterVersion, - required OperatingSystemUtils operatingSystemUtils, }) : _testerDevice = FlutterTesterDevice( kTesterDeviceId, fileSystem: fileSystem, artifacts: artifacts, processManager: processManager, logger: logger, - flutterVersion: flutterVersion, - operatingSystemUtils: operatingSystemUtils, + flutterVersion: flutterVersion, ), super('Flutter tester'); diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index 7e8022f80ed..942598884bf 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -50,6 +50,11 @@ dependencies: async: 2.11.0 unified_analytics: 3.0.0 + cli_config: 0.1.1 + graphs: 2.3.1 + native_assets_builder: 0.2.0 + native_assets_cli: 0.2.0 + # We depend on very specific internal implementation details of the # 'test' package, which change between versions, so when upgrading # this, make sure the tests are still running correctly. @@ -107,4 +112,4 @@ dartdoc: # Exclude this package from the hosted API docs. nodoc: true -# PUBSPEC CHECKSUM: a4aa +# PUBSPEC CHECKSUM: 284b diff --git a/packages/flutter_tools/templates/app/lib/main.dart.tmpl b/packages/flutter_tools/templates/app/lib/main.dart.tmpl index e7ad70d7544..78e535632ce 100644 --- a/packages/flutter_tools/templates/app/lib/main.dart.tmpl +++ b/packages/flutter_tools/templates/app/lib/main.dart.tmpl @@ -27,11 +27,11 @@ import 'dart:async'; import 'package:flutter/services.dart'; import 'package:{{pluginProjectName}}/{{pluginProjectName}}.dart'; {{/withPlatformChannelPluginHook}} -{{#withFfiPluginHook}} +{{#withFfi}} import 'dart:async'; import 'package:{{pluginProjectName}}/{{pluginProjectName}}.dart' as {{pluginProjectName}}; -{{/withFfiPluginHook}} +{{/withFfi}} void main() { runApp(const MyApp()); @@ -213,7 +213,7 @@ class _MyAppState extends State { } } {{/withPlatformChannelPluginHook}} -{{#withFfiPluginHook}} +{{#withFfi}} class MyApp extends StatefulWidget { const MyApp({super.key}); @@ -279,5 +279,5 @@ class _MyAppState extends State { ); } } -{{/withFfiPluginHook}} +{{/withFfi}} {{/withEmptyMain}} diff --git a/packages/flutter_tools/templates/module/common/test/widget_test.dart.tmpl b/packages/flutter_tools/templates/module/common/test/widget_test.dart.tmpl index c27006fbc84..f05c069c133 100644 --- a/packages/flutter_tools/templates/module/common/test/widget_test.dart.tmpl +++ b/packages/flutter_tools/templates/module/common/test/widget_test.dart.tmpl @@ -8,9 +8,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -{{^withFfiPluginHook}} +{{^withFfi}} import 'package:{{projectName}}/main.dart'; -{{/withFfiPluginHook}} +{{/withFfi}} {{^withPluginHook}} void main() { diff --git a/packages/flutter_tools/templates/package_ffi/.gitignore.tmpl b/packages/flutter_tools/templates/package_ffi/.gitignore.tmpl new file mode 100644 index 00000000000..96486fd9302 --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/.gitignore.tmpl @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/flutter_tools/templates/package_ffi/.metadata.tmpl b/packages/flutter_tools/templates/package_ffi/.metadata.tmpl new file mode 100644 index 00000000000..e1a1dd93214 --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/.metadata.tmpl @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: {{flutterRevision}} + channel: {{flutterChannel}} + +project_type: package_ffi diff --git a/packages/flutter_tools/templates/package_ffi/CHANGELOG.md.tmpl b/packages/flutter_tools/templates/package_ffi/CHANGELOG.md.tmpl new file mode 100644 index 00000000000..41cc7d8192e --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/CHANGELOG.md.tmpl @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/flutter_tools/templates/package_ffi/LICENSE.tmpl b/packages/flutter_tools/templates/package_ffi/LICENSE.tmpl new file mode 100644 index 00000000000..ba75c69f7f2 --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/LICENSE.tmpl @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/packages/flutter_tools/templates/package_ffi/README.md.tmpl b/packages/flutter_tools/templates/package_ffi/README.md.tmpl new file mode 100644 index 00000000000..3a636eb722d --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/README.md.tmpl @@ -0,0 +1,49 @@ +# {{projectName}} + +{{description}} + +## Getting Started + +This project is a starting point for a Flutter +[FFI package](https://docs.flutter.dev/development/platform-integration/c-interop), +a specialized package that includes native code directly invoked with Dart FFI. + +## Project stucture + +This template uses the following structure: + +* `src`: Contains the native source code, and a CmakeFile.txt file for building + that source code into a dynamic library. + +* `lib`: Contains the Dart code that defines the API of the plugin, and which + calls into the native code using `dart:ffi`. + +* `bin`: Contains the `build.dart` that performs the external native builds. + +## Buidling and bundling native code + +`build.dart` does the building of native components. + +Bundling is done by Flutter based on the output from `build.dart`. + +## Binding to native code + +To use the native code, bindings in Dart are needed. +To avoid writing these by hand, they are generated from the header file +(`src/{{projectName}}.h`) by `package:ffigen`. +Regenerate the bindings by running `flutter pub run ffigen --config ffigen.yaml`. + +## Invoking native code + +Very short-running native functions can be directly invoked from any isolate. +For example, see `sum` in `lib/{{projectName}}.dart`. + +Longer-running functions should be invoked on a helper isolate to avoid +dropping frames in Flutter applications. +For example, see `sumAsync` in `lib/{{projectName}}.dart`. + +## Flutter help + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/flutter_tools/templates/package_ffi/analysis_options.yaml.tmpl b/packages/flutter_tools/templates/package_ffi/analysis_options.yaml.tmpl new file mode 100644 index 00000000000..a5744c1cfbe --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/analysis_options.yaml.tmpl @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/flutter_tools/templates/package_ffi/build.dart.tmpl b/packages/flutter_tools/templates/package_ffi/build.dart.tmpl new file mode 100644 index 00000000000..3fe2224500d --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/build.dart.tmpl @@ -0,0 +1,24 @@ +import 'package:native_toolchain_c/native_toolchain_c.dart'; +import 'package:logging/logging.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; + +const packageName = '{{projectName}}'; + +void main(List 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); +} diff --git a/packages/flutter_tools/templates/package_ffi/ffigen.yaml.tmpl b/packages/flutter_tools/templates/package_ffi/ffigen.yaml.tmpl new file mode 100644 index 00000000000..c33bb9f92cd --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/ffigen.yaml.tmpl @@ -0,0 +1,20 @@ +# Run with `flutter pub run ffigen --config ffigen.yaml`. +name: {{pluginDartClass}}Bindings +description: | + Bindings for `src/{{projectName}}.h`. + + Regenerate bindings with `flutter pub run ffigen --config ffigen.yaml`. +output: 'lib/{{projectName}}_bindings_generated.dart' +headers: + entry-points: + - 'src/{{projectName}}.h' + include-directives: + - 'src/{{projectName}}.h' +ffi-native: +preamble: | + // ignore_for_file: always_specify_types + // ignore_for_file: camel_case_types + // ignore_for_file: non_constant_identifier_names +comments: + style: any + length: full diff --git a/packages/flutter_tools/templates/package_ffi/lib/projectName.dart.tmpl b/packages/flutter_tools/templates/package_ffi/lib/projectName.dart.tmpl new file mode 100644 index 00000000000..2c3d5cd443c --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/lib/projectName.dart.tmpl @@ -0,0 +1,108 @@ +import 'dart:async'; +import 'dart:isolate'; + +import '{{projectName}}_bindings_generated.dart' as bindings; + +/// A very short-lived native function. +/// +/// For very short-lived functions, it is fine to call them on the main isolate. +/// They will block the Dart execution while running the native function, so +/// only do this for native functions which are guaranteed to be short-lived. +int sum(int a, int b) => bindings.sum(a, b); + +/// A longer lived native function, which occupies the thread calling it. +/// +/// Do not call these kind of native functions in the main isolate. They will +/// block Dart execution. This will cause dropped frames in Flutter applications. +/// Instead, call these native functions on a separate isolate. +/// +/// Modify this to suit your own use case. Example use cases: +/// +/// 1. Reuse a single isolate for various different kinds of requests. +/// 2. Use multiple helper isolates for parallel execution. +Future sumAsync(int a, int b) async { + final SendPort helperIsolateSendPort = await _helperIsolateSendPort; + final int requestId = _nextSumRequestId++; + final _SumRequest request = _SumRequest(requestId, a, b); + final Completer completer = Completer(); + _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> _sumRequests = >{}; + +/// The SendPort belonging to the helper isolate. +Future _helperIsolateSendPort = () async { + // The helper isolate is going to send us back a SendPort, which we want to + // wait for. + final Completer completer = Completer(); + + // 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 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; +}(); diff --git a/packages/flutter_tools/templates/package_ffi/lib/projectName_bindings_generated.dart.tmpl b/packages/flutter_tools/templates/package_ffi/lib/projectName_bindings_generated.dart.tmpl new file mode 100644 index 00000000000..65642b43818 --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/lib/projectName_bindings_generated.dart.tmpl @@ -0,0 +1,30 @@ +// ignore_for_file: always_specify_types +// ignore_for_file: camel_case_types +// ignore_for_file: non_constant_identifier_names + +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +import 'dart:ffi' as ffi; + +/// A very short-lived native function. +/// +/// For very short-lived functions, it is fine to call them on the main isolate. +/// They will block the Dart execution while running the native function, so +/// only do this for native functions which are guaranteed to be short-lived. +@ffi.Native() +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() +external int sum_long_running( + int a, + int b, +); diff --git a/packages/flutter_tools/templates/package_ffi/pubspec.yaml.tmpl b/packages/flutter_tools/templates/package_ffi/pubspec.yaml.tmpl new file mode 100644 index 00000000000..8fa5f3ac67a --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/pubspec.yaml.tmpl @@ -0,0 +1,19 @@ +name: {{projectName}} +description: {{description}} +version: 0.0.1 +homepage: + +environment: + sdk: {{dartSdkVersionBounds}} + +dependencies: + cli_config: ^0.1.1 + logging: ^1.1.1 + native_assets_cli: ^0.2.0 + native_toolchain_c: ^0.2.0 + +dev_dependencies: + ffi: ^2.0.2 + ffigen: ^9.0.0 + flutter_lints: ^2.0.0 + test: ^1.21.0 diff --git a/packages/flutter_tools/templates/package_ffi/src.tmpl/projectName.c.tmpl b/packages/flutter_tools/templates/package_ffi/src.tmpl/projectName.c.tmpl new file mode 100644 index 00000000000..a0d23594f02 --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/src.tmpl/projectName.c.tmpl @@ -0,0 +1,29 @@ +#include "{{projectName}}.h" + +// A very short-lived native function. +// +// For very short-lived functions, it is fine to call them on the main isolate. +// They will block the Dart execution while running the native function, so +// only do this for native functions which are guaranteed to be short-lived. +FFI_PLUGIN_EXPORT intptr_t sum(intptr_t a, intptr_t b) { +#ifdef DEBUG + return a + b + 1000; +#else + return a + b; +#endif +} + +// A longer-lived native function, which occupies the thread calling it. +// +// Do not call these kind of native functions in the main isolate. They will +// block Dart execution. This will cause dropped frames in Flutter applications. +// Instead, call these native functions on a separate isolate. +FFI_PLUGIN_EXPORT intptr_t sum_long_running(intptr_t a, intptr_t b) { + // Simulate work. +#if _WIN32 + Sleep(5000); +#else + usleep(5000 * 1000); +#endif + return a + b; +} diff --git a/packages/flutter_tools/templates/package_ffi/src.tmpl/projectName.h.tmpl b/packages/flutter_tools/templates/package_ffi/src.tmpl/projectName.h.tmpl new file mode 100644 index 00000000000..084c64228f4 --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/src.tmpl/projectName.h.tmpl @@ -0,0 +1,30 @@ +#include +#include +#include + +#if _WIN32 +#include +#else +#include +#include +#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); diff --git a/packages/flutter_tools/templates/package_ffi/test/projectName_test.dart.tmpl b/packages/flutter_tools/templates/package_ffi/test/projectName_test.dart.tmpl new file mode 100644 index 00000000000..f19bce25aab --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/test/projectName_test.dart.tmpl @@ -0,0 +1,14 @@ +import 'package:test/test.dart'; + +import 'package:{{projectName}}/{{projectName}}.dart'; + +void main() { + test('invoke native function', () { + // Tests are run in debug mode. + expect(sum(24, 18), 1042); + }); + + test('invoke async native callback', () async { + expect(await sumAsync(24, 18), 42); + }); +} diff --git a/packages/flutter_tools/templates/plugin_shared/.metadata.tmpl b/packages/flutter_tools/templates/plugin_shared/.metadata.tmpl index 89546c72e76..8cb05910ce9 100644 --- a/packages/flutter_tools/templates/plugin_shared/.metadata.tmpl +++ b/packages/flutter_tools/templates/plugin_shared/.metadata.tmpl @@ -10,6 +10,9 @@ version: {{#withFfiPluginHook}} project_type: plugin_ffi {{/withFfiPluginHook}} +{{#withFfiPackage}} +project_type: package_ffi +{{/withFfiPackage}} {{#withPlatformChannelPluginHook}} project_type: plugin {{/withPlatformChannelPluginHook}} diff --git a/packages/flutter_tools/templates/plugin_shared/pubspec.yaml.tmpl b/packages/flutter_tools/templates/plugin_shared/pubspec.yaml.tmpl index 5b0d6b2967a..c8d9bbf05af 100644 --- a/packages/flutter_tools/templates/plugin_shared/pubspec.yaml.tmpl +++ b/packages/flutter_tools/templates/plugin_shared/pubspec.yaml.tmpl @@ -17,10 +17,10 @@ dependencies: plugin_platform_interface: ^2.0.2 dev_dependencies: -{{#withFfiPluginHook}} - ffi: ^2.0.1 - ffigen: ^6.1.2 -{{/withFfiPluginHook}} +{{#withFfi}} + ffi: ^2.0.2 + ffigen: ^9.0.0 +{{/withFfi}} flutter_test: sdk: flutter flutter_lints: ^2.0.0 diff --git a/packages/flutter_tools/templates/template_manifest.json b/packages/flutter_tools/templates/template_manifest.json index 3729d8909fa..f7cc14a3b95 100644 --- a/packages/flutter_tools/templates/template_manifest.json +++ b/packages/flutter_tools/templates/template_manifest.json @@ -248,6 +248,21 @@ "templates/package/README.md.tmpl", "templates/package/test/projectName_test.dart.tmpl", + "templates/package_ffi/.gitignore.tmpl", + "templates/package_ffi/.metadata.tmpl", + "templates/package_ffi/analysis_options.yaml.tmpl", + "templates/package_ffi/build.dart.tmpl", + "templates/package_ffi/CHANGELOG.md.tmpl", + "templates/package_ffi/ffigen.yaml.tmpl", + "templates/package_ffi/lib/projectName_bindings_generated.dart.tmpl", + "templates/package_ffi/lib/projectName.dart.tmpl", + "templates/package_ffi/LICENSE.tmpl", + "templates/package_ffi/pubspec.yaml.tmpl", + "templates/package_ffi/README.md.tmpl", + "templates/package_ffi/src.tmpl/projectName.c.tmpl", + "templates/package_ffi/src.tmpl/projectName.h.tmpl", + "templates/package_ffi/test/projectName_test.dart.tmpl", + "templates/plugin/android-java.tmpl/build.gradle.tmpl", "templates/plugin/android-java.tmpl/projectName_android.iml.tmpl", "templates/plugin/android-java.tmpl/src/main/java/androidIdentifier/pluginClass.java.tmpl", diff --git a/packages/flutter_tools/test/commands.shard/hermetic/create_usage_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/create_usage_test.dart index 088caf9e754..068bac8a2d0 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/create_usage_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/create_usage_test.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/doctor.dart'; import 'package:flutter_tools/src/doctor_validator.dart'; +import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/project.dart'; import 'package:test/fake.dart'; +import '../../src/common.dart'; import '../../src/context.dart'; +import '../../src/fakes.dart'; import '../../src/test_flutter_command_runner.dart'; import '../../src/testbed.dart'; @@ -79,6 +82,7 @@ void main() { globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'skeleton'), globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'module', 'common'), globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'package'), + globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'package_ffi'), globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'plugin'), globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'plugin_ffi'), globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'plugin_shared'), @@ -109,6 +113,7 @@ void main() { flutterManifest.writeAsStringSync('{"files":[]}'); }, overrides: { DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(), + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), }); }); @@ -133,6 +138,9 @@ void main() { await runner.run(['create', '--no-pub', '--template=plugin_ffi', 'testy5']); expect((await command.usageValues).commandCreateProjectType, 'plugin_ffi'); + + await runner.run(['create', '--no-pub', '--template=package_ffi', 'testy6']); + expect((await command.usageValues).commandCreateProjectType, 'package_ffi'); })); testUsingContext('set iOS host language type as usage value', () => testbed.run(() async { @@ -183,6 +191,29 @@ void main() { }, overrides: { Pub: () => fakePub, })); + + testUsingContext('package_ffi template not enabled', () async { + final CreateCommand command = CreateCommand(); + final CommandRunner runner = createTestCommandRunner(command); + + expect( + runner.run( + [ + 'create', + '--no-pub', + '--template=package_ffi', + 'my_ffi_package', + ], + ), + throwsUsageException( + message: '"package_ffi" is not an allowed value for option "template"', + ), + ); + }, overrides: { + FeatureFlags: () => TestFeatureFlags( + isNativeAssetsEnabled: false, // ignore: avoid_redundant_argument_values, If we graduate the feature to true by default, don't break this test. + ), + }); }); } diff --git a/packages/flutter_tools/test/commands.shard/permeable/build_bundle_test.dart b/packages/flutter_tools/test/commands.shard/permeable/build_bundle_test.dart index a0d34ace277..f20ec4999dd 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/build_bundle_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/build_bundle_test.dart @@ -856,6 +856,7 @@ class FakeBundleBuilder extends Fake implements BundleBuilder { String? applicationKernelFilePath, String? depfilePath, String? assetDirPath, + Uri? nativeAssets, @visibleForTesting BuildSystem? buildSystem, }) async {} } diff --git a/packages/flutter_tools/test/commands.shard/permeable/create_test.dart b/packages/flutter_tools/test/commands.shard/permeable/create_test.dart index 3d5c238a3a8..b9d0ed6d2d6 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/create_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/create_test.dart @@ -2610,6 +2610,18 @@ void main() { , throwsToolExit(message: 'The "--platforms" argument is not supported', exitCode: 2)); }); + testUsingContext('create an ffi package with --platforms throws error.', () async { + Cache.flutterRoot = '../..'; + + final CreateCommand command = CreateCommand(); + final CommandRunner runner = createTestCommandRunner(command); + await expectLater( + runner.run(['create', '--no-pub', '--template=package_ffi', '--platform=ios', projectDir.path]) + , throwsToolExit(message: 'The "--platforms" argument is not supported', exitCode: 2)); + }, overrides: { + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + }); + testUsingContext('create a plugin with android, delete then re-create folders', () async { Cache.flutterRoot = '../..'; @@ -3315,43 +3327,49 @@ void main() { FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true), }); - testUsingContext('FFI plugins error android language', () async { - final CreateCommand command = CreateCommand(); - final CommandRunner runner = createTestCommandRunner(command); - final List args = [ - 'create', - '--no-pub', - '--template=plugin_ffi', - '-a', - 'kotlin', - '--platforms=android', - projectDir.path, - ]; + for (final String template in ['package_ffi', 'plugin_ffi']) { + testUsingContext('$template error android language', () async { + final CreateCommand command = CreateCommand(); + final CommandRunner runner = createTestCommandRunner(command); + final List args = [ + 'create', + '--no-pub', + '--template=$template', + '-a', + 'kotlin', + if (template == 'plugin_ffi') '--platforms=android', + projectDir.path, + ]; - await expectLater( - runner.run(args), - throwsToolExit(message: 'The "android-language" option is not supported with the plugin_ffi template: the language will always be C or C++.'), - ); - }); + await expectLater( + runner.run(args), + throwsToolExit(message: 'The "android-language" option is not supported with the $template template: the language will always be C or C++.'), + ); + }, overrides: { + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + }); - testUsingContext('FFI plugins error ios language', () async { - final CreateCommand command = CreateCommand(); - final CommandRunner runner = createTestCommandRunner(command); - final List args = [ - 'create', - '--no-pub', - '--template=plugin_ffi', - '--ios-language', - 'swift', - '--platforms=ios', - projectDir.path, - ]; + testUsingContext('$template error ios language', () async { + final CreateCommand command = CreateCommand(); + final CommandRunner runner = createTestCommandRunner(command); + final List args = [ + 'create', + '--no-pub', + '--template=$template', + '--ios-language', + 'swift', + if (template == 'plugin_ffi') '--platforms=ios', + projectDir.path, + ]; - await expectLater( - runner.run(args), - throwsToolExit(message: 'The "ios-language" option is not supported with the plugin_ffi template: the language will always be C or C++.'), - ); - }); + await expectLater( + runner.run(args), + throwsToolExit(message: 'The "ios-language" option is not supported with the $template template: the language will always be C or C++.'), + ); + }, overrides: { + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + }); + } testUsingContext('FFI plugins error web platform', () async { final CreateCommand command = CreateCommand(); diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart index 5aab1be653b..31d9d86ac2d 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart @@ -71,10 +71,21 @@ void main() { throwsA(isA())); }); + const String emptyNativeAssets = ''' +format-version: + - 1 + - 0 + - 0 +native-assets: {} +'''; + testWithoutContext('KernelSnapshot handles null result from kernel compilation', () async { fileSystem.file('.dart_tool/package_config.json') ..createSync(recursive: true) ..writeAsStringSync('{"configVersion": 2, "packages":[]}'); + androidEnvironment.buildDir.childFile('native_assets.yaml') + ..createSync(recursive: true) + ..writeAsStringSync(emptyNativeAssets); final String build = androidEnvironment.buildDir.path; final String flutterPatchedSdkPath = artifacts.getArtifactPath( Artifact.flutterPatchedSdkPath, @@ -102,6 +113,8 @@ void main() { '$build/app.dill', '--depfile', '$build/kernel_snapshot.d', + '--native-assets', + '$build/native_assets.yaml', '--verbosity=error', 'file:///lib/main.dart', ], exitCode: 1), @@ -115,6 +128,9 @@ void main() { fileSystem.file('.dart_tool/package_config.json') ..createSync(recursive: true) ..writeAsStringSync('{"configVersion": 2, "packages":[]}'); + androidEnvironment.buildDir.childFile('native_assets.yaml') + ..createSync(recursive: true) + ..writeAsStringSync(emptyNativeAssets); final String build = androidEnvironment.buildDir.path; final String flutterPatchedSdkPath = artifacts.getArtifactPath( Artifact.flutterPatchedSdkPath, @@ -142,6 +158,8 @@ void main() { '$build/app.dill', '--depfile', '$build/kernel_snapshot.d', + '--native-assets', + '$build/native_assets.yaml', '--verbosity=error', 'file:///lib/main.dart', ], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n'), @@ -156,6 +174,9 @@ void main() { fileSystem.file('.dart_tool/package_config.json') ..createSync(recursive: true) ..writeAsStringSync('{"configVersion": 2, "packages":[]}'); + androidEnvironment.buildDir.childFile('native_assets.yaml') + ..createSync(recursive: true) + ..writeAsStringSync(emptyNativeAssets); final String build = androidEnvironment.buildDir.path; final String flutterPatchedSdkPath = artifacts.getArtifactPath( Artifact.flutterPatchedSdkPath, @@ -183,6 +204,8 @@ void main() { '$build/app.dill', '--depfile', '$build/kernel_snapshot.d', + '--native-assets', + '$build/native_assets.yaml', '--verbosity=error', 'file:///lib/main.dart', ], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n'), @@ -198,6 +221,9 @@ void main() { fileSystem.file('.dart_tool/package_config.json') ..createSync(recursive: true) ..writeAsStringSync('{"configVersion": 2, "packages":[]}'); + androidEnvironment.buildDir.childFile('native_assets.yaml') + ..createSync(recursive: true) + ..writeAsStringSync(emptyNativeAssets); final String build = androidEnvironment.buildDir.path; final String flutterPatchedSdkPath = artifacts.getArtifactPath( Artifact.flutterPatchedSdkPath, @@ -225,6 +251,8 @@ void main() { '$build/app.dill', '--depfile', '$build/kernel_snapshot.d', + '--native-assets', + '$build/native_assets.yaml', '--verbosity=error', 'foo', 'bar', @@ -242,6 +270,9 @@ void main() { fileSystem.file('.dart_tool/package_config.json') ..createSync(recursive: true) ..writeAsStringSync('{"configVersion": 2, "packages":[]}'); + androidEnvironment.buildDir.childFile('native_assets.yaml') + ..createSync(recursive: true) + ..writeAsStringSync(emptyNativeAssets); final String build = androidEnvironment.buildDir.path; final String flutterPatchedSdkPath = artifacts.getArtifactPath( Artifact.flutterPatchedSdkPath, @@ -268,6 +299,8 @@ void main() { '--incremental', '--initialize-from-dill', '$build/app.dill', + '--native-assets', + '$build/native_assets.yaml', '--verbosity=error', 'file:///lib/main.dart', ], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n'), @@ -284,6 +317,9 @@ void main() { fileSystem.file('.dart_tool/package_config.json') ..createSync(recursive: true) ..writeAsStringSync('{"configVersion": 2, "packages":[]}'); + androidEnvironment.buildDir.childFile('native_assets.yaml') + ..createSync(recursive: true) + ..writeAsStringSync(emptyNativeAssets); final String build = androidEnvironment.buildDir.path; final String flutterPatchedSdkPath = artifacts.getArtifactPath( Artifact.flutterPatchedSdkPath, @@ -309,6 +345,8 @@ void main() { '--incremental', '--initialize-from-dill', '$build/app.dill', + '--native-assets', + '$build/native_assets.yaml', '--verbosity=error', 'file:///lib/main.dart', ], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n'), @@ -338,6 +376,9 @@ void main() { fileSystem: fileSystem, logger: logger, ); + testEnvironment.buildDir.childFile('native_assets.yaml') + ..createSync(recursive: true) + ..writeAsStringSync(emptyNativeAssets); final String build = testEnvironment.buildDir.path; final String flutterPatchedSdkPath = artifacts.getArtifactPath( Artifact.flutterPatchedSdkPath, @@ -365,6 +406,8 @@ void main() { '--incremental', '--initialize-from-dill', '$build/app.dill', + '--native-assets', + '$build/native_assets.yaml', '--verbosity=error', 'file:///lib/main.dart', ], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey /build/653e11a8e6908714056a57cd6b4f602a/app.dill 0\n'), diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/native_assets_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/native_assets_test.dart new file mode 100644 index 00000000000..86660f3a790 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/build_system/targets/native_assets_test.dart @@ -0,0 +1,140 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/memory.dart'; +import 'package:file_testing/file_testing.dart'; +import 'package:flutter_tools/src/artifacts.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/build_system/build_system.dart'; +import 'package:flutter_tools/src/build_system/exceptions.dart'; +import 'package:flutter_tools/src/build_system/targets/native_assets.dart'; +import 'package:flutter_tools/src/features.dart'; +import 'package:flutter_tools/src/native_assets.dart'; +import 'package:native_assets_cli/native_assets_cli.dart' as native_assets_cli; + +import '../../../src/common.dart'; +import '../../../src/context.dart'; +import '../../../src/fakes.dart'; +import '../../fake_native_assets_build_runner.dart'; + +void main() { + late FakeProcessManager processManager; + late Environment iosEnvironment; + late Artifacts artifacts; + late FileSystem fileSystem; + late Logger logger; + + setUp(() { + processManager = FakeProcessManager.empty(); + logger = BufferLogger.test(); + artifacts = Artifacts.test(); + fileSystem = MemoryFileSystem.test(); + iosEnvironment = Environment.test( + fileSystem.currentDirectory, + defines: { + kBuildMode: BuildMode.profile.cliName, + kTargetPlatform: getNameForTargetPlatform(TargetPlatform.ios), + kIosArchs: 'arm64', + kSdkRoot: 'path/to/iPhoneOS.sdk', + }, + inputs: {}, + 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())); + }); + + testUsingContext('NativeAssets throws error if missing ios archs', () async { + iosEnvironment.defines.remove(kIosArchs); + expect(const NativeAssets().build(iosEnvironment), throwsA(isA())); + }); + + testUsingContext('NativeAssets throws error if missing sdk root', () async { + iosEnvironment.defines.remove(kSdkRoot); + expect(const NativeAssets().build(iosEnvironment), throwsA(isA())); + }); + + // The NativeAssets Target should _always_ be creating a yaml an d file. + // The caching logic depends on this. + for (final bool isNativeAssetsEnabled in [true, false]) { + final String postFix = isNativeAssetsEnabled ? 'enabled' : 'disabled'; + testUsingContext( + 'Successfull native_assets.yaml and native_assets.d creation with feature $postFix', + overrides: { + 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: { + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + }, + () async { + final NativeAssetsBuildRunner buildRunner = FakeNativeAssetsBuildRunner( + buildResult: FakeNativeAssetsBuilderResult(assets: [ + 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.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([ + 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([ + 'package:foo/foo.dart', + 'libfoo.dylib', + ]), + ); + }, + ); +} diff --git a/packages/flutter_tools/test/general.shard/compile_batch_test.dart b/packages/flutter_tools/test/general.shard/compile_batch_test.dart index 0b17a694bf4..e82fb93fe60 100644 --- a/packages/flutter_tools/test/general.shard/compile_batch_test.dart +++ b/packages/flutter_tools/test/general.shard/compile_batch_test.dart @@ -435,4 +435,55 @@ void main() { completer.complete(); await output; }); + + testWithoutContext('KernelCompiler passes native assets', () async { + final BufferLogger logger = BufferLogger.test(); + final StdoutHandler stdoutHandler = StdoutHandler(logger: logger, fileSystem: MemoryFileSystem.test()); + final Completer completer = Completer(); + + final KernelCompiler kernelCompiler = KernelCompiler( + artifacts: Artifacts.test(), + fileSystem: MemoryFileSystem.test(), + fileSystemRoots: [], + fileSystemScheme: '', + logger: logger, + processManager: FakeProcessManager.list([ + FakeCommand(command: const [ + '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 output = kernelCompiler.compile( + sdkRoot: '/path/to/sdkroot', + mainPath: '/path/to/main.dart', + buildMode: BuildMode.debug, + trackWidgetCreation: false, + dartDefines: const [], + packageConfig: PackageConfig.empty, + packagesPath: '.packages', + nativeAssets: 'path/to/native_assets.yaml', + ); + stdoutHandler.compilerOutput + ?.complete(const CompilerOutput('', 0, [])); + completer.complete(); + + expect((await output)?.outputFilename, ''); + }); } diff --git a/packages/flutter_tools/test/general.shard/custom_devices/custom_device_test.dart b/packages/flutter_tools/test/general.shard/custom_devices/custom_device_test.dart index 981ead6085b..fd18c1ce272 100644 --- a/packages/flutter_tools/test/general.shard/custom_devices/custom_device_test.dart +++ b/packages/flutter_tools/test/general.shard/custom_devices/custom_device_test.dart @@ -644,6 +644,7 @@ class FakeBundleBuilder extends Fake implements BundleBuilder { String? applicationKernelFilePath, String? depfilePath, String? assetDirPath, + Uri? nativeAssets, @visibleForTesting BuildSystem? buildSystem }) async {} } diff --git a/packages/flutter_tools/test/general.shard/devfs_test.dart b/packages/flutter_tools/test/general.shard/devfs_test.dart index 1f02688181a..d4b4e5fcf10 100644 --- a/packages/flutter_tools/test/general.shard/devfs_test.dart +++ b/packages/flutter_tools/test/general.shard/devfs_test.dart @@ -702,7 +702,18 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler { Future Function(Uri mainUri, List? invalidatedFiles)? onRecompile; @override - Future recompile(Uri mainUri, List? invalidatedFiles, {String? outputPath, PackageConfig? packageConfig, String? projectRootPath, FileSystem? fs, bool suppressErrors = false, bool checkDartPluginRegistry = false, File? dartPluginRegistrant}) { + Future recompile( + Uri mainUri, + List? invalidatedFiles, { + String? outputPath, + PackageConfig? packageConfig, + String? projectRootPath, + FileSystem? fs, + bool suppressErrors = false, + bool checkDartPluginRegistry = false, + File? dartPluginRegistrant, + Uri? nativeAssetsYaml, + }) { return onRecompile?.call(mainUri, invalidatedFiles) ?? Future.value(const CompilerOutput('', 1, [])); } diff --git a/packages/flutter_tools/test/general.shard/fake_native_assets_build_runner.dart b/packages/flutter_tools/test/general.shard/fake_native_assets_build_runner.dart new file mode 100644 index 00000000000..bcdd74c2d8b --- /dev/null +++ b/packages/flutter_tools/test/general.shard/fake_native_assets_build_runner.dart @@ -0,0 +1,91 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_tools/src/native_assets.dart'; +import 'package:native_assets_builder/native_assets_builder.dart' + as native_assets_builder; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:package_config/package_config_types.dart'; + +/// Mocks all logic instead of using `package:native_assets_builder`, which +/// relies on doing process calls to `pub` and the local file system. +class FakeNativeAssetsBuildRunner implements NativeAssetsBuildRunner { + FakeNativeAssetsBuildRunner({ + this.hasPackageConfigResult = true, + this.packagesWithNativeAssetsResult = const [], + 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 packagesWithNativeAssetsResult; + final CCompilerConfig cCompilerConfigResult; + + int buildInvocations = 0; + int dryRunInvocations = 0; + int hasPackageConfigInvocations = 0; + int packagesWithNativeAssetsInvocations = 0; + + @override + Future 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 dryRun({ + required bool includeParentEnvironment, + required LinkModePreference linkModePreference, + required OS targetOs, + required Uri workingDirectory, + }) async { + dryRunInvocations++; + return dryRunResult; + } + + @override + Future hasPackageConfig() async { + hasPackageConfigInvocations++; + return hasPackageConfigResult; + } + + @override + Future> packagesWithNativeAssets() async { + packagesWithNativeAssetsInvocations++; + return packagesWithNativeAssetsResult; + } + + @override + Future get cCompilerConfig async => cCompilerConfigResult; +} + +final class FakeNativeAssetsBuilderResult + implements native_assets_builder.BuildResult { + const FakeNativeAssetsBuilderResult({ + this.assets = const [], + this.dependencies = const [], + this.success = true, + }); + + @override + final List assets; + + @override + final List dependencies; + + @override + final bool success; +} diff --git a/packages/flutter_tools/test/general.shard/features_test.dart b/packages/flutter_tools/test/general.shard/features_test.dart index f15220e90b5..07d870b0f53 100644 --- a/packages/flutter_tools/test/general.shard/features_test.dart +++ b/packages/flutter_tools/test/general.shard/features_test.dart @@ -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); + }); }); } diff --git a/packages/flutter_tools/test/general.shard/flutter_project_metadata_test.dart b/packages/flutter_tools/test/general.shard/flutter_project_metadata_test.dart index f0033b9e871..6e2970faf8b 100644 --- a/packages/flutter_tools/test/general.shard/flutter_project_metadata_test.dart +++ b/packages/flutter_tools/test/general.shard/flutter_project_metadata_test.dart @@ -5,10 +5,13 @@ import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/flutter_project_metadata.dart'; import 'package:flutter_tools/src/project.dart'; import '../src/common.dart'; +import '../src/context.dart'; +import '../src/fakes.dart'; void main() { late FileSystem fileSystem; @@ -184,4 +187,16 @@ migration: expect(logger.traceText, contains('The key `create_revision` was not found')); }); + + testUsingContext('enabledValues does not contain packageFfi if native-assets not enabled', () { + expect(FlutterProjectType.enabledValues, isNot(contains(FlutterProjectType.packageFfi))); + expect(FlutterProjectType.enabledValues, contains(FlutterProjectType.plugin)); + }); + + testUsingContext('enabledValues contains packageFfi if natives-assets enabled', () { + expect(FlutterProjectType.enabledValues, contains(FlutterProjectType.packageFfi)); + expect(FlutterProjectType.enabledValues, contains(FlutterProjectType.plugin)); + }, overrides: { + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + }); } diff --git a/packages/flutter_tools/test/general.shard/hot_test.dart b/packages/flutter_tools/test/general.shard/hot_test.dart index b8be90ff0b4..b30361d3676 100644 --- a/packages/flutter_tools/test/general.shard/hot_test.dart +++ b/packages/flutter_tools/test/general.shard/hot_test.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - - import 'package:file/memory.dart'; import 'package:flutter_tools/src/application_package.dart'; import 'package:flutter_tools/src/artifacts.dart'; @@ -16,12 +14,15 @@ import 'package:flutter_tools/src/build_system/targets/shader_compiler.dart'; import 'package:flutter_tools/src/compile.dart'; import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/device.dart'; +import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:flutter_tools/src/resident_devtools_handler.dart'; import 'package:flutter_tools/src/resident_runner.dart'; import 'package:flutter_tools/src/run_hot.dart'; import 'package:flutter_tools/src/vmservice.dart'; +import 'package:native_assets_cli/native_assets_cli.dart' hide BuildMode, Target; +import 'package:native_assets_cli/native_assets_cli.dart' as native_assets_cli; import 'package:package_config/package_config.dart'; import 'package:test/fake.dart'; import 'package:vm_service/vm_service.dart' as vm_service; @@ -29,6 +30,7 @@ import 'package:vm_service/vm_service.dart' as vm_service; import '../src/common.dart'; import '../src/context.dart'; import '../src/fakes.dart'; +import 'fake_native_assets_build_runner.dart'; void main() { group('validateReloadReport', () { @@ -548,6 +550,134 @@ void main() { expect(flutterDevice2.stoppedEchoingDeviceLog, true); }); }); + + group('native assets', () { + late TestHotRunnerConfig testingConfig; + late FileSystem fileSystem; + + setUp(() { + fileSystem = MemoryFileSystem.test(); + testingConfig = TestHotRunnerConfig( + successfulHotRestartSetup: true, + ); + }); + testUsingContext('native assets restart', () async { + final FakeDevice device = FakeDevice(); + final FakeFlutterDevice fakeFlutterDevice = FakeFlutterDevice(device); + final List devices = [ + 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('bar', fileSystem.currentDirectory.uri), + ], + dryRunResult: FakeNativeAssetsBuilderResult( + assets: [ + 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: { + 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 devices = [ + 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('bar', fileSystem.currentDirectory.uri), + ], + dryRunResult: FakeNativeAssetsBuilderResult( + assets: [ + 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: { + HotRunnerConfig: () => testingConfig, + Artifacts: () => Artifacts.test(), + FileSystem: () => fileSystem, + Platform: () => FakePlatform(), + ProcessManager: () => FakeProcessManager.empty(), + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true, isMacOSEnabled: true), + }); + }); } class FakeDevFs extends Fake implements DevFS { @@ -580,6 +710,12 @@ class FakeDevFs extends Fake implements DevFS { // Until we fix that, we have to also ignore related lints here. // ignore: avoid_implementing_value_types class FakeDevice extends Fake implements Device { + FakeDevice({ + TargetPlatform targetPlatform = TargetPlatform.tester, + }) : _targetPlatform = targetPlatform; + + final TargetPlatform _targetPlatform; + bool disposed = false; @override @@ -595,7 +731,7 @@ class FakeDevice extends Fake implements Device { bool supportsFlutterExit = true; @override - Future get targetPlatform async => TargetPlatform.tester; + Future get targetPlatform async => _targetPlatform; @override Future get sdkNameAndVersion async => 'Tester'; @@ -658,6 +794,9 @@ class FakeFlutterDevice extends Fake implements FlutterDevice { required List invalidatedFiles, required PackageConfig packageConfig, }) => updateDevFSReportCallback(); + + @override + TargetPlatform? get targetPlatform => device._targetPlatform; } class TestFlutterDevice extends FlutterDevice { diff --git a/packages/flutter_tools/test/general.shard/ios/native_assets_test.dart b/packages/flutter_tools/test/general.shard/ios/native_assets_test.dart new file mode 100644 index 00000000000..0a6b7805785 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/ios/native_assets_test.dart @@ -0,0 +1,274 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:file_testing/file_testing.dart'; +import 'package:flutter_tools/src/artifacts.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/build_system/build_system.dart'; +import 'package:flutter_tools/src/features.dart'; +import 'package:flutter_tools/src/globals.dart' as globals; +import 'package:flutter_tools/src/ios/native_assets.dart'; +import 'package:native_assets_cli/native_assets_cli.dart' hide BuildMode, Target; +import 'package:native_assets_cli/native_assets_cli.dart' as native_assets_cli; +import 'package:package_config/package_config_types.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; +import '../../src/fakes.dart'; +import '../fake_native_assets_build_runner.dart'; + +void main() { + late FakeProcessManager processManager; + late Environment environment; + late Artifacts artifacts; + late FileSystem fileSystem; + late BufferLogger logger; + late Uri projectUri; + + setUp(() { + processManager = FakeProcessManager.empty(); + logger = BufferLogger.test(); + artifacts = Artifacts.test(); + fileSystem = MemoryFileSystem.test(); + environment = Environment.test( + fileSystem.currentDirectory, + inputs: {}, + 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: { + 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: { + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + await buildNativeAssetsIOS( + darwinArchs: [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: { + 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('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: { + 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('bar', projectUri), + ], + dryRunResult: FakeNativeAssetsBuilderResult( + assets: [ + 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.arm64], + environmentType: EnvironmentType.simulator, + projectUri: projectUri, + buildMode: BuildMode.debug, + fileSystem: fileSystem, + yamlParentDirectory: environment.buildDir.uri, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: [ + 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: { + 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.arm64], + environmentType: EnvironmentType.simulator, + projectUri: projectUri, + buildMode: BuildMode.debug, + fileSystem: fileSystem, + yamlParentDirectory: environment.buildDir.uri, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: [ + Package('bar', projectUri), + ], + ), + ); + expect( + environment.buildDir.childFile('native_assets.yaml'), + exists, + ); + }); + + testUsingContext('build with assets', overrides: { + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + ProcessManager: () => FakeProcessManager.list( + [ + const FakeCommand( + command: [ + 'lipo', + '-create', + '-output', + '/build/native_assets/ios/bar.dylib', + 'bar.dylib', + ], + ), + const FakeCommand( + command: [ + 'install_name_tool', + '-id', + '@executable_path/Frameworks/bar.dylib', + '/build/native_assets/ios/bar.dylib', + ], + ), + const FakeCommand( + command: [ + '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.arm64], + environmentType: EnvironmentType.simulator, + projectUri: projectUri, + buildMode: BuildMode.debug, + fileSystem: fileSystem, + yamlParentDirectory: environment.buildDir.uri, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: [ + Package('bar', projectUri), + ], + buildResult: FakeNativeAssetsBuilderResult( + assets: [ + 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, + ); + }); +} diff --git a/packages/flutter_tools/test/general.shard/macos/native_assets_test.dart b/packages/flutter_tools/test/general.shard/macos/native_assets_test.dart new file mode 100644 index 00000000000..8c9e865c8d7 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/macos/native_assets_test.dart @@ -0,0 +1,385 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/artifacts.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/build_system/build_system.dart'; +import 'package:flutter_tools/src/features.dart'; +import 'package:flutter_tools/src/globals.dart' as globals; +import 'package:flutter_tools/src/macos/native_assets.dart'; +import 'package:flutter_tools/src/native_assets.dart'; +import 'package:native_assets_cli/native_assets_cli.dart' hide BuildMode, Target; +import 'package:native_assets_cli/native_assets_cli.dart' as native_assets_cli; +import 'package:package_config/package_config_types.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; +import '../../src/fakes.dart'; +import '../fake_native_assets_build_runner.dart'; + +void main() { + late FakeProcessManager processManager; + late Environment environment; + late Artifacts artifacts; + late FileSystem fileSystem; + late BufferLogger logger; + late Uri projectUri; + + setUp(() { + processManager = FakeProcessManager.empty(); + logger = BufferLogger.test(); + artifacts = Artifacts.test(); + fileSystem = MemoryFileSystem.test(); + environment = Environment.test( + fileSystem.currentDirectory, + inputs: {}, + 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: { + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + expect( + await dryRunNativeAssetsMacOS( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + hasPackageConfigResult: false, + ), + ), + null, + ); + expect( + (globals.logger as BufferLogger).traceText, + contains('No package config found. Skipping native assets compilation.'), + ); + }); + + testUsingContext('build with no package config', overrides: { + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + await buildNativeAssetsMacOS( + darwinArchs: [DarwinArch.arm64], + projectUri: projectUri, + buildMode: BuildMode.debug, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + hasPackageConfigResult: false, + ), + ); + expect( + (globals.logger as BufferLogger).traceText, + contains('No package config found. Skipping native assets compilation.'), + ); + }); + + testUsingContext('dry run for multiple OSes with no package config', overrides: { + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + await dryRunNativeAssetsMultipeOSes( + projectUri: projectUri, + fileSystem: fileSystem, + targetPlatforms: [ + TargetPlatform.darwin, + TargetPlatform.ios, + ], + buildRunner: FakeNativeAssetsBuildRunner( + hasPackageConfigResult: false, + ), + ); + expect( + (globals.logger as BufferLogger).traceText, + contains('No package config found. Skipping native assets compilation.'), + ); + }); + + testUsingContext('dry run with assets but not enabled', overrides: { + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + expect( + () => dryRunNativeAssetsMacOS( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: [ + Package('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: { + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + final Uri? nativeAssetsYaml = await dryRunNativeAssetsMacOS( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: [ + Package('bar', projectUri), + ], + dryRunResult: FakeNativeAssetsBuilderResult( + assets: [ + Asset( + id: 'package:bar/bar.dart', + linkMode: LinkMode.dynamic, + target: native_assets_cli.Target.macOSArm64, + path: AssetAbsolutePath(Uri.file('bar.dylib')), + ), + Asset( + id: 'package:bar/bar.dart', + linkMode: LinkMode.dynamic, + target: native_assets_cli.Target.macOSX64, + path: AssetAbsolutePath(Uri.file('bar.dylib')), + ), + ], + ), + ), + ); + expect( + nativeAssetsYaml, + projectUri.resolve('build/native_assets/macos/native_assets.yaml'), + ); + expect( + await fileSystem.file(nativeAssetsYaml).readAsString(), + contains('package:bar/bar.dart'), + ); + }); + + testUsingContext('build with assets but not enabled', overrides: { + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + expect( + () => buildNativeAssetsMacOS( + darwinArchs: [DarwinArch.arm64], + projectUri: projectUri, + buildMode: BuildMode.debug, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: [ + 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: { + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + final (Uri? nativeAssetsYaml, _) = await buildNativeAssetsMacOS( + darwinArchs: [DarwinArch.arm64], + projectUri: projectUri, + buildMode: BuildMode.debug, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: [ + Package('bar', projectUri), + ], + ), + ); + expect( + nativeAssetsYaml, + projectUri.resolve('build/native_assets/macos/native_assets.yaml'), + ); + expect( + await fileSystem.file(nativeAssetsYaml).readAsString(), + isNot(contains('package:bar/bar.dart')), + ); + }); + + for (final bool flutterTester in [false, true]) { + String testName = ''; + if (flutterTester) { + testName += ' flutter tester'; + } + testUsingContext('build with assets$testName', overrides: { + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + ProcessManager: () => FakeProcessManager.list( + [ + const FakeCommand( + command: [ + 'lipo', + '-create', + '-output', + '/build/native_assets/macos/bar.dylib', + 'bar.dylib', + ], + ), + const FakeCommand( + command: [ + 'install_name_tool', + '-id', + '@executable_path/Frameworks/bar.dylib', + '/build/native_assets/macos/bar.dylib', + ], + ), + const FakeCommand( + command: [ + 'codesign', + '--force', + '--sign', + '-', + '--timestamp=none', + '/build/native_assets/macos/bar.dylib', + ], + ), + ], + ), + }, () async { + if (const LocalPlatform().isWindows) { + return; // Backslashes in commands, but we will never run these commands on Windows. + } + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + final (Uri? nativeAssetsYaml, _) = await buildNativeAssetsMacOS( + darwinArchs: [DarwinArch.arm64], + projectUri: projectUri, + buildMode: BuildMode.debug, + fileSystem: fileSystem, + flutterTester: flutterTester, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: [ + Package('bar', projectUri), + ], + buildResult: FakeNativeAssetsBuilderResult( + assets: [ + Asset( + id: 'package:bar/bar.dart', + linkMode: LinkMode.dynamic, + target: native_assets_cli.Target.macOSArm64, + path: AssetAbsolutePath(Uri.file('bar.dylib')), + ), + ], + ), + ), + ); + expect( + nativeAssetsYaml, + projectUri.resolve('build/native_assets/macos/native_assets.yaml'), + ); + expect( + await fileSystem.file(nativeAssetsYaml).readAsString(), + stringContainsInOrder([ + 'package:bar/bar.dart', + if (flutterTester) + // Tests run on host system, so the have the full path on the system. + '- ${projectUri.resolve('/build/native_assets/macos/bar.dylib').toFilePath()}' + else + // Apps are a bundle with the dylibs on their dlopen path. + '- bar.dylib', + ]), + ); + }); + } + + testUsingContext('static libs not supported', overrides: { + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + expect( + () => dryRunNativeAssetsMacOS( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: [ + Package('bar', projectUri), + ], + dryRunResult: FakeNativeAssetsBuilderResult( + assets: [ + Asset( + id: 'package:bar/bar.dart', + linkMode: LinkMode.static, + target: native_assets_cli.Target.macOSArm64, + path: AssetAbsolutePath(Uri.file('bar.a')), + ), + Asset( + id: 'package:bar/bar.dart', + linkMode: LinkMode.static, + target: native_assets_cli.Target.macOSX64, + path: AssetAbsolutePath(Uri.file('bar.a')), + ), + ], + ), + ), + ), + throwsToolExit( + message: 'Native asset(s) package:bar/bar.dart have their link mode set to ' + 'static, but this is not yet supported. ' + 'For more info see https://github.com/dart-lang/sdk/issues/49418.', + ), + ); + }); + + // This logic is mocked in the other tests to avoid having test order + // randomization causing issues with what processes are invoked. + // Exercise the parsing of the process output in this separate test. + testUsingContext('NativeAssetsBuildRunnerImpl.cCompilerConfig', overrides: { + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + ProcessManager: () => FakeProcessManager.list( + [ + const FakeCommand( + command: ['xcrun', 'clang', '--version'], + stdout: ''' +Apple clang version 14.0.0 (clang-1400.0.29.202) +Target: arm64-apple-darwin22.6.0 +Thread model: posix +InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin''', + ) + ], + ), + }, () async { + if (!const LocalPlatform().isMacOS) { + // TODO(dacoharkes): Implement other OSes. https://github.com/flutter/flutter/issues/129757 + return; + } + + final NativeAssetsBuildRunner runner = NativeAssetsBuildRunnerImpl(projectUri, fileSystem, logger); + final CCompilerConfig result = await runner.cCompilerConfig; + expect( + result.cc, + Uri.file( + '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang', + ), + ); + }); +} diff --git a/packages/flutter_tools/test/general.shard/preview_device_test.dart b/packages/flutter_tools/test/general.shard/preview_device_test.dart index fa94d8600f3..876dbc64950 100644 --- a/packages/flutter_tools/test/general.shard/preview_device_test.dart +++ b/packages/flutter_tools/test/general.shard/preview_device_test.dart @@ -99,6 +99,7 @@ class FakeBundleBuilder extends Fake implements BundleBuilder { String? applicationKernelFilePath, String? depfilePath, String? assetDirPath, + Uri? nativeAssets, @visibleForTesting BuildSystem? buildSystem }) async { final Directory assetDirectory = fileSystem diff --git a/packages/flutter_tools/test/general.shard/resident_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_runner_test.dart index bd93c7c85fc..42b8dafb90d 100644 --- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart @@ -18,6 +18,7 @@ import 'package:flutter_tools/src/base/io.dart' as io; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/build_system/build_system.dart'; import 'package:flutter_tools/src/build_system/targets/scene_importer.dart'; import 'package:flutter_tools/src/build_system/targets/shader_compiler.dart'; import 'package:flutter_tools/src/compile.dart'; @@ -25,6 +26,7 @@ import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device_port_forwarder.dart'; +import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; @@ -34,6 +36,9 @@ import 'package:flutter_tools/src/run_cold.dart'; import 'package:flutter_tools/src/run_hot.dart'; import 'package:flutter_tools/src/version.dart'; import 'package:flutter_tools/src/vmservice.dart'; +import 'package:native_assets_cli/native_assets_cli.dart' + hide BuildMode, Target; +import 'package:native_assets_cli/native_assets_cli.dart' as native_assets_cli; import 'package:package_config/package_config.dart'; import 'package:test/fake.dart'; import 'package:vm_service/vm_service.dart' as vm_service; @@ -43,6 +48,7 @@ import '../src/context.dart'; import '../src/fake_vm_services.dart'; import '../src/fakes.dart'; import '../src/testbed.dart'; +import 'fake_native_assets_build_runner.dart'; final vm_service.Event fakeUnpausedEvent = vm_service.Event( kind: vm_service.EventKind.kResume, @@ -2322,6 +2328,82 @@ flutter: expect(flutterDevice.devFS!.hasSetAssetDirectory, true); expect(fakeVmServiceHost!.hasRemainingExpectations, false); })); + + testUsingContext( + 'native assets', + () => testbed.run(() async { + final FileSystem fileSystem = globals.fs; + final Environment environment = Environment.test( + fileSystem.currentDirectory, + inputs: {}, + 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: [ + listViews, + listViews, + ]); + globals.fs + .file(globals.fs.path.join('lib', 'main.dart')) + .createSync(recursive: true); + final FakeNativeAssetsBuildRunner buildRunner = FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: [ + Package('bar', projectUri), + ], + dryRunResult: FakeNativeAssetsBuilderResult( + assets: [ + Asset( + id: 'package:bar/bar.dart', + linkMode: LinkMode.dynamic, + target: native_assets_cli.Target.macOSArm64, + path: AssetAbsolutePath(Uri.file('bar.dylib')), + ), + ], + ), + ); + residentRunner = HotRunner( + [ + 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: { + ProcessManager: () => FakeProcessManager.any(), + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true, isMacOSEnabled: true), + }); } // This implements [dds.DartDevelopmentService], not the [DartDevelopmentService] @@ -2386,7 +2468,7 @@ class FakeFlutterDevice extends Fake implements FlutterDevice { DevelopmentShaderCompiler get developmentShaderCompiler => const FakeShaderCompiler(); @override - TargetPlatform get targetPlatform => TargetPlatform.android; + TargetPlatform targetPlatform = TargetPlatform.android; @override Stream get vmServiceUris => Stream.value(testUri); @@ -2521,6 +2603,7 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler { bool suppressErrors = false, bool checkDartPluginRegistry = false, File? dartPluginRegistrant, + Uri? nativeAssetsYaml, }) async { didSuppressErrors = suppressErrors; return nextOutput ?? const CompilerOutput('foo.dill', 0, []); diff --git a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart index 01a1f098ed9..4fc88ae771b 100644 --- a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart @@ -1444,6 +1444,7 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler { bool suppressErrors = false, bool checkDartPluginRegistry = false, File? dartPluginRegistrant, + Uri? nativeAssetsYaml, }) async { return const CompilerOutput('foo.dill', 0, []); } diff --git a/packages/flutter_tools/test/general.shard/test/test_compiler_test.dart b/packages/flutter_tools/test/general.shard/test/test_compiler_test.dart index 7aa5bbb4dda..429e94890d7 100644 --- a/packages/flutter_tools/test/general.shard/test/test_compiler_test.dart +++ b/packages/flutter_tools/test/general.shard/test/test_compiler_test.dart @@ -234,6 +234,7 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler { bool suppressErrors = false, bool checkDartPluginRegistry = false, File? dartPluginRegistrant, + Uri? nativeAssetsYaml, }) async { if (compilerOutput != null) { fileSystem!.file(compilerOutput!.outputFilename).createSync(recursive: true); diff --git a/packages/flutter_tools/test/general.shard/tester/flutter_tester_test.dart b/packages/flutter_tools/test/general.shard/tester/flutter_tester_test.dart index 6cc826e7dee..165075e067f 100644 --- a/packages/flutter_tools/test/general.shard/tester/flutter_tester_test.dart +++ b/packages/flutter_tools/test/general.shard/tester/flutter_tester_test.dart @@ -98,7 +98,6 @@ void main() { artifacts: Artifacts.test(), logger: BufferLogger.test(), flutterVersion: FakeFlutterVersion(), - operatingSystemUtils: FakeOperatingSystemUtils(), ); logLines = []; device.getLogReader().logLines.listen(logLines.add); @@ -213,7 +212,6 @@ FlutterTesterDevices setUpFlutterTesterDevices() { processManager: FakeProcessManager.any(), fileSystem: MemoryFileSystem.test(), flutterVersion: FakeFlutterVersion(), - operatingSystemUtils: FakeOperatingSystemUtils(), ); } diff --git a/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart b/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart index e457408c05f..6936829941b 100644 --- a/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart +++ b/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart @@ -1190,6 +1190,7 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler { bool suppressErrors = false, bool checkDartPluginRegistry = false, File? dartPluginRegistrant, + Uri? nativeAssetsYaml, }) async { return output; } diff --git a/packages/flutter_tools/test/host_cross_arch.shard/ios_content_validation_test.dart b/packages/flutter_tools/test/host_cross_arch.shard/ios_content_validation_test.dart index 3b3de5f7921..c35287c6e04 100644 --- a/packages/flutter_tools/test/host_cross_arch.shard/ios_content_validation_test.dart +++ b/packages/flutter_tools/test/host_cross_arch.shard/ios_content_validation_test.dart @@ -254,6 +254,7 @@ void main() { 'VERBOSE_SCRIPT_LOGGING': '1', 'FLUTTER_BUILD_MODE': 'release', 'ACTION': 'install', + 'FLUTTER_BUILD_DIR': 'build', // Skip bitcode stripping since we just checked that above. }, ); diff --git a/packages/flutter_tools/test/integration.shard/native_assets_test.dart b/packages/flutter_tools/test/integration.shard/native_assets_test.dart new file mode 100644 index 00000000000..e539e74a27a --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/native_assets_test.dart @@ -0,0 +1,360 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This test exercises the embedding of the native assets mapping in dill files. +// An initial dill file is created by `flutter assemble` and used for running +// the application. This dill must contain the mapping. +// When doing hot reload, this mapping must stay in place. +// When doing a hot restart, a new dill file is pushed. This dill file must also +// contain the native assets mapping. +// When doing a hot reload, this mapping must stay in place. + +@Timeout(Duration(minutes: 10)) +library; + +import 'dart:io'; + +import 'package:file/file.dart'; +import 'package:file_testing/file_testing.dart'; + +import '../src/common.dart'; +import 'test_utils.dart' show fileSystem, platform; +import 'transition_test_utils.dart'; + +final String hostOs = platform.operatingSystem; + +final List devices = [ + 'flutter-tester', + hostOs, +]; + +final List buildSubcommands = [ + hostOs, + if (hostOs == 'macos') 'ios', +]; + +final List add2appBuildSubcommands = [ + if (hostOs == 'macos') ...[ + 'macos-framework', + 'ios-framework', + ], +]; + +/// The build modes to target for each flutter command that supports passing +/// a build mode. +/// +/// The flow of compiling kernel as well as bundling dylibs can differ based on +/// build mode, so we should cover this. +const List buildModes = [ + 'debug', + 'profile', + 'release', +]; + +const String packageName = 'package_with_native_assets'; + +const String exampleAppName = '${packageName}_example'; + +const String dylibName = 'lib$packageName.dylib'; + +void main() { + if (!platform.isMacOS) { + // TODO(dacoharkes): Implement other OSes. https://github.com/flutter/flutter/issues/129757 + return; + } + + setUpAll(() { + processManager.runSync([ + flutterBin, + 'config', + '--enable-native-assets', + ]); + }); + + for (final String device in devices) { + for (final String buildMode in buildModes) { + if (device == 'flutter-tester' && buildMode != 'debug') { + continue; + } + final String hotReload = buildMode == 'debug' ? ' hot reload and hot restart' : ''; + testWithoutContext('flutter run$hotReload with native assets $device $buildMode', () async { + await inTempDir((Directory tempDirectory) async { + final Directory packageDirectory = await createTestProject(packageName, tempDirectory); + final Directory exampleDirectory = packageDirectory.childDirectory('example'); + + final ProcessTestResult result = await runFlutter( + [ + 'run', + '-d$device', + '--$buildMode', + ], + exampleDirectory.path, + [ + Multiple([ + 'Flutter run key commands.', + ], handler: (String line) { + if (buildMode == 'debug') { + // Do a hot reload diff on the initial dill file. + return 'r'; + } else { + // No hot reload and hot restart in release mode. + return 'q'; + } + }), + if (buildMode == 'debug') ...[ + Barrier( + 'Performing hot reload...'.padRight(progressMessageWidth), + logging: true, + ), + Multiple([ + RegExp('Reloaded .*'), + ], handler: (String line) { + // Do a hot restart, pushing a new complete dill file. + return 'R'; + }), + Barrier('Performing hot restart...'.padRight(progressMessageWidth)), + Multiple([ + RegExp('Restarted application .*'), + ], handler: (String line) { + // Do another hot reload, pushing a diff to the second dill file. + return 'r'; + }), + Barrier( + 'Performing hot reload...'.padRight(progressMessageWidth), + logging: true, + ), + Multiple([ + RegExp('Reloaded .*'), + ], handler: (String line) { + return 'q'; + }), + ], + const Barrier('Application finished.'), + ], + logging: false, + ); + if (result.exitCode != 0) { + throw Exception('flutter run failed: ${result.exitCode}\n${result.stderr}\n${result.stdout}'); + } + final String stdout = result.stdout.join('\n'); + // Check that we did not fail to resolve the native function in the + // dynamic library. + expect(stdout, isNot(contains("Invalid argument(s): Couldn't resolve native function 'sum'"))); + // And also check that we did not have any other exceptions that might + // shadow the exception we would have gotten. + expect(stdout, isNot(contains('EXCEPTION CAUGHT BY WIDGETS LIBRARY'))); + + if (device == 'macos') { + expectDylibIsBundledMacOS(exampleDirectory, buildMode); + } + if (device == hostOs) { + expectCCompilerIsConfigured(exampleDirectory); + } + }); + }); + } + } + + testWithoutContext('flutter test with native assets', () async { + await inTempDir((Directory tempDirectory) async { + final Directory packageDirectory = await createTestProject(packageName, tempDirectory); + + final ProcessTestResult result = await runFlutter( + [ + 'test', + ], + packageDirectory.path, + [ + Barrier(RegExp('.* All tests passed!')), + ], + logging: false, + ); + if (result.exitCode != 0) { + throw Exception('flutter test failed: ${result.exitCode}\n${result.stderr}\n${result.stdout}'); + } + }); + }); + + for (final String buildSubcommand in buildSubcommands) { + for (final String buildMode in buildModes) { + testWithoutContext('flutter build $buildSubcommand with native assets $buildMode', () async { + await inTempDir((Directory tempDirectory) async { + final Directory packageDirectory = await createTestProject(packageName, tempDirectory); + final Directory exampleDirectory = packageDirectory.childDirectory('example'); + + final ProcessResult result = processManager.runSync( + [ + flutterBin, + 'build', + buildSubcommand, + '--$buildMode', + if (buildSubcommand == 'ios') '--no-codesign', + ], + workingDirectory: exampleDirectory.path, + ); + if (result.exitCode != 0) { + throw Exception('flutter build failed: ${result.exitCode}\n${result.stderr}\n${result.stdout}'); + } + + if (buildSubcommand == 'macos') { + expectDylibIsBundledMacOS(exampleDirectory, buildMode); + } else if (buildSubcommand == 'ios') { + expectDylibIsBundledIos(exampleDirectory, buildMode); + } + expectCCompilerIsConfigured(exampleDirectory); + }); + }); + } + + // This could be an hermetic unit test if the native_assets_builder + // could mock process runs and file system. + // https://github.com/dart-lang/native/issues/90. + testWithoutContext('flutter build $buildSubcommand error on static libraries', () async { + await inTempDir((Directory tempDirectory) async { + final Directory packageDirectory = await createTestProject(packageName, tempDirectory); + final File buildDotDart = packageDirectory.childFile('build.dart'); + final String buildDotDartContents = await buildDotDart.readAsString(); + // Overrides the build to output static libraries. + final String buildDotDartContentsNew = buildDotDartContents.replaceFirst( + 'final buildConfig = await BuildConfig.fromArgs(args);', + r''' + final buildConfig = await BuildConfig.fromArgs([ + '-D${LinkModePreference.configKey}=${LinkModePreference.static}', + ...args, + ]); +''', + ); + expect(buildDotDartContentsNew, isNot(buildDotDartContents)); + await buildDotDart.writeAsString(buildDotDartContentsNew); + final Directory exampleDirectory = packageDirectory.childDirectory('example'); + + final ProcessResult result = processManager.runSync( + [ + flutterBin, + 'build', + buildSubcommand, + if (buildSubcommand == 'ios') '--no-codesign', + ], + workingDirectory: exampleDirectory.path, + ); + expect(result.exitCode, isNot(0)); + expect(result.stderr, contains('link mode set to static, but this is not yet supported')); + }); + }); + } + + for (final String add2appBuildSubcommand in add2appBuildSubcommands) { + testWithoutContext('flutter build $add2appBuildSubcommand with native assets', () async { + await inTempDir((Directory tempDirectory) async { + final Directory packageDirectory = await createTestProject(packageName, tempDirectory); + final Directory exampleDirectory = packageDirectory.childDirectory('example'); + + final ProcessResult result = processManager.runSync( + [ + flutterBin, + 'build', + add2appBuildSubcommand, + ], + workingDirectory: exampleDirectory.path, + ); + if (result.exitCode != 0) { + throw Exception('flutter build failed: ${result.exitCode}\n${result.stderr}\n${result.stdout}'); + } + + for (final String buildMode in buildModes) { + expectDylibIsBundledWithFrameworks(exampleDirectory, buildMode, add2appBuildSubcommand.replaceAll('-framework', '')); + } + expectCCompilerIsConfigured(exampleDirectory); + }); + }); + } +} + +/// For `flutter build` we can't easily test whether running the app works. +/// Check that we have the dylibs in the app. +void expectDylibIsBundledMacOS(Directory appDirectory, String buildMode) { + final Directory appBundle = appDirectory.childDirectory('build/$hostOs/Build/Products/${buildMode.upperCaseFirst()}/$exampleAppName.app'); + expect(appBundle, exists); + final Directory dylibsFolder = appBundle.childDirectory('Contents/Frameworks'); + expect(dylibsFolder, exists); + final File dylib = dylibsFolder.childFile(dylibName); + expect(dylib, exists); +} + +void expectDylibIsBundledIos(Directory appDirectory, String buildMode) { + final Directory appBundle = appDirectory.childDirectory('build/ios/${buildMode.upperCaseFirst()}-iphoneos/Runner.app'); + expect(appBundle, exists); + final Directory dylibsFolder = appBundle.childDirectory('Frameworks'); + expect(dylibsFolder, exists); + final File dylib = dylibsFolder.childFile(dylibName); + expect(dylib, exists); +} + +/// For `flutter build` we can't easily test whether running the app works. +/// Check that we have the dylibs in the app. +void expectDylibIsBundledWithFrameworks(Directory appDirectory, String buildMode, String os) { + final Directory frameworksFolder = appDirectory.childDirectory('build/$os/framework/${buildMode.upperCaseFirst()}'); + expect(frameworksFolder, exists); + final File dylib = frameworksFolder.childFile(dylibName); + expect(dylib, exists); +} + +/// Check that the native assets are built with the C Compiler that Flutter uses. +/// +/// This inspects the build configuration to see if the C compiler was configured. +void expectCCompilerIsConfigured(Directory appDirectory) { + final Directory nativeAssetsBuilderDir = appDirectory.childDirectory('.dart_tool/native_assets_builder/'); + for (final Directory subDir in nativeAssetsBuilderDir.listSync().whereType()) { + final File config = subDir.childFile('config.yaml'); + expect(config, exists); + final String contents = config.readAsStringSync(); + // Dry run does not pass compiler info. + if (contents.contains('dry_run: true')) { + continue; + } + expect(contents, contains('cc: ')); + } +} + +extension on String { + String upperCaseFirst() { + return replaceFirst(this[0], this[0].toUpperCase()); + } +} + +Future createTestProject(String packageName, Directory tempDirectory) async { + final ProcessResult result = processManager.runSync( + [ + flutterBin, + 'create', + '--template=package_ffi', + packageName, + ], + workingDirectory: tempDirectory.path, + ); + + if (result.exitCode != 0) { + throw Exception('flutter create failed: ${result.exitCode}\n${result.stderr}\n${result.stdout}'); + } + + final Directory packageDirectory = tempDirectory.childDirectory(packageName); + + // No platform-specific boilerplate files. + expect(packageDirectory.childDirectory('android/'), isNot(exists)); + expect(packageDirectory.childDirectory('ios/'), isNot(exists)); + expect(packageDirectory.childDirectory('linux/'), isNot(exists)); + expect(packageDirectory.childDirectory('macos/'), isNot(exists)); + expect(packageDirectory.childDirectory('windows/'), isNot(exists)); + + return packageDirectory; +} + +Future inTempDir(Future Function(Directory tempDirectory) fun) async { + final Directory tempDirectory = fileSystem.directory(fileSystem.systemTempDirectory.createTempSync().resolveSymbolicLinksSync()); + try { + await fun(tempDirectory); + } finally { + tryToDelete(tempDirectory); + } +} diff --git a/packages/flutter_tools/test/integration.shard/overall_experience_test.dart b/packages/flutter_tools/test/integration.shard/overall_experience_test.dart index e9071b5a557..8cc4fff7e37 100644 --- a/packages/flutter_tools/test/integration.shard/overall_experience_test.dart +++ b/packages/flutter_tools/test/integration.shard/overall_experience_test.dart @@ -26,310 +26,11 @@ @Tags(['no-shuffle']) library; -import 'dart:async'; -import 'dart:convert'; import 'dart:io'; -import 'package:meta/meta.dart'; -import 'package:process/process.dart'; - import '../src/common.dart'; import 'test_utils.dart' show fileSystem; - -const ProcessManager processManager = LocalProcessManager(); -final String flutterRoot = getFlutterRoot(); -final String flutterBin = fileSystem.path.join(flutterRoot, 'bin', 'flutter'); - -void debugPrint(String message) { - // This is called to intentionally print debugging output when a test is - // either taking too long or has failed. - // ignore: avoid_print - print(message); -} - -typedef LineHandler = String? Function(String line); - -abstract class Transition { - const Transition({this.handler, this.logging}); - - /// Callback that is invoked when the transition matches. - /// - /// This should not throw, even if the test is failing. (For example, don't use "expect" - /// in these callbacks.) Throwing here would prevent the [runFlutter] function from running - /// to completion, which would leave zombie `flutter` processes around. - final LineHandler? handler; - - /// Whether to enable or disable logging when this transition is matched. - /// - /// The default value, null, leaves the logging state unaffected. - final bool? logging; - - bool matches(String line); - - @protected - bool lineMatchesPattern(String line, Pattern pattern) { - if (pattern is String) { - return line == pattern; - } - return line.contains(pattern); - } - - @protected - String describe(Pattern pattern) { - if (pattern is String) { - return '"$pattern"'; - } - if (pattern is RegExp) { - return '/${pattern.pattern}/'; - } - return '$pattern'; - } -} - -class Barrier extends Transition { - const Barrier(this.pattern, {super.handler, super.logging}); - final Pattern pattern; - - @override - bool matches(String line) => lineMatchesPattern(line, pattern); - - @override - String toString() => describe(pattern); -} - -class Multiple extends Transition { - Multiple(List patterns, { - super.handler, - super.logging, - }) : _originalPatterns = patterns, - patterns = patterns.toList(); - - final List _originalPatterns; - final List patterns; - - @override - bool matches(String line) { - for (int index = 0; index < patterns.length; index += 1) { - if (lineMatchesPattern(line, patterns[index])) { - patterns.removeAt(index); - break; - } - } - return patterns.isEmpty; - } - - @override - String toString() { - if (patterns.isEmpty) { - return '${_originalPatterns.map(describe).join(', ')} (all matched)'; - } - return '${_originalPatterns.map(describe).join(', ')} (matched ${_originalPatterns.length - patterns.length} so far)'; - } -} - -class LogLine { - const LogLine(this.channel, this.stamp, this.message); - final String channel; - final String stamp; - final String message; - - bool get couldBeCrash => message.contains('Oops; flutter has exited unexpectedly:'); - - @override - String toString() => '$stamp $channel: $message'; - - void printClearly() { - debugPrint('$stamp $channel: ${clarify(message)}'); - } - - static String clarify(String line) { - return line.runes.map((int rune) { - if (rune >= 0x20 && rune <= 0x7F) { - return String.fromCharCode(rune); - } - switch (rune) { - case 0x00: return ''; - case 0x07: return ''; - case 0x08: return ''; - case 0x09: return ''; - case 0x0A: return ''; - case 0x0D: return ''; - } - return '<${rune.toRadixString(16).padLeft(rune <= 0xFF ? 2 : rune <= 0xFFFF ? 4 : 5, '0')}>'; - }).join(); - } -} - -class ProcessTestResult { - const ProcessTestResult(this.exitCode, this.logs); - final int exitCode; - final List logs; - - List get stdout { - return logs - .where((LogLine log) => log.channel == 'stdout') - .map((LogLine log) => log.message) - .toList(); - } - - List get stderr { - return logs - .where((LogLine log) => log.channel == 'stderr') - .map((LogLine log) => log.message) - .toList(); - } - - @override - String toString() => 'exit code $exitCode\nlogs:\n ${logs.join('\n ')}\n'; -} - -Future runFlutter( - List arguments, - String workingDirectory, - List transitions, { - bool debug = false, - bool logging = true, - Duration expectedMaxDuration = const Duration(minutes: 10), // must be less than test timeout of 15 minutes! See ../../dart_test.yaml. -}) async { - final Stopwatch clock = Stopwatch()..start(); - final Process process = await processManager.start( - [flutterBin, ...arguments], - workingDirectory: workingDirectory, - ); - final List logs = []; - int nextTransition = 0; - void describeStatus() { - if (transitions.isNotEmpty) { - debugPrint('Expected state transitions:'); - for (int index = 0; index < transitions.length; index += 1) { - debugPrint( - '${index.toString().padLeft(5)} ' - '${index < nextTransition ? 'ALREADY MATCHED ' : - index == nextTransition ? 'NOW WAITING FOR>' : - ' '} ${transitions[index]}'); - } - } - if (logs.isEmpty) { - debugPrint('So far nothing has been logged${ debug ? "" : "; use debug:true to print all output" }.'); - } else { - debugPrint('Log${ debug ? "" : " (only contains logged lines; use debug:true to print all output)" }:'); - for (final LogLine log in logs) { - log.printClearly(); - } - } - } - bool streamingLogs = false; - Timer? timeout; - void processTimeout() { - if (!streamingLogs) { - streamingLogs = true; - if (!debug) { - debugPrint('Test is taking a long time (${clock.elapsed.inSeconds} seconds so far).'); - } - describeStatus(); - debugPrint('(streaming all logs from this point on...)'); - } else { - debugPrint('(taking a long time...)'); - } - } - String stamp() => '[${(clock.elapsed.inMilliseconds / 1000.0).toStringAsFixed(1).padLeft(5)}s]'; - void processStdout(String line) { - final LogLine log = LogLine('stdout', stamp(), line); - if (logging) { - logs.add(log); - } - if (streamingLogs) { - log.printClearly(); - } - if (nextTransition < transitions.length && transitions[nextTransition].matches(line)) { - if (streamingLogs) { - debugPrint('(matched ${transitions[nextTransition]})'); - } - if (transitions[nextTransition].logging != null) { - if (!logging && transitions[nextTransition].logging!) { - logs.add(log); - } - logging = transitions[nextTransition].logging!; - if (streamingLogs) { - if (logging) { - debugPrint('(enabled logging)'); - } else { - debugPrint('(disabled logging)'); - } - } - } - if (transitions[nextTransition].handler != null) { - final String? command = transitions[nextTransition].handler!(line); - if (command != null) { - final LogLine inLog = LogLine('stdin', stamp(), command); - logs.add(inLog); - if (streamingLogs) { - inLog.printClearly(); - } - process.stdin.write(command); - } - } - nextTransition += 1; - timeout?.cancel(); - timeout = Timer(expectedMaxDuration ~/ 5, processTimeout); // This is not a failure timeout, just when to start logging verbosely to help debugging. - } - } - void processStderr(String line) { - final LogLine log = LogLine('stdout', stamp(), line); - logs.add(log); - if (streamingLogs) { - log.printClearly(); - } - } - if (debug) { - processTimeout(); - } else { - timeout = Timer(expectedMaxDuration ~/ 2, processTimeout); // This is not a failure timeout, just when to start logging verbosely to help debugging. - } - process.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen(processStdout); - process.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen(processStderr); - unawaited(process.exitCode.timeout(expectedMaxDuration, onTimeout: () { // This is a failure timeout, must not be short. - debugPrint('${stamp()} (process is not quitting, trying to send a "q" just in case that helps)'); - debugPrint('(a functional test should never reach this point)'); - final LogLine inLog = LogLine('stdin', stamp(), 'q'); - logs.add(inLog); - if (streamingLogs) { - inLog.printClearly(); - } - process.stdin.write('q'); - return -1; // discarded - }).then( - (int i) => i, - onError: (Object error) { - // ignore errors here, they will be reported on the next line - return -1; // discarded - }, - )); - final int exitCode = await process.exitCode; - if (streamingLogs) { - debugPrint('${stamp()} (process terminated with exit code $exitCode)'); - } - timeout?.cancel(); - if (nextTransition < transitions.length) { - debugPrint('The subprocess terminated before all the expected transitions had been matched.'); - if (logs.any((LogLine line) => line.couldBeCrash)) { - debugPrint('The subprocess may in fact have crashed. Check the stderr logs below.'); - } - debugPrint('The transition that we were hoping to see next but that we never saw was:'); - debugPrint('${nextTransition.toString().padLeft(5)} NOW WAITING FOR> ${transitions[nextTransition]}'); - if (!streamingLogs) { - describeStatus(); - debugPrint('(process terminated with exit code $exitCode)'); - } - throw TestFailure('Missed some expected transitions.'); - } - if (streamingLogs) { - debugPrint('${stamp()} (completed execution successfully!)'); - } - return ProcessTestResult(exitCode, logs); -} - -const int progressMessageWidth = 64; +import 'transition_test_utils.dart'; void main() { testWithoutContext('flutter run writes and clears pidfile appropriately', () async { diff --git a/packages/flutter_tools/test/integration.shard/transition_test_utils.dart b/packages/flutter_tools/test/integration.shard/transition_test_utils.dart new file mode 100644 index 00000000000..9c349176003 --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/transition_test_utils.dart @@ -0,0 +1,338 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:meta/meta.dart'; +import 'package:process/process.dart'; + +import '../src/common.dart'; +import 'test_utils.dart' show fileSystem; + +const ProcessManager processManager = LocalProcessManager(); +final String flutterRoot = getFlutterRoot(); +final String flutterBin = fileSystem.path.join(flutterRoot, 'bin', 'flutter'); + +void debugPrint(String message) { + // This is called to intentionally print debugging output when a test is + // either taking too long or has failed. + // ignore: avoid_print + print(message); +} + +typedef LineHandler = String? Function(String line); + +abstract class Transition { + const Transition({this.handler, this.logging}); + + /// Callback that is invoked when the transition matches. + /// + /// This should not throw, even if the test is failing. (For example, don't use "expect" + /// in these callbacks.) Throwing here would prevent the [runFlutter] function from running + /// to completion, which would leave zombie `flutter` processes around. + final LineHandler? handler; + + /// Whether to enable or disable logging when this transition is matched. + /// + /// The default value, null, leaves the logging state unaffected. + final bool? logging; + + bool matches(String line); + + @protected + bool lineMatchesPattern(String line, Pattern pattern) { + if (pattern is String) { + return line == pattern; + } + return line.contains(pattern); + } + + @protected + String describe(Pattern pattern) { + if (pattern is String) { + return '"$pattern"'; + } + if (pattern is RegExp) { + return '/${pattern.pattern}/'; + } + return '$pattern'; + } +} + +class Barrier extends Transition { + const Barrier(this.pattern, {super.handler, super.logging}); + final Pattern pattern; + + @override + bool matches(String line) => lineMatchesPattern(line, pattern); + + @override + String toString() => describe(pattern); +} + +class Multiple extends Transition { + Multiple( + List patterns, { + super.handler, + super.logging, + }) : _originalPatterns = patterns, + patterns = patterns.toList(); + + final List _originalPatterns; + final List patterns; + + @override + bool matches(String line) { + for (int index = 0; index < patterns.length; index += 1) { + if (lineMatchesPattern(line, patterns[index])) { + patterns.removeAt(index); + break; + } + } + return patterns.isEmpty; + } + + @override + String toString() { + if (patterns.isEmpty) { + return '${_originalPatterns.map(describe).join(', ')} (all matched)'; + } + return '${_originalPatterns.map(describe).join(', ')} (matched ${_originalPatterns.length - patterns.length} so far)'; + } +} + +class LogLine { + const LogLine(this.channel, this.stamp, this.message); + final String channel; + final String stamp; + final String message; + + bool get couldBeCrash => + message.contains('Oops; flutter has exited unexpectedly:'); + + @override + String toString() => '$stamp $channel: $message'; + + void printClearly() { + debugPrint('$stamp $channel: ${clarify(message)}'); + } + + static String clarify(String line) { + return line.runes.map((int rune) { + if (rune >= 0x20 && rune <= 0x7F) { + return String.fromCharCode(rune); + } + switch (rune) { + case 0x00: + return ''; + case 0x07: + return ''; + case 0x08: + return ''; + case 0x09: + return ''; + case 0x0A: + return ''; + case 0x0D: + return ''; + } + return '<${rune.toRadixString(16).padLeft(rune <= 0xFF ? 2 : rune <= 0xFFFF ? 4 : 5, '0')}>'; + }).join(); + } +} + +class ProcessTestResult { + const ProcessTestResult(this.exitCode, this.logs); + final int exitCode; + final List logs; + + List get stdout { + return logs + .where((LogLine log) => log.channel == 'stdout') + .map((LogLine log) => log.message) + .toList(); + } + + List get stderr { + return logs + .where((LogLine log) => log.channel == 'stderr') + .map((LogLine log) => log.message) + .toList(); + } + + @override + String toString() => 'exit code $exitCode\nlogs:\n ${logs.join('\n ')}\n'; +} + +Future runFlutter( + List arguments, + String workingDirectory, + List transitions, { + bool debug = false, + bool logging = true, + Duration expectedMaxDuration = const Duration( + minutes: + 10), // must be less than test timeout of 15 minutes! See ../../dart_test.yaml. +}) async { + final Stopwatch clock = Stopwatch()..start(); + final Process process = await processManager.start( + [flutterBin, ...arguments], + workingDirectory: workingDirectory, + ); + final List logs = []; + int nextTransition = 0; + void describeStatus() { + if (transitions.isNotEmpty) { + debugPrint('Expected state transitions:'); + for (int index = 0; index < transitions.length; index += 1) { + debugPrint('${index.toString().padLeft(5)} ' + '${index < nextTransition ? 'ALREADY MATCHED ' : index == nextTransition ? 'NOW WAITING FOR>' : ' '} ${transitions[index]}'); + } + } + if (logs.isEmpty) { + debugPrint( + 'So far nothing has been logged${debug ? "" : "; use debug:true to print all output"}.'); + } else { + debugPrint( + 'Log${debug ? "" : " (only contains logged lines; use debug:true to print all output)"}:'); + for (final LogLine log in logs) { + log.printClearly(); + } + } + } + + bool streamingLogs = false; + Timer? timeout; + void processTimeout() { + if (!streamingLogs) { + streamingLogs = true; + if (!debug) { + debugPrint( + 'Test is taking a long time (${clock.elapsed.inSeconds} seconds so far).'); + } + describeStatus(); + debugPrint('(streaming all logs from this point on...)'); + } else { + debugPrint('(taking a long time...)'); + } + } + + String stamp() => + '[${(clock.elapsed.inMilliseconds / 1000.0).toStringAsFixed(1).padLeft(5)}s]'; + void processStdout(String line) { + final LogLine log = LogLine('stdout', stamp(), line); + if (logging) { + logs.add(log); + } + if (streamingLogs) { + log.printClearly(); + } + if (nextTransition < transitions.length && + transitions[nextTransition].matches(line)) { + if (streamingLogs) { + debugPrint('(matched ${transitions[nextTransition]})'); + } + if (transitions[nextTransition].logging != null) { + if (!logging && transitions[nextTransition].logging!) { + logs.add(log); + } + logging = transitions[nextTransition].logging!; + if (streamingLogs) { + if (logging) { + debugPrint('(enabled logging)'); + } else { + debugPrint('(disabled logging)'); + } + } + } + if (transitions[nextTransition].handler != null) { + final String? command = transitions[nextTransition].handler!(line); + if (command != null) { + final LogLine inLog = LogLine('stdin', stamp(), command); + logs.add(inLog); + if (streamingLogs) { + inLog.printClearly(); + } + process.stdin.write(command); + } + } + nextTransition += 1; + timeout?.cancel(); + timeout = Timer(expectedMaxDuration ~/ 5, + processTimeout); // This is not a failure timeout, just when to start logging verbosely to help debugging. + } + } + + void processStderr(String line) { + final LogLine log = LogLine('stdout', stamp(), line); + logs.add(log); + if (streamingLogs) { + log.printClearly(); + } + } + + if (debug) { + processTimeout(); + } else { + timeout = Timer(expectedMaxDuration ~/ 2, + processTimeout); // This is not a failure timeout, just when to start logging verbosely to help debugging. + } + process.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen(processStdout); + process.stderr + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen(processStderr); + unawaited(process.exitCode.timeout(expectedMaxDuration, onTimeout: () { + // This is a failure timeout, must not be short. + debugPrint( + '${stamp()} (process is not quitting, trying to send a "q" just in case that helps)'); + debugPrint('(a functional test should never reach this point)'); + final LogLine inLog = LogLine('stdin', stamp(), 'q'); + logs.add(inLog); + if (streamingLogs) { + inLog.printClearly(); + } + process.stdin.write('q'); + return -1; // discarded + }).then( + (int i) => i, + onError: (Object error) { + // ignore errors here, they will be reported on the next line + return -1; // discarded + }, + )); + final int exitCode = await process.exitCode; + if (streamingLogs) { + debugPrint('${stamp()} (process terminated with exit code $exitCode)'); + } + timeout?.cancel(); + if (nextTransition < transitions.length) { + debugPrint( + 'The subprocess terminated before all the expected transitions had been matched.'); + if (logs.any((LogLine line) => line.couldBeCrash)) { + debugPrint( + 'The subprocess may in fact have crashed. Check the stderr logs below.'); + } + debugPrint( + 'The transition that we were hoping to see next but that we never saw was:'); + debugPrint( + '${nextTransition.toString().padLeft(5)} NOW WAITING FOR> ${transitions[nextTransition]}'); + if (!streamingLogs) { + describeStatus(); + debugPrint('(process terminated with exit code $exitCode)'); + } + throw TestFailure('Missed some expected transitions.'); + } + if (streamingLogs) { + debugPrint('${stamp()} (completed execution successfully!)'); + } + return ProcessTestResult(exitCode, logs); +} + +const int progressMessageWidth = 64; diff --git a/packages/flutter_tools/test/src/fakes.dart b/packages/flutter_tools/test/src/fakes.dart index 2cd16a00a77..4cc4ab1c227 100644 --- a/packages/flutter_tools/test/src/fakes.dart +++ b/packages/flutter_tools/test/src/fakes.dart @@ -449,6 +449,7 @@ class TestFeatureFlags implements FeatureFlags { this.areCustomDevicesEnabled = false, this.isFlutterWebWasmEnabled = false, this.isCliAnimationEnabled = true, + this.isNativeAssetsEnabled = false, }); @override @@ -481,6 +482,9 @@ class TestFeatureFlags implements FeatureFlags { @override final bool isCliAnimationEnabled; + @override + final bool isNativeAssetsEnabled; + @override bool isEnabled(Feature feature) { switch (feature) { @@ -502,6 +506,8 @@ class TestFeatureFlags implements FeatureFlags { return areCustomDevicesEnabled; case cliAnimation: return isCliAnimationEnabled; + case nativeAssets: + return isNativeAssetsEnabled; } return false; }