diff --git a/engine/src/flutter/web_sdk/BUILD.gn b/engine/src/flutter/web_sdk/BUILD.gn index 50a4c04cf29..99ae952a9f5 100644 --- a/engine/src/flutter/web_sdk/BUILD.gn +++ b/engine/src/flutter/web_sdk/BUILD.gn @@ -220,6 +220,8 @@ template("_compile_platform") { skwasm_library = "dart:_skwasm_stub" if (invoker.kernel_target == "dart2wasm") { skwasm_library = "dart:_skwasm_impl" + } else if (invoker.kernel_target == "ddc") { + args += [ "--include-unsupported-platform-library-stubs" ] } args += [ diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart index b59d4b3912b..6867be4c62a 100644 --- a/packages/flutter_tools/lib/src/build_info.dart +++ b/packages/flutter_tools/lib/src/build_info.dart @@ -53,6 +53,7 @@ class BuildInfo { this.assumeInitializeFromDillUpToDate = false, this.buildNativeAssets = true, this.useLocalCanvasKit = false, + this.includeUnsupportedPlatformLibraryStubs = false, this.webEnableHotReload = false, }) : extraFrontEndOptions = extraFrontEndOptions ?? const [], extraGenSnapshotOptions = extraGenSnapshotOptions ?? const [], @@ -181,6 +182,11 @@ class BuildInfo { /// If set, builds native assets with `build.dart` from all packages. final bool buildNativeAssets; + /// If set, unsupported core libraries can be imported without causing a compile time error. + /// + /// This should only be used by developer tooling as unsupported APIs will throw at runtime. + final bool includeUnsupportedPlatformLibraryStubs; + /// If set, web builds will use the locally built CanvasKit instead of using the CDN final bool useLocalCanvasKit; diff --git a/packages/flutter_tools/lib/src/commands/widget_preview.dart b/packages/flutter_tools/lib/src/commands/widget_preview.dart index d21224ce39b..14b108c6193 100644 --- a/packages/flutter_tools/lib/src/commands/widget_preview.dart +++ b/packages/flutter_tools/lib/src/commands/widget_preview.dart @@ -481,6 +481,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C // Don't try and download canvaskit from the CDN. useLocalCanvasKit: true, webEnableHotReload: true, + includeUnsupportedPlatformLibraryStubs: true, ), webEnableExposeUrl: false, webEnableExpressionEvaluation: true, diff --git a/packages/flutter_tools/lib/src/compile.dart b/packages/flutter_tools/lib/src/compile.dart index c878069e27f..d2b95c59a34 100644 --- a/packages/flutter_tools/lib/src/compile.dart +++ b/packages/flutter_tools/lib/src/compile.dart @@ -207,6 +207,16 @@ List buildModeOptions(BuildMode mode, List dartDefines) => switc _ => throw Exception('Unknown BuildMode: $mode'), }; +final _extraFrontEndOptionsFilterSet = { + // Don't allow for users to pass --include-unsupported-platform-library-stubs to bypass + // unsupported library compile time errors as this flag is only safe for use by developer + // tooling. + '--include-unsupported-platform-library-stubs', +}; + +Iterable? _filterExtraFrontEndOptions(List? options) => + options?.where((e) => !_extraFrontEndOptionsFilterSet.contains(e)); + /// A compiler interface for producing single (non-incremental) kernel files. class KernelCompiler { KernelCompiler({ @@ -371,7 +381,7 @@ class KernelCompiler { if (nativeAssets != null) ...['--native-assets', nativeAssets], // See: https://github.com/flutter/flutter/issues/103994 '--verbosity=error', - ...?extraFrontEndOptions, + ...?_filterExtraFrontEndOptions(extraFrontEndOptions), mainUri ?? '--native-assets-only', ]; @@ -509,6 +519,7 @@ abstract class ResidentCompiler { required ShutdownHooks shutdownHooks, bool testCompilation, bool trackWidgetCreation, + bool includeUnsupportedPlatformLibraryStubs, String packagesPath, List fileSystemRoots, String? fileSystemScheme, @@ -631,6 +642,7 @@ class DefaultResidentCompiler implements ResidentCompiler { required ShutdownHooks shutdownHooks, this.testCompilation = false, this.trackWidgetCreation = true, + this.includeUnsupportedPlatformLibraryStubs = false, this.packagesPath, List fileSystemRoots = const [], this.fileSystemScheme, @@ -653,7 +665,17 @@ class DefaultResidentCompiler implements ResidentCompiler { // This is a URI, not a file path, so the forward slash is correct even on Windows. sdkRoot = sdkRoot.endsWith('/') ? sdkRoot : '$sdkRoot/', // Make a copy, we might need to modify it later. - fileSystemRoots = List.from(fileSystemRoots); + fileSystemRoots = List.from(fileSystemRoots) { + // Currently, the only developer tooling that requires support for importing unsupported + // platform libraries at compile time is the widget previewer. Only developer tooling use cases + // should support this, so this is restricted to the widget previewer's runtime target for now. + if (includeUnsupportedPlatformLibraryStubs && targetModel != TargetModel.dartdevc) { + throw StateError( + 'includeUnsupportedPlatformLibraryStubs should only be used by the widget-preview ' + 'command.', + ); + } + } final Logger _logger; final ProcessManager _processManager; @@ -664,6 +686,7 @@ class DefaultResidentCompiler implements ResidentCompiler { final bool testCompilation; final BuildMode buildMode; final bool trackWidgetCreation; + final bool includeUnsupportedPlatformLibraryStubs; final String? packagesPath; final TargetModel targetModel; final List fileSystemRoots; @@ -867,6 +890,8 @@ class DefaultResidentCompiler implements ResidentCompiler { if (packagesPath != null) ...['--packages', packagesPath!], ...buildModeOptions(buildMode, dartDefines), if (trackWidgetCreation) '--track-widget-creation', + if (includeUnsupportedPlatformLibraryStubs) + '--include-unsupported-platform-library-stubs', for (final String root in fileSystemRoots) ...['--filesystem-root', root], if (fileSystemScheme != null) ...['--filesystem-scheme', fileSystemScheme!], if (initializeFromDill != null) ...[ @@ -886,7 +911,7 @@ class DefaultResidentCompiler implements ResidentCompiler { if (unsafePackageSerialization) '--unsafe-package-serialization', // See: https://github.com/flutter/flutter/issues/103994 '--verbosity=error', - ...?extraFrontEndOptions, + ...?_filterExtraFrontEndOptions(extraFrontEndOptions), ]; _logger.printTrace(command.join(' ')); _server = await _processManager.start(command); diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index e33adc19615..8f22a8cbcc0 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -125,6 +125,7 @@ class FlutterDevice { globals.artifacts!.getHostArtifact(HostArtifact.flutterWebSdk).path, buildMode: buildInfo.mode, trackWidgetCreation: buildInfo.trackWidgetCreation, + includeUnsupportedPlatformLibraryStubs: buildInfo.includeUnsupportedPlatformLibraryStubs, fileSystemRoots: buildInfo.fileSystemRoots, // Override the filesystem scheme so that the frontend_server can find // the generated entrypoint code. diff --git a/packages/flutter_tools/test/general.shard/compile_test.dart b/packages/flutter_tools/test/general.shard/compile_test.dart index d2febf9ea1e..03473a26209 100644 --- a/packages/flutter_tools/test/general.shard/compile_test.dart +++ b/packages/flutter_tools/test/general.shard/compile_test.dart @@ -2,13 +2,21 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; + import 'package:file/file.dart'; import 'package:file/memory.dart'; +import 'package:flutter_tools/src/artifacts.dart'; +import 'package:flutter_tools/src/base/common.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/compile.dart'; +import 'package:package_config/package_config.dart'; import '../src/common.dart'; +import '../src/fake_process_manager.dart'; +import '../src/fakes.dart' hide FakeProcess; void main() { testWithoutContext('StdoutHandler can produce output message', () async { @@ -147,4 +155,195 @@ void main() { ); }, ); + + testWithoutContext( + 'includeUnsupportedPlatformLibraryStubs is only valid for Target.dartdevc', + () { + final unsupportedTargetModels = { + TargetModel.flutter, + TargetModel.flutterRunner, + TargetModel.vm, + }; + + // Initializing the compiler with includeUnsupportedPlatformLibraryStubs for targets other + // than DDC is not currently supported as it's limited for use with the widget previewer. + for (final target in unsupportedTargetModels) { + try { + ResidentCompiler( + 'sdkroot', + buildMode: BuildMode.debug, + logger: BufferLogger.test(), + processManager: FakeProcessManager.any(), + artifacts: Artifacts.test(), + platform: FakePlatform(), + fileSystem: MemoryFileSystem.test(), + shutdownHooks: FakeShutdownHooks(), + targetModel: target, + includeUnsupportedPlatformLibraryStubs: true, + ); + fail('Unsupported target did not throw.'); + } on StateError catch (e) { + expect( + e.message, + 'includeUnsupportedPlatformLibraryStubs should only be used by the widget-preview ' + 'command.', + ); + } + } + + // Initializing the compiler with includeUnsupportedPlatformLibraryStubs for DDC is + // supported. + ResidentCompiler( + 'sdkroot', + buildMode: BuildMode.debug, + logger: BufferLogger.test(), + processManager: FakeProcessManager.any(), + artifacts: Artifacts.test(), + platform: FakePlatform(), + fileSystem: MemoryFileSystem.test(), + shutdownHooks: FakeShutdownHooks(), + targetModel: TargetModel.dartdevc, + includeUnsupportedPlatformLibraryStubs: true, + ); + }, + ); + + testWithoutContext( + 'Strips --include-unsupported-platform-library-stubs from extraFrontEndOptions', + () async { + final completer = Completer(); + final processManager = FakeProcessManager.list([ + FakeCommand( + command: const [ + 'Artifact.engineDartAotRuntime.TargetPlatform.web_javascript', + 'Artifact.frontendServerSnapshotForEngineDartSdk.TargetPlatform.web_javascript', + '--sdk-root', + 'sdkroot/', + '--incremental', + '--target=dartdevc', + '--experimental-emit-debug-metadata', + '--output-dill', + 'foo.dill', + '-Ddart.vm.profile=false', + '-Ddart.vm.product=false', + '--enable-asserts', + '--track-widget-creation', + '--verbosity=error', + '--extra-flag', + ], + onRun: (_) => completer.complete(), + ), + ]); + final compiler = DefaultResidentCompiler( + 'sdkroot', + buildMode: BuildMode.debug, + logger: BufferLogger.test(), + processManager: processManager, + artifacts: Artifacts.test(), + platform: FakePlatform(), + fileSystem: MemoryFileSystem.test(), + shutdownHooks: FakeShutdownHooks(), + targetModel: TargetModel.dartdevc, + // Don't explicitly enable includeUnsupportedPlatformLibraryStubs to ensure it's not + // included in the argument list. + // ignore: avoid_redundant_argument_values + includeUnsupportedPlatformLibraryStubs: false, + extraFrontEndOptions: [ + '--include-unsupported-platform-library-stubs', + // Include a random extra flag to ensure not all extra options are stripped. + '--extra-flag', + ], + ); + + await runZonedGuarded( + () { + // This throws ToolExit as the FakeProcess immediately closes stdout and stderr. + compiler.recompile( + Uri.file('foo.dart'), + [], + outputPath: 'foo.dill', + packageConfig: PackageConfig.empty, + ); + }, + (e, st) { + if (e is! ToolExit) { + completer.completeError(e, st); + } + }, + ); + + // Fail if the command isn't run. This can happen when the commands actual arguments don't + // match. + await completer.future.timeout(const Duration(seconds: 5)); + }, + ); + + testWithoutContext( + '--include-unsupported-platform-library-stubs when includeUnsupportedPlatformLibraryStubs is set', + () async { + final completer = Completer(); + final processManager = FakeProcessManager.list([ + FakeCommand( + command: const [ + 'Artifact.engineDartAotRuntime.TargetPlatform.web_javascript', + 'Artifact.frontendServerSnapshotForEngineDartSdk.TargetPlatform.web_javascript', + '--sdk-root', + 'sdkroot/', + '--incremental', + '--target=dartdevc', + '--experimental-emit-debug-metadata', + '--output-dill', + 'foo.dill', + '-Ddart.vm.profile=false', + '-Ddart.vm.product=false', + '--enable-asserts', + '--track-widget-creation', + '--include-unsupported-platform-library-stubs', + '--verbosity=error', + '--extra-flag', + ], + onRun: (_) => completer.complete(), + ), + ]); + final compiler = DefaultResidentCompiler( + 'sdkroot', + buildMode: BuildMode.debug, + logger: BufferLogger.test(), + processManager: processManager, + artifacts: Artifacts.test(), + platform: FakePlatform(), + fileSystem: MemoryFileSystem.test(), + shutdownHooks: FakeShutdownHooks(), + targetModel: TargetModel.dartdevc, + // Explicitly enable includeUnsupportedPlatformLibraryStubs to ensure it's included in the + // argument list. + includeUnsupportedPlatformLibraryStubs: true, + extraFrontEndOptions: [ + // Include a random extra flag to ensure not all extra options are stripped. + '--extra-flag', + ], + ); + + await runZonedGuarded( + () { + // This throws ToolExit as the FakeProcess immediately closes stdout and stderr. + compiler.recompile( + Uri.file('foo.dart'), + [], + outputPath: 'foo.dill', + packageConfig: PackageConfig.empty, + ); + }, + (e, st) { + if (e is! ToolExit) { + completer.completeError(e, st); + } + }, + ); + + // Fail if the command isn't run. This can happen when the commands actual arguments don't + // match. + await completer.future.timeout(const Duration(seconds: 5)); + }, + ); }