| // 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:usage/uuid/uuid.dart'; | |
| import 'artifacts.dart'; | |
| import 'base/common.dart'; | |
| import 'base/file_system.dart'; | |
| import 'base/io.dart'; | |
| import 'base/logger.dart'; | |
| import 'base/platform.dart'; | |
| import 'build_info.dart'; | |
| import 'convert.dart'; | |
| /// Opt-in changes to the dart compilers. | |
| const List<String> 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) { | |
| switch (rawValue) { | |
| case 'flutter': | |
| return flutter; | |
| case 'flutter_runner': | |
| return flutterRunner; | |
| case 'vm': | |
| return vm; | |
| case 'dartdevc': | |
| return dartdevc; | |
| } | |
| throw Exception('Unexpected target model $rawValue'); | |
| } | |
| const TargetModel._(this._value); | |
| /// The Flutter patched Dart SDK. | |
| static const TargetModel flutter = TargetModel._('flutter'); | |
| /// The Fuchsia patched SDK. | |
| static const TargetModel flutterRunner = TargetModel._('flutter_runner'); | |
| /// The Dart VM. | |
| static const TargetModel vm = TargetModel._('vm'); | |
| /// The development compiler for JavaScript. | |
| static const TargetModel dartdevc = TargetModel._('dartdevc'); | |
| final String _value; | |
| @override | |
| String toString() => _value; | |
| } | |
| class CompilerOutput { | |
| const CompilerOutput(this.outputFilename, this.errorCount, this.sources, {this.expressionData}); | |
| final String outputFilename; | |
| final int errorCount; | |
| final List<Uri> sources; | |
| /// This field is only non-null for expression compilation requests. | |
| final Uint8List? expressionData; | |
| } | |
| enum StdoutState { CollectDiagnostic, CollectDependencies } | |
| /// Handles stdin/stdout communication with the frontend server. | |
| class StdoutHandler { | |
| StdoutHandler({ | |
| required Logger logger, | |
| required FileSystem fileSystem, | |
| }) : _logger = logger, | |
| _fileSystem = fileSystem { | |
| reset(); | |
| } | |
| final Logger _logger; | |
| final FileSystem _fileSystem; | |
| String? boundaryKey; | |
| StdoutState state = StdoutState.CollectDiagnostic; | |
| Completer<CompilerOutput?>? compilerOutput; | |
| final List<Uri> sources = <Uri>[]; | |
| bool _suppressCompilerMessages = false; | |
| bool _expectSources = true; | |
| bool _readFile = false; | |
| void handler(String message) { | |
| const String 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 CompilerOutput output = CompilerOutput( | |
| fileName, | |
| errorCount, | |
| sources, | |
| expressionData: expressionData, | |
| ); | |
| compilerOutput?.complete(output); | |
| return; | |
| } | |
| if (state == StdoutState.CollectDiagnostic) { | |
| if (!_suppressCompilerMessages) { | |
| _logger.printError(message); | |
| } else { | |
| _logger.printTrace(message); | |
| } | |
| } else { | |
| assert(state == 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; | |
| } | |
| } | |
| /// 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', | |
| ...kDartCompilerExperiments, | |
| ], | |
| BuildMode.release => <String>[ | |
| '-Ddart.vm.profile=false', | |
| '-Ddart.vm.product=true', | |
| ...kDartCompilerExperiments, | |
| ], | |
| _ => throw Exception('Unknown BuildMode: $mode') | |
| }; | |
| /// A compiler interface for producing single (non-incremental) kernel files. | |
| class KernelCompiler { | |
| KernelCompiler({ | |
| required FileSystem fileSystem, | |
| required Logger logger, | |
| required ProcessManager processManager, | |
| required Artifacts artifacts, | |
| required List<String> fileSystemRoots, | |
| String? fileSystemScheme, | |
| @visibleForTesting StdoutHandler? stdoutHandler, | |
| }) : _logger = logger, | |
| _fileSystem = fileSystem, | |
| _artifacts = artifacts, | |
| _processManager = processManager, | |
| _fileSystemScheme = fileSystemScheme, | |
| _fileSystemRoots = fileSystemRoots, | |
| _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, | |
| 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, | |
| }) async { | |
| final TargetPlatform? platform = targetModel == TargetModel.dartdevc ? TargetPlatform.web_javascript : null; | |
| final String frontendServer = _artifacts.getArtifactPath( | |
| Artifact.frontendServerSnapshotForEngineDartSdk, | |
| platform: platform, | |
| ); | |
| // This is a URI, not a file path, so the forward slash is correct even on Windows. | |
| if (!sdkRoot.endsWith('/')) { | |
| sdkRoot = '$sdkRoot/'; | |
| } | |
| final String engineDartPath = _artifacts.getArtifactPath(Artifact.engineDartBinary, platform: platform); | |
| if (!_processManager.canRun(engineDartPath)) { | |
| throwToolExit('Unable to find Dart binary at $engineDartPath'); | |
| } | |
| String? mainUri; | |
| final File mainFile = _fileSystem.file(mainPath); | |
| final Uri mainFileUri = mainFile.uri; | |
| if (packagesPath != null) { | |
| mainUri = packageConfig.toPackageUri(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.toPackageUri(dartPluginRegistrantFileUri)?.toString() ?? | |
| toMultiRootPath(dartPluginRegistrantFileUri, _fileSystemScheme, _fileSystemRoots, _fileSystem.path.separator == r'\'); | |
| } | |
| final List<String> command = <String>[ | |
| engineDartPath, | |
| '--disable-dart-dev', | |
| frontendServer, | |
| '--sdk-root', | |
| sdkRoot, | |
| '--target=$targetModel', | |
| '--no-print-incremental-dependencies', | |
| for (final Object dartDefine in dartDefines) | |
| '-D$dartDefine', | |
| ...buildModeOptions(buildMode, dartDefines), | |
| if (trackWidgetCreation) '--track-widget-creation', | |
| 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', | |
| ], | |
| // See: https://github.com/flutter/flutter/issues/103994 | |
| '--verbosity=error', | |
| ...?extraFrontEndOptions, | |
| mainUri, | |
| ]; | |
| _logger.printTrace(command.join(' ')); | |
| final Process server = await _processManager.start(command); | |
| server.stderr | |
| .transform<String>(utf8.decoder) | |
| .listen(_logger.printError); | |
| server.stdout | |
| .transform<String>(utf8.decoder) | |
| .transform<String>(const LineSplitter()) | |
| .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} | |
| ); | |
| Uri mainUri; | |
| List<Uri>? invalidatedFiles; | |
| String outputPath; | |
| PackageConfig packageConfig; | |
| bool suppressErrors; | |
| final Uri? additionalSourceUri; | |
| @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.line, | |
| this.column, | |
| this.jsModules, | |
| this.jsFrameValues, | |
| this.moduleName, | |
| this.expression, | |
| ); | |
| final String? libraryUri; | |
| 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(); | |
| } | |
| /// 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 class ResidentCompiler { | |
| factory ResidentCompiler(String sdkRoot, { | |
| required BuildMode buildMode, | |
| required Logger logger, | |
| required ProcessManager processManager, | |
| required Artifacts artifacts, | |
| required Platform platform, | |
| required FileSystem fileSystem, | |
| bool testCompilation, | |
| bool trackWidgetCreation, | |
| String packagesPath, | |
| List<String> fileSystemRoots, | |
| String? fileSystemScheme, | |
| String initializeFromDill, | |
| bool assumeInitializeFromDillUpToDate, | |
| TargetModel targetModel, | |
| bool unsafePackageSerialization, | |
| List<String> extraFrontEndOptions, | |
| String platformDill, | |
| List<String>? dartDefines, | |
| String librariesSpec, | |
| }) = DefaultResidentCompiler; | |
| // 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 Dart script identified by | |
| /// [mainPath], [invalidatedFiles] list is ignored. | |
| /// On successive runs [invalidatedFiles] indicates which files need to be | |
| /// recompiled. If [mainPath] is [null], previously used [mainPath] entry | |
| /// point that is used for recompilation. | |
| /// 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]. | |
| 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, | |
| }); | |
| 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, | |
| 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 this.buildMode, | |
| required Logger logger, | |
| required ProcessManager processManager, | |
| required Artifacts artifacts, | |
| required Platform platform, | |
| required FileSystem fileSystem, | |
| this.testCompilation = false, | |
| this.trackWidgetCreation = true, | |
| this.packagesPath, | |
| List<String> fileSystemRoots = const <String>[], | |
| this.fileSystemScheme, | |
| this.initializeFromDill, | |
| this.assumeInitializeFromDillUpToDate = false, | |
| this.targetModel = TargetModel.flutter, | |
| this.unsafePackageSerialization = false, | |
| this.extraFrontEndOptions, | |
| this.platformDill, | |
| List<String>? dartDefines, | |
| this.librariesSpec, | |
| @visibleForTesting StdoutHandler? stdoutHandler, | |
| }) : _logger = logger, | |
| _processManager = processManager, | |
| _artifacts = artifacts, | |
| _stdoutHandler = stdoutHandler ?? StdoutHandler(logger: logger, fileSystem: fileSystem), | |
| _platform = platform, | |
| dartDefines = dartDefines ?? const <String>[], | |
| // 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(fileSystemRoots); | |
| final Logger _logger; | |
| final ProcessManager _processManager; | |
| final Artifacts _artifacts; | |
| final Platform _platform; | |
| final bool testCompilation; | |
| final BuildMode buildMode; | |
| final bool trackWidgetCreation; | |
| final String? packagesPath; | |
| final TargetModel targetModel; | |
| final List<String> fileSystemRoots; | |
| final String? fileSystemScheme; | |
| final String? initializeFromDill; | |
| final bool assumeInitializeFromDillUpToDate; | |
| final bool unsafePackageSerialization; | |
| 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; | |
| bool _compileRequestNeedsConfirmation = false; | |
| final StreamController<_CompilationRequest> _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, | |
| }) 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<CompilerOutput?> completer = Completer<CompilerOutput?>(); | |
| _controller.add(_RecompileRequest( | |
| completer, | |
| mainUri, | |
| invalidatedFiles, | |
| outputPath, | |
| packageConfig, | |
| suppressErrors, | |
| additionalSourceUri: additionalSourceUri, | |
| )); | |
| return completer.future; | |
| } | |
| Future<CompilerOutput?> _recompile(_RecompileRequest request) async { | |
| _stdoutHandler.reset(); | |
| _compileRequestNeedsConfirmation = true; | |
| _stdoutHandler._suppressCompilerMessages = request.suppressErrors; | |
| final String mainUri = request.packageConfig.toPackageUri(request.mainUri)?.toString() ?? | |
| toMultiRootPath(request.mainUri, fileSystemScheme, fileSystemRoots, _platform.isWindows); | |
| String? additionalSourceUri; | |
| if (request.additionalSourceUri != null) { | |
| additionalSourceUri = request.packageConfig.toPackageUri(request.additionalSourceUri!)?.toString() ?? | |
| toMultiRootPath(request.additionalSourceUri!, fileSystemScheme, fileSystemRoots, _platform.isWindows); | |
| } | |
| final Process? server = _server; | |
| if (server == null) { | |
| return _compile(mainUri, request.outputPath, additionalSourceUri: additionalSourceUri); | |
| } | |
| final String inputKey = Uuid().generateV4(); | |
| 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.toPackageUri(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 List<_CompilationRequest> _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} | |
| ) async { | |
| final TargetPlatform? platform = (targetModel == TargetModel.dartdevc) ? TargetPlatform.web_javascript : null; | |
| final String frontendServer = _artifacts.getArtifactPath( | |
| Artifact.frontendServerSnapshotForEngineDartSdk, | |
| platform: platform, | |
| ); | |
| final List<String> command = <String>[ | |
| _artifacts.getArtifactPath(Artifact.engineDartBinary, platform: platform), | |
| '--disable-dart-dev', | |
| frontendServer, | |
| '--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-widget-creation', | |
| 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 (platformDill != null) ...<String>[ | |
| '--platform', | |
| platformDill!, | |
| ], | |
| if (unsafePackageSerialization) '--unsafe-package-serialization', | |
| // See: https://github.com/flutter/flutter/issues/103994 | |
| '--verbosity=error', | |
| ...?extraFrontEndOptions, | |
| ]; | |
| _logger.printTrace(command.join(' ')); | |
| _server = await _processManager.start(command); | |
| _server?.stdout | |
| .transform<String>(utf8.decoder) | |
| .transform<String>(const LineSplitter()) | |
| .listen( | |
| _stdoutHandler.handler, | |
| onDone: () { | |
| // when outputFilename future is not completed, but stdout is closed | |
| // process has died unexpectedly. | |
| if (_stdoutHandler.compilerOutput?.isCompleted == false) { | |
| _stdoutHandler.compilerOutput?.complete(); | |
| throwToolExit('the Dart compiler exited unexpectedly.'); | |
| } | |
| }); | |
| _server?.stderr | |
| .transform<String>(utf8.decoder) | |
| .transform<String>(const LineSplitter()) | |
| .listen(_logger.printError); | |
| unawaited(_server?.exitCode.then((int code) { | |
| if (code != 0) { | |
| throwToolExit('the Dart compiler exited unexpectedly.'); | |
| } | |
| })); | |
| _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<CompilerOutput?> completer = Completer<CompilerOutput?>(); | |
| final _CompileExpressionRequest 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; | |
| } | |
| final String inputKey = Uuid().generateV4(); | |
| server.stdin | |
| ..writeln('compile-expression $inputKey') | |
| ..writeln(request.expression); | |
| request.definitions?.forEach(server.stdin.writeln); | |
| server.stdin.writeln(inputKey); | |
| request.definitionTypes?.forEach(server.stdin.writeln); | |
| server.stdin.writeln(inputKey); | |
| request.typeDefinitions?.forEach(server.stdin.writeln); | |
| server.stdin.writeln(inputKey); | |
| request.typeBounds?.forEach(server.stdin.writeln); | |
| server.stdin.writeln(inputKey); | |
| request.typeDefaults?.forEach(server.stdin.writeln); | |
| server.stdin | |
| ..writeln(inputKey) | |
| ..writeln(request.libraryUri ?? '') | |
| ..writeln(request.klass ?? '') | |
| ..writeln(request.method ?? '') | |
| ..writeln(request.isStatic); | |
| return _stdoutHandler.compilerOutput?.future; | |
| } | |
| @override | |
| Future<CompilerOutput?> compileExpressionToJs( | |
| String libraryUri, | |
| 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<CompilerOutput?> completer = Completer<CompilerOutput?>(); | |
| _controller.add( | |
| _CompileExpressionToJsRequest( | |
| completer, libraryUri, 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; | |
| } | |
| final String inputKey = Uuid().generateV4(); | |
| server.stdin | |
| ..writeln('compile-expression-to-js $inputKey') | |
| ..writeln(request.libraryUri ?? '') | |
| ..writeln(request.line) | |
| ..writeln(request.column); | |
| request.jsModules?.forEach((String k, String v) { server.stdin.writeln('$k:$v'); }); | |
| server.stdin.writeln(inputKey); | |
| request.jsFrameValues?.forEach((String k, String v) { server.stdin.writeln('$k:$v'); }); | |
| server.stdin | |
| ..writeln(inputKey) | |
| ..writeln(request.moduleName ?? '') | |
| ..writeln(request.expression ?? ''); | |
| 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<CompilerOutput?> 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 String fileSystemRoot in fileSystemRoots) { | |
| if (filePath.startsWith(fileSystemRoot)) { | |
| return '$scheme://${filePath.substring(fileSystemRoot.length)}'; | |
| } | |
| } | |
| return fileUri.toString(); | |
| } |