| // 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:typed_data'; | |
| import 'package:meta/meta.dart'; | |
| import 'package:package_config/package_config.dart'; | |
| import 'package:process/process.dart'; | |
| import 'package:uuid/uuid.dart'; | |
| import 'artifacts.dart'; | |
| import 'base/common.dart'; | |
| import 'base/config.dart'; | |
| import 'base/context.dart'; | |
| import 'base/file_system.dart'; | |
| import 'base/io.dart'; | |
| import 'base/logger.dart'; | |
| import 'base/platform.dart'; | |
| import 'base/process.dart'; | |
| import 'base/utils.dart'; | |
| import 'build_info.dart'; | |
| import 'bundle.dart'; | |
| import 'convert.dart'; | |
| import 'dart/package_map.dart'; | |
| /// Opt-in changes to the dart compilers. | |
| const kDartCompilerExperiments = <String>[]; | |
| /// The target model describes the set of core libraries that are available within | |
| /// the SDK. | |
| class TargetModel { | |
| /// Parse a [TargetModel] from a raw string. | |
| /// | |
| /// Throws an exception if passed a value other than 'flutter', | |
| /// 'flutter_runner', 'vm', or 'dartdevc'. | |
| factory TargetModel(String rawValue) { | |
| return switch (rawValue) { | |
| 'flutter' => flutter, | |
| 'flutter_runner' => flutterRunner, | |
| 'vm' => vm, | |
| 'dartdevc' => dartdevc, | |
| _ => throw Exception('Unexpected target model $rawValue'), | |
| }; | |
| } | |
| const TargetModel._(this._value); | |
| /// The Flutter patched Dart SDK. | |
| static const flutter = TargetModel._('flutter'); | |
| /// The Fuchsia patched SDK. | |
| static const flutterRunner = TargetModel._('flutter_runner'); | |
| /// The Dart VM. | |
| static const vm = TargetModel._('vm'); | |
| /// The development compiler for JavaScript. | |
| static const dartdevc = TargetModel._('dartdevc'); | |
| final String _value; | |
| @override | |
| String toString() => _value; | |
| /// Infers the appropriate [TargetModel] from a given [TargetPlatform]. | |
| static TargetModel fromTargetPlatform(TargetPlatform? platform) { | |
| return switch (platform) { | |
| TargetPlatform.web_javascript => TargetModel.dartdevc, | |
| TargetPlatform.fuchsia_arm64 || TargetPlatform.fuchsia_x64 => TargetModel.flutterRunner, | |
| _ => TargetModel.flutter, | |
| }; | |
| } | |
| } | |
| class CompilerOutput { | |
| const CompilerOutput( | |
| this.outputFilename, | |
| this.errorCount, | |
| this.sources, { | |
| this.expressionData, | |
| this.errorMessage, | |
| }); | |
| final String outputFilename; | |
| final int errorCount; | |
| final List<Uri> sources; | |
| /// This field is only non-null for expression compilation requests. | |
| final Uint8List? expressionData; | |
| /// This field is only non-null when a compilation error was encountered. | |
| final String? errorMessage; | |
| } | |
| enum StdoutState { CollectDiagnostic, CollectDependencies } | |
| /// Handles stdin/stdout communication with the frontend server. | |
| class StdoutHandler { | |
| StdoutHandler({required this._logger, required this._fileSystem}) { | |
| reset(); | |
| } | |
| final Logger _logger; | |
| final FileSystem _fileSystem; | |
| String? boundaryKey; | |
| StdoutState state = StdoutState.CollectDiagnostic; | |
| Completer<CompilerOutput?>? compilerOutput; | |
| final sources = <Uri>[]; | |
| var _suppressCompilerMessages = false; | |
| var _expectSources = true; | |
| var _readFile = false; | |
| var _errorBuffer = StringBuffer(); | |
| void handler(String message) { | |
| const kResultPrefix = 'result '; | |
| if (boundaryKey == null && message.startsWith(kResultPrefix)) { | |
| boundaryKey = message.substring(kResultPrefix.length); | |
| return; | |
| } | |
| final String? messageBoundaryKey = boundaryKey; | |
| if (messageBoundaryKey != null && message.startsWith(messageBoundaryKey)) { | |
| if (_expectSources) { | |
| if (state == StdoutState.CollectDiagnostic) { | |
| state = StdoutState.CollectDependencies; | |
| return; | |
| } | |
| } | |
| if (message.length <= messageBoundaryKey.length) { | |
| compilerOutput?.complete(); | |
| return; | |
| } | |
| final int spaceDelimiter = message.lastIndexOf(' '); | |
| final String fileName = message.substring(messageBoundaryKey.length + 1, spaceDelimiter); | |
| final int errorCount = int.parse(message.substring(spaceDelimiter + 1).trim()); | |
| Uint8List? expressionData; | |
| if (_readFile) { | |
| expressionData = _fileSystem.file(fileName).readAsBytesSync(); | |
| } | |
| final output = CompilerOutput( | |
| fileName, | |
| errorCount, | |
| sources, | |
| expressionData: expressionData, | |
| errorMessage: _errorBuffer.isNotEmpty ? _errorBuffer.toString() : null, | |
| ); | |
| compilerOutput?.complete(output); | |
| return; | |
| } | |
| switch (state) { | |
| case StdoutState.CollectDiagnostic when _suppressCompilerMessages: | |
| _logger.printTrace(message); | |
| _errorBuffer.writeln(message); | |
| case StdoutState.CollectDiagnostic: | |
| _logger.printError(message); | |
| _errorBuffer.writeln(message); | |
| case StdoutState.CollectDependencies: | |
| switch (message[0]) { | |
| case '+': | |
| sources.add(Uri.parse(message.substring(1))); | |
| case '-': | |
| sources.remove(Uri.parse(message.substring(1))); | |
| default: | |
| _logger.printTrace('Unexpected prefix for $message uri - ignoring'); | |
| } | |
| } | |
| } | |
| // This is needed to get ready to process next compilation result output, | |
| // with its own boundary key and new completer. | |
| void reset({ | |
| bool suppressCompilerMessages = false, | |
| bool expectSources = true, | |
| bool readFile = false, | |
| }) { | |
| boundaryKey = null; | |
| compilerOutput = Completer<CompilerOutput?>(); | |
| _suppressCompilerMessages = suppressCompilerMessages; | |
| _expectSources = expectSources; | |
| _readFile = readFile; | |
| state = StdoutState.CollectDiagnostic; | |
| _errorBuffer = StringBuffer(); | |
| } | |
| } | |
| /// List the preconfigured build options for a given build mode. | |
| List<String> buildModeOptions(BuildMode mode, List<String> dartDefines) => switch (mode) { | |
| BuildMode.debug => <String>[ | |
| // These checks allow the CLI to override the value of this define for unit | |
| // testing the framework. | |
| if (!dartDefines.any((String define) => define.startsWith('dart.vm.profile'))) | |
| '-Ddart.vm.profile=false', | |
| if (!dartDefines.any((String define) => define.startsWith('dart.vm.product'))) | |
| '-Ddart.vm.product=false', | |
| '--enable-asserts', | |
| ], | |
| BuildMode.profile => <String>[ | |
| // These checks allow the CLI to override the value of this define for | |
| // benchmarks with most timeline traces disabled. | |
| if (!dartDefines.any((String define) => define.startsWith('dart.vm.profile'))) | |
| '-Ddart.vm.profile=true', | |
| if (!dartDefines.any((String define) => define.startsWith('dart.vm.product'))) | |
| '-Ddart.vm.product=false', | |
| '--delete-tostring-package-uri=dart:ui', | |
| '--delete-tostring-package-uri=package:flutter', | |
| ...kDartCompilerExperiments, | |
| ], | |
| BuildMode.release => <String>[ | |
| '-Ddart.vm.profile=false', | |
| '-Ddart.vm.product=true', | |
| '--delete-tostring-package-uri=dart:ui', | |
| '--delete-tostring-package-uri=package:flutter', | |
| ...kDartCompilerExperiments, | |
| ], | |
| _ => throw Exception('Unknown BuildMode: $mode'), | |
| }; | |
| final _extraFrontEndOptionsFilterSet = <String>{ | |
| // 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<String>? _filterExtraFrontEndOptions(List<String>? options) => | |
| options?.where((e) => !_extraFrontEndOptionsFilterSet.contains(e)); | |
| /// A compiler interface for producing single (non-incremental) kernel files. | |
| class KernelCompiler { | |
| KernelCompiler({ | |
| required FileSystem fileSystem, | |
| required Logger logger, | |
| required this._processManager, | |
| required this._artifacts, | |
| required this._fileSystemRoots, | |
| this._fileSystemScheme, | |
| @visibleForTesting StdoutHandler? stdoutHandler, | |
| }) : _logger = logger, | |
| _fileSystem = fileSystem, | |
| _stdoutHandler = stdoutHandler ?? StdoutHandler(logger: logger, fileSystem: fileSystem); | |
| final FileSystem _fileSystem; | |
| final Artifacts _artifacts; | |
| final ProcessManager _processManager; | |
| final Logger _logger; | |
| final String? _fileSystemScheme; | |
| final List<String> _fileSystemRoots; | |
| final StdoutHandler _stdoutHandler; | |
| Future<CompilerOutput?> compile({ | |
| required String sdkRoot, | |
| String? mainPath, | |
| String? outputFilePath, | |
| String? depFilePath, | |
| TargetModel targetModel = TargetModel.flutter, | |
| bool linkPlatformKernelIn = false, | |
| bool aot = false, | |
| String? frontendServerStarterPath, | |
| List<String>? extraFrontEndOptions, | |
| List<String>? fileSystemRoots, | |
| String? fileSystemScheme, | |
| String? initializeFromDill, | |
| String? platformDill, | |
| Directory? buildDir, | |
| String? targetOS, | |
| bool checkDartPluginRegistry = false, | |
| required String? packagesPath, | |
| required BuildMode buildMode, | |
| required bool trackWidgetCreation, | |
| required List<String> dartDefines, | |
| required PackageConfig packageConfig, | |
| String? nativeAssets, | |
| }) async { | |
| final TargetPlatform? platform = targetModel == TargetModel.dartdevc | |
| ? TargetPlatform.web_javascript | |
| : null; | |
| // This is a URI, not a file path, so the forward slash is correct even on Windows. | |
| if (!sdkRoot.endsWith('/')) { | |
| sdkRoot = '$sdkRoot/'; | |
| } | |
| String? mainUri; | |
| if (mainPath != null) { | |
| final File mainFile = _fileSystem.file(mainPath); | |
| final Uri mainFileUri = mainFile.uri; | |
| if (packagesPath != null) { | |
| mainUri = packageConfig.toPackageUriForWorkspace(mainFileUri)?.toString(); | |
| } | |
| mainUri ??= toMultiRootPath( | |
| mainFileUri, | |
| _fileSystemScheme, | |
| _fileSystemRoots, | |
| _fileSystem.path.separator == r'\', | |
| ); | |
| } | |
| if (outputFilePath != null && !_fileSystem.isFileSync(outputFilePath)) { | |
| _fileSystem.file(outputFilePath).createSync(recursive: true); | |
| } | |
| // Check if there's a Dart plugin registrant. | |
| // This is contained in the file `dart_plugin_registrant.dart` under `.dart_tools/flutter_build/`. | |
| final File? dartPluginRegistrant = checkDartPluginRegistry | |
| ? buildDir?.parent.childFile('dart_plugin_registrant.dart') | |
| : null; | |
| String? dartPluginRegistrantUri; | |
| if (dartPluginRegistrant != null && dartPluginRegistrant.existsSync()) { | |
| final Uri dartPluginRegistrantFileUri = dartPluginRegistrant.uri; | |
| dartPluginRegistrantUri = | |
| packageConfig.toPackageUriForWorkspace(dartPluginRegistrantFileUri)?.toString() ?? | |
| toMultiRootPath( | |
| dartPluginRegistrantFileUri, | |
| _fileSystemScheme, | |
| _fileSystemRoots, | |
| _fileSystem.path.separator == r'\', | |
| ); | |
| } | |
| final List<String> commandToStartFrontendServer; | |
| if (frontendServerStarterPath != null && frontendServerStarterPath.isNotEmpty) { | |
| final String engineDartPath = _artifacts.getArtifactPath( | |
| Artifact.engineDartBinary, | |
| platform: platform, | |
| ); | |
| if (!_processManager.canRun(engineDartPath)) { | |
| throwToolExit('Unable to find Dart binary at $engineDartPath'); | |
| } | |
| commandToStartFrontendServer = <String>[engineDartPath, frontendServerStarterPath]; | |
| } else { | |
| final String engineDartAotRuntimePath = _artifacts.getArtifactPath( | |
| Artifact.engineDartAotRuntime, | |
| platform: platform, | |
| ); | |
| if (!_processManager.canRun(engineDartAotRuntimePath)) { | |
| throwToolExit('Unable to find dartaotruntime binary at $engineDartAotRuntimePath'); | |
| } | |
| commandToStartFrontendServer = <String>[ | |
| engineDartAotRuntimePath, | |
| _artifacts.getArtifactPath( | |
| Artifact.frontendServerSnapshotForEngineDartSdk, | |
| platform: platform, | |
| ), | |
| ]; | |
| } | |
| final List<String> command = | |
| commandToStartFrontendServer + | |
| <String>[ | |
| '--sdk-root', | |
| sdkRoot, | |
| '--target=$targetModel', | |
| '--no-print-incremental-dependencies', | |
| for (final Object dartDefine in dartDefines) '-D$dartDefine', | |
| ...buildModeOptions(buildMode, dartDefines), | |
| if (trackWidgetCreation) '--track-creation-locations', | |
| if (!linkPlatformKernelIn) '--no-link-platform', | |
| if (aot) ...<String>[ | |
| '--aot', | |
| '--tfa', | |
| // The --target-os flag only makes sense for whole program compilation. | |
| if (targetOS != null) ...<String>['--target-os', targetOS], | |
| ], | |
| if (packagesPath != null) ...<String>['--packages', packagesPath], | |
| if (outputFilePath != null) ...<String>['--output-dill', outputFilePath], | |
| if (depFilePath != null && | |
| (fileSystemRoots == null || fileSystemRoots.isEmpty)) ...<String>[ | |
| '--depfile', | |
| depFilePath, | |
| ], | |
| if (fileSystemRoots != null) | |
| for (final String root in fileSystemRoots) ...<String>['--filesystem-root', root], | |
| if (fileSystemScheme != null) ...<String>['--filesystem-scheme', fileSystemScheme], | |
| if (initializeFromDill != null) ...<String>[ | |
| '--incremental', | |
| '--initialize-from-dill', | |
| initializeFromDill, | |
| ], | |
| if (platformDill != null) ...<String>['--platform', platformDill], | |
| if (dartPluginRegistrantUri != null) ...<String>[ | |
| '--source', | |
| dartPluginRegistrantUri, | |
| '--source', | |
| 'package:flutter/src/dart_plugin_registrant.dart', | |
| '-Dflutter.dart_plugin_registrant=$dartPluginRegistrantUri', | |
| ], | |
| if (nativeAssets != null) ...<String>['--native-assets', nativeAssets], | |
| // See: https://github.com/flutter/flutter/issues/103994 | |
| '--verbosity=error', | |
| ...?_filterExtraFrontEndOptions(extraFrontEndOptions), | |
| mainUri ?? '--native-assets-only', | |
| ]; | |
| _logger.printTrace(command.join(' ')); | |
| final Process server = await _processManager.start(command); | |
| // Use permissive decoder for compiler stderr which may contain invalid UTF-8 | |
| server.stderr.transform<String>(utf8AllowMalformed.decoder).listen(_logger.printError); | |
| server.stdout.transform(utf8LineDecoder).listen(_stdoutHandler.handler); | |
| final int exitCode = await server.exitCode; | |
| if (exitCode == 0) { | |
| return _stdoutHandler.compilerOutput?.future; | |
| } | |
| return null; | |
| } | |
| } | |
| /// Class that allows to serialize compilation requests to the compiler. | |
| abstract class _CompilationRequest { | |
| _CompilationRequest(this.completer); | |
| Completer<CompilerOutput?> completer; | |
| Future<CompilerOutput?> _run(DefaultResidentCompiler compiler); | |
| Future<void> run(DefaultResidentCompiler compiler) async { | |
| completer.complete(await _run(compiler)); | |
| } | |
| } | |
| class _RecompileRequest extends _CompilationRequest { | |
| _RecompileRequest( | |
| super.completer, | |
| this.mainUri, | |
| this.invalidatedFiles, | |
| this.outputPath, | |
| this.packageConfig, | |
| this.suppressErrors, { | |
| this.additionalSourceUri, | |
| this.nativeAssetsYamlUri, | |
| required this.recompileRestart, | |
| }); | |
| Uri mainUri; | |
| List<Uri>? invalidatedFiles; | |
| String outputPath; | |
| PackageConfig packageConfig; | |
| bool suppressErrors; | |
| final Uri? additionalSourceUri; | |
| final Uri? nativeAssetsYamlUri; | |
| final bool recompileRestart; | |
| @override | |
| Future<CompilerOutput?> _run(DefaultResidentCompiler compiler) async => compiler._recompile(this); | |
| } | |
| class _CompileExpressionRequest extends _CompilationRequest { | |
| _CompileExpressionRequest( | |
| super.completer, | |
| this.expression, | |
| this.definitions, | |
| this.definitionTypes, | |
| this.typeDefinitions, | |
| this.typeBounds, | |
| this.typeDefaults, | |
| this.libraryUri, | |
| this.klass, | |
| this.method, | |
| this.isStatic, | |
| ); | |
| String expression; | |
| List<String>? definitions; | |
| List<String>? definitionTypes; | |
| List<String>? typeDefinitions; | |
| List<String>? typeBounds; | |
| List<String>? typeDefaults; | |
| String? libraryUri; | |
| String? klass; | |
| String? method; | |
| bool isStatic; | |
| @override | |
| Future<CompilerOutput?> _run(DefaultResidentCompiler compiler) async => | |
| compiler._compileExpression(this); | |
| } | |
| class _CompileExpressionToJsRequest extends _CompilationRequest { | |
| _CompileExpressionToJsRequest( | |
| super.completer, | |
| this.libraryUri, | |
| this.scriptUri, | |
| this.line, | |
| this.column, | |
| this.jsModules, | |
| this.jsFrameValues, | |
| this.moduleName, | |
| this.expression, | |
| ); | |
| final String? libraryUri; | |
| final String? scriptUri; | |
| final int line; | |
| final int column; | |
| final Map<String, String>? jsModules; | |
| final Map<String, String>? jsFrameValues; | |
| final String? moduleName; | |
| final String? expression; | |
| @override | |
| Future<CompilerOutput?> _run(DefaultResidentCompiler compiler) async => | |
| compiler._compileExpressionToJs(this); | |
| } | |
| class _RejectRequest extends _CompilationRequest { | |
| _RejectRequest(super.completer); | |
| @override | |
| Future<CompilerOutput?> _run(DefaultResidentCompiler compiler) async => compiler._reject(); | |
| } | |
| ResidentCompilerFactory get residentCompilerFactory => | |
| context.get<ResidentCompilerFactory>() ?? const ResidentCompilerFactory(); | |
| /// A factory for generating [ResidentCompiler] instances. | |
| class ResidentCompilerFactory { | |
| const ResidentCompilerFactory(); | |
| ResidentCompiler create({ | |
| required TargetPlatform targetPlatform, | |
| required BuildInfo buildInfo, | |
| required Logger logger, | |
| required ProcessManager processManager, | |
| required Artifacts artifacts, | |
| required Platform platform, | |
| required FileSystem fileSystem, | |
| required ShutdownHooks shutdownHooks, | |
| required Config config, | |
| bool testCompilation = false, | |
| TargetModel? targetModelOverride, | |
| }) { | |
| String sdkRoot = artifacts.getArtifactPath( | |
| Artifact.flutterPatchedSdkPath, | |
| platform: targetPlatform, | |
| mode: buildInfo.mode, | |
| ); | |
| String? platformDillPath; | |
| String? librariesSpec; | |
| TargetModel targetModel = targetModelOverride ?? .flutter; | |
| // Configure the compiler to target the DDC runtime. | |
| if (targetPlatform case .web_javascript) { | |
| sdkRoot = artifacts.getHostArtifact(HostArtifact.flutterWebSdk).path; | |
| targetModel = .dartdevc; | |
| const platformDillName = 'ddc_outline.dill'; | |
| platformDillPath = fileSystem | |
| .file( | |
| fileSystem.path.join( | |
| artifacts.getHostArtifact(.webPlatformKernelFolder).path, | |
| platformDillName, | |
| ), | |
| ) | |
| .absolute | |
| .uri | |
| .toString(); | |
| librariesSpec = fileSystem | |
| .file(artifacts.getHostArtifact(.flutterWebLibrariesJson)) | |
| .uri | |
| .toString(); | |
| buildInfo = buildInfo.copyWith( | |
| // Override the filesystem scheme so that the frontend_server can find | |
| // the generated entrypoint code. | |
| fileSystemScheme: 'org-dartlang-app', | |
| extraFrontEndOptions: [ | |
| ...buildInfo.extraFrontEndOptions, | |
| if (buildInfo.webEnableHotReload) | |
| // These flags are only valid to be passed when compiling with DDC. | |
| ...<String>['--dartdevc-canary', '--dartdevc-module-format=ddc'], | |
| ], | |
| ); | |
| } else { | |
| if (targetPlatform case .fuchsia_arm64 || .fuchsia_x64) { | |
| targetModel = .flutterRunner; | |
| } | |
| buildInfo = buildInfo.copyWith( | |
| extraFrontEndOptions: [ | |
| ...buildInfo.extraFrontEndOptions, | |
| '--enable-experiment=alternative-invalidation-strategy', | |
| ], | |
| ); | |
| } | |
| return DefaultResidentCompiler( | |
| sdkRoot, | |
| buildInfo: buildInfo, | |
| logger: logger, | |
| processManager: processManager, | |
| artifacts: artifacts, | |
| platform: platform, | |
| fileSystem: fileSystem, | |
| shutdownHooks: shutdownHooks, | |
| config: config, | |
| targetModel: targetModel, | |
| platformDill: platformDillPath, | |
| librariesSpec: librariesSpec, | |
| testCompilation: testCompilation, | |
| ); | |
| } | |
| } | |
| /// Wrapper around incremental frontend server compiler, that communicates with | |
| /// server via stdin/stdout. | |
| /// | |
| /// The wrapper is intended to stay resident in memory as user changes, reloads, | |
| /// restarts the Flutter app. | |
| abstract interface class ResidentCompiler { | |
| // TODO(zanderso): find a better way to configure additional file system | |
| // roots from the runner. | |
| // See: https://github.com/flutter/flutter/issues/50494 | |
| void addFileSystemRoot(String root); | |
| /// If invoked for the first time, it compiles the Dart script identified by | |
| /// [mainUri], [invalidatedFiles] list is ignored. | |
| /// On successive runs [invalidatedFiles] indicates which files need to be | |
| /// recompiled. | |
| /// | |
| /// Binary file name is returned if compilation was successful, otherwise | |
| /// null is returned. | |
| /// | |
| /// If [checkDartPluginRegistry] is true, it is the caller's responsibility | |
| /// to ensure that the generated registrant file has been updated such that | |
| /// it is wrapping [mainUri]. | |
| /// | |
| /// If [recompileRestart] is true, uses the `recompile-restart` instruction | |
| /// intended for a hot restart instead. | |
| Future<CompilerOutput?> recompile( | |
| Uri mainUri, | |
| List<Uri>? invalidatedFiles, { | |
| required String outputPath, | |
| required PackageConfig packageConfig, | |
| required FileSystem fs, | |
| String? projectRootPath, | |
| bool suppressErrors = false, | |
| bool checkDartPluginRegistry = false, | |
| File? dartPluginRegistrant, | |
| Uri? nativeAssetsYaml, | |
| bool recompileRestart = false, | |
| }); | |
| Future<CompilerOutput?> compileExpression( | |
| String expression, | |
| List<String>? definitions, | |
| List<String>? definitionTypes, | |
| List<String>? typeDefinitions, | |
| List<String>? typeBounds, | |
| List<String>? typeDefaults, | |
| String? libraryUri, | |
| String? klass, | |
| String? method, | |
| bool isStatic, | |
| ); | |
| /// Compiles [expression] in [libraryUri] at [line]:[column] to JavaScript | |
| /// in [moduleName]. | |
| /// | |
| /// Values listed in [jsFrameValues] are substituted for their names in the | |
| /// [expression]. | |
| /// | |
| /// Ensures that all [jsModules] are loaded and accessible inside the | |
| /// expression. | |
| /// | |
| /// Example values of parameters: | |
| /// [moduleName] is of the form '/packages/hello_world_main.dart' | |
| /// [jsFrameValues] is a map from js variable name to its primitive value | |
| /// or another variable name, for example | |
| /// { 'x': '1', 'y': 'y', 'o': 'null' } | |
| /// [jsModules] is a map from variable name to the module name, where | |
| /// variable name is the name originally used in JavaScript to contain the | |
| /// module object, for example: | |
| /// { 'dart':'dart_sdk', 'main': '/packages/hello_world_main.dart' } | |
| /// Returns a [CompilerOutput] including the name of the file containing the | |
| /// compilation result and a number of errors. | |
| Future<CompilerOutput?> compileExpressionToJs( | |
| String libraryUri, | |
| String scriptUri, | |
| int line, | |
| int column, | |
| Map<String, String> jsModules, | |
| Map<String, String> jsFrameValues, | |
| String moduleName, | |
| String expression, | |
| ); | |
| /// Should be invoked when results of compilation are accepted by the client. | |
| /// | |
| /// Either [accept] or [reject] should be called after every [recompile] call. | |
| void accept(); | |
| /// Should be invoked when results of compilation are rejected by the client. | |
| /// | |
| /// Either [accept] or [reject] should be called after every [recompile] call. | |
| Future<CompilerOutput?> reject(); | |
| /// Should be invoked when frontend server compiler should forget what was | |
| /// accepted previously so that next call to [recompile] produces complete | |
| /// kernel file. | |
| void reset(); | |
| Future<Object> shutdown(); | |
| } | |
| @visibleForTesting | |
| class DefaultResidentCompiler implements ResidentCompiler { | |
| DefaultResidentCompiler( | |
| String sdkRoot, { | |
| required BuildInfo buildInfo, | |
| required Logger logger, | |
| required this._processManager, | |
| required this.artifacts, | |
| required this._platform, | |
| required FileSystem fileSystem, | |
| required this._shutdownHooks, | |
| required Config config, | |
| this.testCompilation = false, | |
| this.targetModel = TargetModel.flutter, | |
| this.platformDill, | |
| this.librariesSpec, | |
| @visibleForTesting StdoutHandler? stdoutHandler, | |
| }) : buildMode = buildInfo.mode, | |
| trackWidgetCreation = buildInfo.trackWidgetCreation, | |
| includeUnsupportedPlatformLibraryStubs = buildInfo.includeUnsupportedPlatformLibraryStubs, | |
| packagesPath = buildInfo.packageConfigPath, | |
| initializeFromDill = | |
| buildInfo.initializeFromDill ?? | |
| getDefaultCachedKernelPath( | |
| config: config, | |
| fileSystem: fileSystem, | |
| trackWidgetCreation: buildInfo.trackWidgetCreation, | |
| dartDefines: buildInfo.dartDefines, | |
| targetModel: targetModel, | |
| extraFrontEndOptions: buildInfo.extraFrontEndOptions, | |
| ), | |
| extraFrontEndOptions = buildInfo.extraFrontEndOptions, | |
| fileSystemScheme = buildInfo.fileSystemScheme, | |
| assumeInitializeFromDillUpToDate = buildInfo.assumeInitializeFromDillUpToDate, | |
| frontendServerStarterPath = buildInfo.frontendServerStarterPath, | |
| _logger = logger, | |
| _stdoutHandler = stdoutHandler ?? StdoutHandler(logger: logger, fileSystem: fileSystem), | |
| dartDefines = buildInfo.dartDefines, | |
| // 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<String>.from(buildInfo.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; | |
| final Artifacts artifacts; | |
| final Platform _platform; | |
| final ShutdownHooks _shutdownHooks; | |
| final bool testCompilation; | |
| final BuildMode buildMode; | |
| final bool trackWidgetCreation; | |
| final bool includeUnsupportedPlatformLibraryStubs; | |
| final String? packagesPath; | |
| final TargetModel targetModel; | |
| final List<String> fileSystemRoots; | |
| final String? fileSystemScheme; | |
| final String? initializeFromDill; | |
| final bool assumeInitializeFromDillUpToDate; | |
| final String? frontendServerStarterPath; | |
| final List<String>? extraFrontEndOptions; | |
| final List<String> dartDefines; | |
| final String? librariesSpec; | |
| @override | |
| void addFileSystemRoot(String root) { | |
| fileSystemRoots.add(root); | |
| } | |
| /// The path to the root of the Dart SDK used to compile. | |
| /// | |
| /// This is used to resolve the [platformDill]. | |
| final String sdkRoot; | |
| /// The path to the platform dill file. | |
| /// | |
| /// This does not need to be provided for the normal Flutter workflow. | |
| final String? platformDill; | |
| Process? _server; | |
| final StdoutHandler _stdoutHandler; | |
| var _compileRequestNeedsConfirmation = false; | |
| final _controller = StreamController<_CompilationRequest>(); | |
| @override | |
| Future<CompilerOutput?> recompile( | |
| Uri mainUri, | |
| List<Uri>? invalidatedFiles, { | |
| required String outputPath, | |
| required PackageConfig packageConfig, | |
| bool suppressErrors = false, | |
| bool checkDartPluginRegistry = false, | |
| File? dartPluginRegistrant, | |
| String? projectRootPath, | |
| FileSystem? fs, | |
| Uri? nativeAssetsYaml, | |
| bool recompileRestart = false, | |
| }) async { | |
| if (!_controller.hasListener) { | |
| _controller.stream.listen(_handleCompilationRequest); | |
| } | |
| Uri? additionalSourceUri; | |
| // `dart_plugin_registrant.dart` contains the Dart plugin registry. | |
| if (checkDartPluginRegistry && | |
| dartPluginRegistrant != null && | |
| dartPluginRegistrant.existsSync()) { | |
| additionalSourceUri = dartPluginRegistrant.uri; | |
| } | |
| final completer = Completer<CompilerOutput?>(); | |
| _controller.add( | |
| _RecompileRequest( | |
| completer, | |
| mainUri, | |
| invalidatedFiles, | |
| outputPath, | |
| packageConfig, | |
| suppressErrors, | |
| additionalSourceUri: additionalSourceUri, | |
| nativeAssetsYamlUri: nativeAssetsYaml, | |
| recompileRestart: recompileRestart, | |
| ), | |
| ); | |
| return completer.future; | |
| } | |
| Future<CompilerOutput?> _recompile(_RecompileRequest request) async { | |
| _stdoutHandler.reset(); | |
| _compileRequestNeedsConfirmation = true; | |
| _stdoutHandler._suppressCompilerMessages = request.suppressErrors; | |
| final String mainUri = | |
| request.packageConfig.toPackageUriForWorkspace(request.mainUri)?.toString() ?? | |
| toMultiRootPath(request.mainUri, fileSystemScheme, fileSystemRoots, _platform.isWindows); | |
| String? additionalSourceUri; | |
| if (request.additionalSourceUri != null) { | |
| additionalSourceUri = | |
| request.packageConfig | |
| .toPackageUriForWorkspace(request.additionalSourceUri!) | |
| ?.toString() ?? | |
| toMultiRootPath( | |
| request.additionalSourceUri!, | |
| fileSystemScheme, | |
| fileSystemRoots, | |
| _platform.isWindows, | |
| ); | |
| } | |
| final nativeAssets = request.nativeAssetsYamlUri?.toString(); | |
| final Process? server = _server; | |
| if (server == null) { | |
| return _compile( | |
| mainUri, | |
| request.outputPath, | |
| additionalSourceUri: additionalSourceUri, | |
| nativeAssetsUri: nativeAssets, | |
| ); | |
| } | |
| final String inputKey = const Uuid().v4(); | |
| if (nativeAssets != null && nativeAssets.isNotEmpty) { | |
| server.stdin.writeln('native-assets $nativeAssets'); | |
| _logger.printTrace('<- native-assets $nativeAssets'); | |
| } | |
| if (request.recompileRestart) { | |
| server.stdin.writeln('recompile-restart $mainUri $inputKey'); | |
| } else { | |
| server.stdin.writeln('recompile $mainUri $inputKey'); | |
| } | |
| _logger.printTrace('<- recompile $mainUri $inputKey'); | |
| final List<Uri>? invalidatedFiles = request.invalidatedFiles; | |
| if (invalidatedFiles != null) { | |
| for (final Uri fileUri in invalidatedFiles) { | |
| String message; | |
| if (fileUri.scheme == 'package') { | |
| message = fileUri.toString(); | |
| } else { | |
| message = | |
| request.packageConfig.toPackageUriForWorkspace(fileUri)?.toString() ?? | |
| toMultiRootPath(fileUri, fileSystemScheme, fileSystemRoots, _platform.isWindows); | |
| } | |
| server.stdin.writeln(message); | |
| _logger.printTrace(message); | |
| } | |
| } | |
| server.stdin.writeln(inputKey); | |
| _logger.printTrace('<- $inputKey'); | |
| return _stdoutHandler.compilerOutput?.future; | |
| } | |
| final _compilationQueue = <_CompilationRequest>[]; | |
| Future<void> _handleCompilationRequest(_CompilationRequest request) async { | |
| final bool isEmpty = _compilationQueue.isEmpty; | |
| _compilationQueue.add(request); | |
| // Only trigger processing if queue was empty - i.e. no other requests | |
| // are currently being processed. This effectively enforces "one | |
| // compilation request at a time". | |
| if (isEmpty) { | |
| while (_compilationQueue.isNotEmpty) { | |
| final _CompilationRequest request = _compilationQueue.first; | |
| await request.run(this); | |
| _compilationQueue.removeAt(0); | |
| } | |
| } | |
| } | |
| Future<CompilerOutput?> _compile( | |
| String scriptUri, | |
| String? outputPath, { | |
| String? additionalSourceUri, | |
| String? nativeAssetsUri, | |
| }) async { | |
| final TargetPlatform? platform = (targetModel == TargetModel.dartdevc) | |
| ? TargetPlatform.web_javascript | |
| : null; | |
| late final List<String> commandToStartFrontendServer; | |
| if (frontendServerStarterPath != null && frontendServerStarterPath!.isNotEmpty) { | |
| commandToStartFrontendServer = <String>[ | |
| artifacts.getArtifactPath(Artifact.engineDartBinary, platform: platform), | |
| frontendServerStarterPath!, | |
| ]; | |
| } else { | |
| commandToStartFrontendServer = <String>[ | |
| artifacts.getArtifactPath(Artifact.engineDartAotRuntime, platform: platform), | |
| artifacts.getArtifactPath( | |
| Artifact.frontendServerSnapshotForEngineDartSdk, | |
| platform: platform, | |
| ), | |
| ]; | |
| } | |
| final command = <String>[ | |
| ...commandToStartFrontendServer, | |
| '--sdk-root', | |
| sdkRoot, | |
| '--incremental', | |
| if (testCompilation) '--no-print-incremental-dependencies', | |
| '--target=$targetModel', | |
| // TODO(annagrin): remove once this becomes the default behavior | |
| // in the frontend_server. | |
| // https://github.com/flutter/flutter/issues/59902 | |
| '--experimental-emit-debug-metadata', | |
| for (final Object dartDefine in dartDefines) '-D$dartDefine', | |
| if (outputPath != null) ...<String>['--output-dill', outputPath], | |
| // If we have a platform dill, we don't need to pass the libraries spec, | |
| // since the information is embedded in the .dill file. | |
| if (librariesSpec != null && platformDill == null) ...<String>[ | |
| '--libraries-spec', | |
| librariesSpec!, | |
| ], | |
| if (packagesPath != null) ...<String>['--packages', packagesPath!], | |
| ...buildModeOptions(buildMode, dartDefines), | |
| if (trackWidgetCreation) '--track-creation-locations', | |
| if (includeUnsupportedPlatformLibraryStubs) '--include-unsupported-platform-library-stubs', | |
| for (final String root in fileSystemRoots) ...<String>['--filesystem-root', root], | |
| if (fileSystemScheme != null) ...<String>['--filesystem-scheme', fileSystemScheme!], | |
| if (initializeFromDill != null) ...<String>['--initialize-from-dill', initializeFromDill!], | |
| if (assumeInitializeFromDillUpToDate) '--assume-initialize-from-dill-up-to-date', | |
| if (additionalSourceUri != null) ...<String>[ | |
| '--source', | |
| additionalSourceUri, | |
| '--source', | |
| 'package:flutter/src/dart_plugin_registrant.dart', | |
| '-Dflutter.dart_plugin_registrant=$additionalSourceUri', | |
| ], | |
| if (nativeAssetsUri != null) ...<String>['--native-assets', nativeAssetsUri], | |
| if (platformDill != null) ...<String>['--platform', platformDill!], | |
| // See: https://github.com/flutter/flutter/issues/103994 | |
| '--verbosity=error', | |
| ...?_filterExtraFrontEndOptions(extraFrontEndOptions), | |
| ]; | |
| _logger.printTrace(command.join(' ')); | |
| _server = await _processManager.start(command); | |
| _server?.stdout | |
| .transform(utf8LineDecoder) | |
| .listen( | |
| _stdoutHandler.handler, | |
| onDone: () { | |
| // when outputFilename future is not completed, but stdout is closed | |
| // process has died unexpectedly. | |
| if (_stdoutHandler.compilerOutput?.isCompleted == false && | |
| !_shutdownHooks.isShuttingDown) { | |
| _stdoutHandler.compilerOutput?.complete(); | |
| throwToolExit('The Dart compiler exited unexpectedly.'); | |
| } | |
| }, | |
| ); | |
| // Use permissive decoder for compiler stderr which may contain invalid UTF-8 | |
| _server?.stderr.transform(utf8AllowMalformedLineDecoder).listen(_logger.printError); | |
| unawaited( | |
| _server?.exitCode.then((int code) { | |
| // The frontend server exits with a 253 error code when we shutdown due to a signal. | |
| // Don't treat this as an error if we're in the middle of the shutdown sequence. | |
| if (code != 0 && !_shutdownHooks.isShuttingDown) { | |
| throwToolExit('The Dart compiler exited unexpectedly.'); | |
| } | |
| }), | |
| ); | |
| 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'); | |
| return _stdoutHandler.compilerOutput?.future; | |
| } | |
| @override | |
| Future<CompilerOutput?> compileExpression( | |
| String expression, | |
| List<String>? definitions, | |
| List<String>? definitionTypes, | |
| List<String>? typeDefinitions, | |
| List<String>? typeBounds, | |
| List<String>? typeDefaults, | |
| String? libraryUri, | |
| String? klass, | |
| String? method, | |
| bool isStatic, | |
| ) async { | |
| if (!_controller.hasListener) { | |
| _controller.stream.listen(_handleCompilationRequest); | |
| } | |
| final completer = Completer<CompilerOutput?>(); | |
| final request = _CompileExpressionRequest( | |
| completer, | |
| expression, | |
| definitions, | |
| definitionTypes, | |
| typeDefinitions, | |
| typeBounds, | |
| typeDefaults, | |
| libraryUri, | |
| klass, | |
| method, | |
| isStatic, | |
| ); | |
| _controller.add(request); | |
| return completer.future; | |
| } | |
| Future<CompilerOutput?> _compileExpression(_CompileExpressionRequest request) async { | |
| _stdoutHandler.reset(suppressCompilerMessages: true, expectSources: false, readFile: true); | |
| // 'compile-expression' should be invoked after compiler has been started, | |
| // program was compiled. | |
| final Process? server = _server; | |
| if (server == null) { | |
| return null; | |
| } | |
| server.stdin | |
| ..writeln('JSON_INPUT') | |
| ..writeln( | |
| json.encode(<String, Object?>{ | |
| 'type': 'COMPILE_EXPRESSION', | |
| 'data': <String, Object?>{ | |
| 'expression': request.expression, | |
| 'definitions': request.definitions ?? <String>[], | |
| 'definitionTypes': request.definitionTypes ?? <String>[], | |
| 'typeDefinitions': request.typeDefinitions ?? <String>[], | |
| 'typeBounds': request.typeBounds ?? <String>[], | |
| 'typeDefaults': request.typeDefaults ?? <String>[], | |
| 'libraryUri': request.libraryUri ?? '', | |
| 'class': request.klass, | |
| 'method': request.method, | |
| 'static': request.isStatic, | |
| }, | |
| }), | |
| ); | |
| return _stdoutHandler.compilerOutput?.future; | |
| } | |
| @override | |
| Future<CompilerOutput?> compileExpressionToJs( | |
| String libraryUri, | |
| String scriptUri, | |
| int line, | |
| int column, | |
| Map<String, String> jsModules, | |
| Map<String, String> jsFrameValues, | |
| String moduleName, | |
| String expression, | |
| ) { | |
| if (!_controller.hasListener) { | |
| _controller.stream.listen(_handleCompilationRequest); | |
| } | |
| final completer = Completer<CompilerOutput?>(); | |
| _controller.add( | |
| _CompileExpressionToJsRequest( | |
| completer, | |
| libraryUri, | |
| scriptUri, | |
| line, | |
| column, | |
| jsModules, | |
| jsFrameValues, | |
| moduleName, | |
| expression, | |
| ), | |
| ); | |
| return completer.future; | |
| } | |
| Future<CompilerOutput?> _compileExpressionToJs(_CompileExpressionToJsRequest request) async { | |
| _stdoutHandler.reset(suppressCompilerMessages: true, expectSources: false); | |
| // 'compile-expression-to-js' should be invoked after compiler has been started, | |
| // program was compiled. | |
| final Process? server = _server; | |
| if (server == null) { | |
| return null; | |
| } | |
| server.stdin | |
| ..writeln('JSON_INPUT') | |
| ..writeln( | |
| json.encode({ | |
| 'type': 'COMPILE_EXPRESSION_JS', | |
| 'data': { | |
| 'expression': request.expression, | |
| 'libraryUri': request.libraryUri, | |
| 'scriptUri': request.scriptUri, | |
| 'line': request.line, | |
| 'column': request.column, | |
| 'jsModules': request.jsModules, | |
| 'jsFrameValues': request.jsFrameValues, | |
| 'moduleName': request.moduleName, | |
| }, | |
| }), | |
| ); | |
| return _stdoutHandler.compilerOutput?.future; | |
| } | |
| @override | |
| void accept() { | |
| if (_compileRequestNeedsConfirmation) { | |
| _server?.stdin.writeln('accept'); | |
| _logger.printTrace('<- accept'); | |
| } | |
| _compileRequestNeedsConfirmation = false; | |
| } | |
| @override | |
| Future<CompilerOutput?> reject() { | |
| if (!_controller.hasListener) { | |
| _controller.stream.listen(_handleCompilationRequest); | |
| } | |
| final completer = Completer<CompilerOutput?>(); | |
| _controller.add(_RejectRequest(completer)); | |
| return completer.future; | |
| } | |
| Future<CompilerOutput?> _reject() async { | |
| if (!_compileRequestNeedsConfirmation) { | |
| return Future<CompilerOutput?>.value(); | |
| } | |
| _stdoutHandler.reset(expectSources: false); | |
| _server?.stdin.writeln('reject'); | |
| _logger.printTrace('<- reject'); | |
| _compileRequestNeedsConfirmation = false; | |
| return _stdoutHandler.compilerOutput?.future; | |
| } | |
| @override | |
| void reset() { | |
| _server?.stdin.writeln('reset'); | |
| _logger.printTrace('<- reset'); | |
| } | |
| @override | |
| Future<Object> shutdown() async { | |
| // Server was never successfully created. | |
| final Process? server = _server; | |
| if (server == null) { | |
| return 0; | |
| } | |
| _logger.printTrace('killing pid ${server.pid}'); | |
| server.kill(); | |
| return server.exitCode; | |
| } | |
| } | |
| /// Convert a file URI into a multi-root scheme URI if provided, otherwise | |
| /// return unmodified. | |
| @visibleForTesting | |
| String toMultiRootPath(Uri fileUri, String? scheme, List<String> fileSystemRoots, bool windows) { | |
| if (scheme == null || fileSystemRoots.isEmpty || fileUri.scheme != 'file') { | |
| return fileUri.toString(); | |
| } | |
| final String filePath = fileUri.toFilePath(windows: windows); | |
| for (final fileSystemRoot in fileSystemRoots) { | |
| if (filePath.startsWith(fileSystemRoot)) { | |
| return '$scheme://${filePath.substring(fileSystemRoot.length)}'; | |
| } | |
| } | |
| return fileUri.toString(); | |
| } |