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; }