1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6import 'dart:typed_data';
7
8import 'package:meta/meta.dart';
9import 'package:package_config/package_config.dart';
10import 'package:process/process.dart';
11import 'package:usage/uuid/uuid.dart';
12
13import 'artifacts.dart';
14import 'base/common.dart';
15import 'base/file_system.dart';
16import 'base/io.dart';
17import 'base/logger.dart';
18import 'base/platform.dart';
19import 'build_info.dart';
20import 'convert.dart';
21
22/// Opt-in changes to the dart compilers.
23const kDartCompilerExperiments = <String>[];
24
25/// The target model describes the set of core libraries that are available within
26/// the SDK.
27class TargetModel {
28 /// Parse a [TargetModel] from a raw string.
29 ///
30 /// Throws an exception if passed a value other than 'flutter',
31 /// 'flutter_runner', 'vm', or 'dartdevc'.
32 factory TargetModel(String rawValue) {
33 return switch (rawValue) {
34 'flutter' => flutter,
35 'flutter_runner' => flutterRunner,
36 'vm' => vm,
37 'dartdevc' => dartdevc,
38 _ => throw Exception('Unexpected target model $rawValue'),
39 };
40 }
41
42 const TargetModel._(this._value);
43
44 /// The Flutter patched Dart SDK.
45 static const flutter = TargetModel._('flutter');
46
47 /// The Fuchsia patched SDK.
48 static const flutterRunner = TargetModel._('flutter_runner');
49
50 /// The Dart VM.
51 static const vm = TargetModel._('vm');
52
53 /// The development compiler for JavaScript.
54 static const dartdevc = TargetModel._('dartdevc');
55
56 final String _value;
57
58 @override
59 String toString() => _value;
60}
61
62class CompilerOutput {
63 const CompilerOutput(
64 this.outputFilename,
65 this.errorCount,
66 this.sources, {
67 this.expressionData,
68 this.errorMessage,
69 });
70
71 final String outputFilename;
72 final int errorCount;
73 final List<Uri> sources;
74
75 /// This field is only non-null for expression compilation requests.
76 final Uint8List? expressionData;
77
78 /// This field is only non-null when a compilation error was encountered.
79 final String? errorMessage;
80}
81
82enum StdoutState { CollectDiagnostic, CollectDependencies }
83
84/// Handles stdin/stdout communication with the frontend server.
85class StdoutHandler {
86 StdoutHandler({required Logger logger, required FileSystem fileSystem})
87 : _logger = logger,
88 _fileSystem = fileSystem {
89 reset();
90 }
91
92 final Logger _logger;
93 final FileSystem _fileSystem;
94
95 String? boundaryKey;
96 StdoutState state = StdoutState.CollectDiagnostic;
97 Completer<CompilerOutput?>? compilerOutput;
98 final sources = <Uri>[];
99
100 var _suppressCompilerMessages = false;
101 var _expectSources = true;
102 var _readFile = false;
103 var _errorBuffer = StringBuffer();
104
105 void handler(String message) {
106 const kResultPrefix = 'result ';
107 if (boundaryKey == null && message.startsWith(kResultPrefix)) {
108 boundaryKey = message.substring(kResultPrefix.length);
109 return;
110 }
111 final String? messageBoundaryKey = boundaryKey;
112 if (messageBoundaryKey != null && message.startsWith(messageBoundaryKey)) {
113 if (_expectSources) {
114 if (state == StdoutState.CollectDiagnostic) {
115 state = StdoutState.CollectDependencies;
116 return;
117 }
118 }
119 if (message.length <= messageBoundaryKey.length) {
120 compilerOutput?.complete();
121 return;
122 }
123 final int spaceDelimiter = message.lastIndexOf(' ');
124 final String fileName = message.substring(messageBoundaryKey.length + 1, spaceDelimiter);
125 final int errorCount = int.parse(message.substring(spaceDelimiter + 1).trim());
126 Uint8List? expressionData;
127 if (_readFile) {
128 expressionData = _fileSystem.file(fileName).readAsBytesSync();
129 }
130 final output = CompilerOutput(
131 fileName,
132 errorCount,
133 sources,
134 expressionData: expressionData,
135 errorMessage: _errorBuffer.isNotEmpty ? _errorBuffer.toString() : null,
136 );
137 compilerOutput?.complete(output);
138 return;
139 }
140 switch (state) {
141 case StdoutState.CollectDiagnostic when _suppressCompilerMessages:
142 _logger.printTrace(message);
143 _errorBuffer.writeln(message);
144 case StdoutState.CollectDiagnostic:
145 _logger.printError(message);
146 _errorBuffer.writeln(message);
147 case StdoutState.CollectDependencies:
148 switch (message[0]) {
149 case '+':
150 sources.add(Uri.parse(message.substring(1)));
151 case '-':
152 sources.remove(Uri.parse(message.substring(1)));
153 default:
154 _logger.printTrace('Unexpected prefix for $message uri - ignoring');
155 }
156 }
157 }
158
159 // This is needed to get ready to process next compilation result output,
160 // with its own boundary key and new completer.
161 void reset({
162 bool suppressCompilerMessages = false,
163 bool expectSources = true,
164 bool readFile = false,
165 }) {
166 boundaryKey = null;
167 compilerOutput = Completer<CompilerOutput?>();
168 _suppressCompilerMessages = suppressCompilerMessages;
169 _expectSources = expectSources;
170 _readFile = readFile;
171 state = StdoutState.CollectDiagnostic;
172 _errorBuffer = StringBuffer();
173 }
174}
175
176/// List the preconfigured build options for a given build mode.
177List<String> buildModeOptions(BuildMode mode, List<String> dartDefines) => switch (mode) {
178 BuildMode.debug => <String>[
179 // These checks allow the CLI to override the value of this define for unit
180 // testing the framework.
181 if (!dartDefines.any((String define) => define.startsWith('dart.vm.profile')))
182 '-Ddart.vm.profile=false',
183 if (!dartDefines.any((String define) => define.startsWith('dart.vm.product')))
184 '-Ddart.vm.product=false',
185 '--enable-asserts',
186 ],
187 BuildMode.profile => <String>[
188 // These checks allow the CLI to override the value of this define for
189 // benchmarks with most timeline traces disabled.
190 if (!dartDefines.any((String define) => define.startsWith('dart.vm.profile')))
191 '-Ddart.vm.profile=true',
192 if (!dartDefines.any((String define) => define.startsWith('dart.vm.product')))
193 '-Ddart.vm.product=false',
194 '--delete-tostring-package-uri=dart:ui',
195 '--delete-tostring-package-uri=package:flutter',
196 ...kDartCompilerExperiments,
197 ],
198 BuildMode.release => <String>[
199 '-Ddart.vm.profile=false',
200 '-Ddart.vm.product=true',
201 '--delete-tostring-package-uri=dart:ui',
202 '--delete-tostring-package-uri=package:flutter',
203 ...kDartCompilerExperiments,
204 ],
205 _ => throw Exception('Unknown BuildMode: $mode'),
206};
207
208/// A compiler interface for producing single (non-incremental) kernel files.
209class KernelCompiler {
210 KernelCompiler({
211 required FileSystem fileSystem,
212 required Logger logger,
213 required ProcessManager processManager,
214 required Artifacts artifacts,
215 required List<String> fileSystemRoots,
216 String? fileSystemScheme,
217 @visibleForTesting StdoutHandler? stdoutHandler,
218 }) : _logger = logger,
219 _fileSystem = fileSystem,
220 _artifacts = artifacts,
221 _processManager = processManager,
222 _fileSystemScheme = fileSystemScheme,
223 _fileSystemRoots = fileSystemRoots,
224 _stdoutHandler = stdoutHandler ?? StdoutHandler(logger: logger, fileSystem: fileSystem);
225
226 final FileSystem _fileSystem;
227 final Artifacts _artifacts;
228 final ProcessManager _processManager;
229 final Logger _logger;
230 final String? _fileSystemScheme;
231 final List<String> _fileSystemRoots;
232 final StdoutHandler _stdoutHandler;
233
234 Future<CompilerOutput?> compile({
235 required String sdkRoot,
236 String? mainPath,
237 String? outputFilePath,
238 String? depFilePath,
239 TargetModel targetModel = TargetModel.flutter,
240 bool linkPlatformKernelIn = false,
241 bool aot = false,
242 String? frontendServerStarterPath,
243 List<String>? extraFrontEndOptions,
244 List<String>? fileSystemRoots,
245 String? fileSystemScheme,
246 String? initializeFromDill,
247 String? platformDill,
248 Directory? buildDir,
249 String? targetOS,
250 bool checkDartPluginRegistry = false,
251 required String? packagesPath,
252 required BuildMode buildMode,
253 required bool trackWidgetCreation,
254 required List<String> dartDefines,
255 required PackageConfig packageConfig,
256 String? nativeAssets,
257 }) async {
258 final TargetPlatform? platform = targetModel == TargetModel.dartdevc
259 ? TargetPlatform.web_javascript
260 : null;
261 // This is a URI, not a file path, so the forward slash is correct even on Windows.
262 if (!sdkRoot.endsWith('/')) {
263 sdkRoot = '$sdkRoot/';
264 }
265 String? mainUri;
266 if (mainPath != null) {
267 final File mainFile = _fileSystem.file(mainPath);
268 final Uri mainFileUri = mainFile.uri;
269 if (packagesPath != null) {
270 mainUri = packageConfig.toPackageUri(mainFileUri)?.toString();
271 }
272 mainUri ??= toMultiRootPath(
273 mainFileUri,
274 _fileSystemScheme,
275 _fileSystemRoots,
276 _fileSystem.path.separator == r'\',
277 );
278 }
279 if (outputFilePath != null && !_fileSystem.isFileSync(outputFilePath)) {
280 _fileSystem.file(outputFilePath).createSync(recursive: true);
281 }
282
283 // Check if there's a Dart plugin registrant.
284 // This is contained in the file `dart_plugin_registrant.dart` under `.dart_tools/flutter_build/`.
285 final File? dartPluginRegistrant = checkDartPluginRegistry
286 ? buildDir?.parent.childFile('dart_plugin_registrant.dart')
287 : null;
288
289 String? dartPluginRegistrantUri;
290 if (dartPluginRegistrant != null && dartPluginRegistrant.existsSync()) {
291 final Uri dartPluginRegistrantFileUri = dartPluginRegistrant.uri;
292 dartPluginRegistrantUri =
293 packageConfig.toPackageUri(dartPluginRegistrantFileUri)?.toString() ??
294 toMultiRootPath(
295 dartPluginRegistrantFileUri,
296 _fileSystemScheme,
297 _fileSystemRoots,
298 _fileSystem.path.separator == r'\',
299 );
300 }
301
302 final List<String> commandToStartFrontendServer;
303 if (frontendServerStarterPath != null && frontendServerStarterPath.isNotEmpty) {
304 final String engineDartPath = _artifacts.getArtifactPath(
305 Artifact.engineDartBinary,
306 platform: platform,
307 );
308 if (!_processManager.canRun(engineDartPath)) {
309 throwToolExit('Unable to find Dart binary at $engineDartPath');
310 }
311 commandToStartFrontendServer = <String>[engineDartPath, frontendServerStarterPath];
312 } else {
313 final String engineDartAotRuntimePath = _artifacts.getArtifactPath(
314 Artifact.engineDartAotRuntime,
315 platform: platform,
316 );
317 if (!_processManager.canRun(engineDartAotRuntimePath)) {
318 throwToolExit('Unable to find dartaotruntime binary at $engineDartAotRuntimePath');
319 }
320 commandToStartFrontendServer = <String>[
321 engineDartAotRuntimePath,
322 _artifacts.getArtifactPath(
323 Artifact.frontendServerSnapshotForEngineDartSdk,
324 platform: platform,
325 ),
326 ];
327 }
328
329 final List<String> command =
330 commandToStartFrontendServer +
331 <String>[
332 '--sdk-root',
333 sdkRoot,
334 '--target=$targetModel',
335 '--no-print-incremental-dependencies',
336 for (final Object dartDefine in dartDefines) '-D$dartDefine',
337 ...buildModeOptions(buildMode, dartDefines),
338 if (trackWidgetCreation) '--track-widget-creation',
339 if (!linkPlatformKernelIn) '--no-link-platform',
340 if (aot) ...<String>[
341 '--aot',
342 '--tfa',
343 // The --target-os flag only makes sense for whole program compilation.
344 if (targetOS != null) ...<String>['--target-os', targetOS],
345 ],
346 if (packagesPath != null) ...<String>['--packages', packagesPath],
347 if (outputFilePath != null) ...<String>['--output-dill', outputFilePath],
348 if (depFilePath != null &&
349 (fileSystemRoots == null || fileSystemRoots.isEmpty)) ...<String>[
350 '--depfile',
351 depFilePath,
352 ],
353 if (fileSystemRoots != null)
354 for (final String root in fileSystemRoots) ...<String>['--filesystem-root', root],
355 if (fileSystemScheme != null) ...<String>['--filesystem-scheme', fileSystemScheme],
356 if (initializeFromDill != null) ...<String>[
357 '--incremental',
358 '--initialize-from-dill',
359 initializeFromDill,
360 ],
361 if (platformDill != null) ...<String>['--platform', platformDill],
362 if (dartPluginRegistrantUri != null) ...<String>[
363 '--source',
364 dartPluginRegistrantUri,
365 '--source',
366 'package:flutter/src/dart_plugin_registrant.dart',
367 '-Dflutter.dart_plugin_registrant=$dartPluginRegistrantUri',
368 ],
369 if (nativeAssets != null) ...<String>['--native-assets', nativeAssets],
370 // See: https://github.com/flutter/flutter/issues/103994
371 '--verbosity=error',
372 ...?extraFrontEndOptions,
373 if (mainUri != null) mainUri else '--native-assets-only',
374 ];
375
376 _logger.printTrace(command.join(' '));
377 final Process server = await _processManager.start(command);
378
379 server.stderr.transform<String>(utf8.decoder).listen(_logger.printError);
380 server.stdout
381 .transform<String>(utf8.decoder)
382 .transform<String>(const LineSplitter())
383 .listen(_stdoutHandler.handler);
384 final int exitCode = await server.exitCode;
385 if (exitCode == 0) {
386 return _stdoutHandler.compilerOutput?.future;
387 }
388 return null;
389 }
390}
391
392/// Class that allows to serialize compilation requests to the compiler.
393abstract class _CompilationRequest {
394 _CompilationRequest(this.completer);
395
396 Completer<CompilerOutput?> completer;
397
398 Future<CompilerOutput?> _run(DefaultResidentCompiler compiler);
399
400 Future<void> run(DefaultResidentCompiler compiler) async {
401 completer.complete(await _run(compiler));
402 }
403}
404
405class _RecompileRequest extends _CompilationRequest {
406 _RecompileRequest(
407 super.completer,
408 this.mainUri,
409 this.invalidatedFiles,
410 this.outputPath,
411 this.packageConfig,
412 this.suppressErrors, {
413 this.additionalSourceUri,
414 this.nativeAssetsYamlUri,
415 required this.recompileRestart,
416 });
417
418 Uri mainUri;
419 List<Uri>? invalidatedFiles;
420 String outputPath;
421 PackageConfig packageConfig;
422 bool suppressErrors;
423 final Uri? additionalSourceUri;
424 final Uri? nativeAssetsYamlUri;
425 final bool recompileRestart;
426
427 @override
428 Future<CompilerOutput?> _run(DefaultResidentCompiler compiler) async => compiler._recompile(this);
429}
430
431class _CompileExpressionRequest extends _CompilationRequest {
432 _CompileExpressionRequest(
433 super.completer,
434 this.expression,
435 this.definitions,
436 this.definitionTypes,
437 this.typeDefinitions,
438 this.typeBounds,
439 this.typeDefaults,
440 this.libraryUri,
441 this.klass,
442 this.method,
443 this.isStatic,
444 );
445
446 String expression;
447 List<String>? definitions;
448 List<String>? definitionTypes;
449 List<String>? typeDefinitions;
450 List<String>? typeBounds;
451 List<String>? typeDefaults;
452 String? libraryUri;
453 String? klass;
454 String? method;
455 bool isStatic;
456
457 @override
458 Future<CompilerOutput?> _run(DefaultResidentCompiler compiler) async =>
459 compiler._compileExpression(this);
460}
461
462class _CompileExpressionToJsRequest extends _CompilationRequest {
463 _CompileExpressionToJsRequest(
464 super.completer,
465 this.libraryUri,
466 this.line,
467 this.column,
468 this.jsModules,
469 this.jsFrameValues,
470 this.moduleName,
471 this.expression,
472 );
473
474 final String? libraryUri;
475 final int line;
476 final int column;
477 final Map<String, String>? jsModules;
478 final Map<String, String>? jsFrameValues;
479 final String? moduleName;
480 final String? expression;
481
482 @override
483 Future<CompilerOutput?> _run(DefaultResidentCompiler compiler) async =>
484 compiler._compileExpressionToJs(this);
485}
486
487class _RejectRequest extends _CompilationRequest {
488 _RejectRequest(super.completer);
489
490 @override
491 Future<CompilerOutput?> _run(DefaultResidentCompiler compiler) async => compiler._reject();
492}
493
494/// Wrapper around incremental frontend server compiler, that communicates with
495/// server via stdin/stdout.
496///
497/// The wrapper is intended to stay resident in memory as user changes, reloads,
498/// restarts the Flutter app.
499abstract class ResidentCompiler {
500 factory ResidentCompiler(
501 String sdkRoot, {
502 required BuildMode buildMode,
503 required Logger logger,
504 required ProcessManager processManager,
505 required Artifacts artifacts,
506 required Platform platform,
507 required FileSystem fileSystem,
508 bool testCompilation,
509 bool trackWidgetCreation,
510 String packagesPath,
511 List<String> fileSystemRoots,
512 String? fileSystemScheme,
513 String initializeFromDill,
514 bool assumeInitializeFromDillUpToDate,
515 TargetModel targetModel,
516 bool unsafePackageSerialization,
517 String? frontendServerStarterPath,
518 List<String> extraFrontEndOptions,
519 String platformDill,
520 List<String>? dartDefines,
521 String librariesSpec,
522 }) = DefaultResidentCompiler;
523
524 // TODO(zanderso): find a better way to configure additional file system
525 // roots from the runner.
526 // See: https://github.com/flutter/flutter/issues/50494
527 void addFileSystemRoot(String root);
528
529 /// If invoked for the first time, it compiles the Dart script identified by
530 /// [mainUri], [invalidatedFiles] list is ignored.
531 /// On successive runs [invalidatedFiles] indicates which files need to be
532 /// recompiled.
533 ///
534 /// Binary file name is returned if compilation was successful, otherwise
535 /// null is returned.
536 ///
537 /// If [checkDartPluginRegistry] is true, it is the caller's responsibility
538 /// to ensure that the generated registrant file has been updated such that
539 /// it is wrapping [mainUri].
540 ///
541 /// If [recompileRestart] is true, uses the `recompile-restart` instruction
542 /// intended for a hot restart instead.
543 Future<CompilerOutput?> recompile(
544 Uri mainUri,
545 List<Uri>? invalidatedFiles, {
546 required String outputPath,
547 required PackageConfig packageConfig,
548 required FileSystem fs,
549 String? projectRootPath,
550 bool suppressErrors = false,
551 bool checkDartPluginRegistry = false,
552 File? dartPluginRegistrant,
553 Uri? nativeAssetsYaml,
554 bool recompileRestart = false,
555 });
556
557 Future<CompilerOutput?> compileExpression(
558 String expression,
559 List<String>? definitions,
560 List<String>? definitionTypes,
561 List<String>? typeDefinitions,
562 List<String>? typeBounds,
563 List<String>? typeDefaults,
564 String? libraryUri,
565 String? klass,
566 String? method,
567 bool isStatic,
568 );
569
570 /// Compiles [expression] in [libraryUri] at [line]:[column] to JavaScript
571 /// in [moduleName].
572 ///
573 /// Values listed in [jsFrameValues] are substituted for their names in the
574 /// [expression].
575 ///
576 /// Ensures that all [jsModules] are loaded and accessible inside the
577 /// expression.
578 ///
579 /// Example values of parameters:
580 /// [moduleName] is of the form '/packages/hello_world_main.dart'
581 /// [jsFrameValues] is a map from js variable name to its primitive value
582 /// or another variable name, for example
583 /// { 'x': '1', 'y': 'y', 'o': 'null' }
584 /// [jsModules] is a map from variable name to the module name, where
585 /// variable name is the name originally used in JavaScript to contain the
586 /// module object, for example:
587 /// { 'dart':'dart_sdk', 'main': '/packages/hello_world_main.dart' }
588 /// Returns a [CompilerOutput] including the name of the file containing the
589 /// compilation result and a number of errors.
590 Future<CompilerOutput?> compileExpressionToJs(
591 String libraryUri,
592 int line,
593 int column,
594 Map<String, String> jsModules,
595 Map<String, String> jsFrameValues,
596 String moduleName,
597 String expression,
598 );
599
600 /// Should be invoked when results of compilation are accepted by the client.
601 ///
602 /// Either [accept] or [reject] should be called after every [recompile] call.
603 void accept();
604
605 /// Should be invoked when results of compilation are rejected by the client.
606 ///
607 /// Either [accept] or [reject] should be called after every [recompile] call.
608 Future<CompilerOutput?> reject();
609
610 /// Should be invoked when frontend server compiler should forget what was
611 /// accepted previously so that next call to [recompile] produces complete
612 /// kernel file.
613 void reset();
614
615 Future<Object> shutdown();
616}
617
618@visibleForTesting
619class DefaultResidentCompiler implements ResidentCompiler {
620 DefaultResidentCompiler(
621 String sdkRoot, {
622 required this.buildMode,
623 required Logger logger,
624 required ProcessManager processManager,
625 required this.artifacts,
626 required Platform platform,
627 required FileSystem fileSystem,
628 this.testCompilation = false,
629 this.trackWidgetCreation = true,
630 this.packagesPath,
631 List<String> fileSystemRoots = const <String>[],
632 this.fileSystemScheme,
633 this.initializeFromDill,
634 this.assumeInitializeFromDillUpToDate = false,
635 this.targetModel = TargetModel.flutter,
636 this.unsafePackageSerialization = false,
637 this.frontendServerStarterPath,
638 this.extraFrontEndOptions,
639 this.platformDill,
640 List<String>? dartDefines,
641 this.librariesSpec,
642 @visibleForTesting StdoutHandler? stdoutHandler,
643 }) : _logger = logger,
644 _processManager = processManager,
645 _stdoutHandler = stdoutHandler ?? StdoutHandler(logger: logger, fileSystem: fileSystem),
646 _platform = platform,
647 dartDefines = dartDefines ?? const <String>[],
648 // This is a URI, not a file path, so the forward slash is correct even on Windows.
649 sdkRoot = sdkRoot.endsWith('/') ? sdkRoot : '$sdkRoot/',
650 // Make a copy, we might need to modify it later.
651 fileSystemRoots = List<String>.from(fileSystemRoots);
652
653 final Logger _logger;
654 final ProcessManager _processManager;
655 final Artifacts artifacts;
656 final Platform _platform;
657
658 final bool testCompilation;
659 final BuildMode buildMode;
660 final bool trackWidgetCreation;
661 final String? packagesPath;
662 final TargetModel targetModel;
663 final List<String> fileSystemRoots;
664 final String? fileSystemScheme;
665 final String? initializeFromDill;
666 final bool assumeInitializeFromDillUpToDate;
667 final bool unsafePackageSerialization;
668 final String? frontendServerStarterPath;
669 final List<String>? extraFrontEndOptions;
670 final List<String> dartDefines;
671 final String? librariesSpec;
672
673 @override
674 void addFileSystemRoot(String root) {
675 fileSystemRoots.add(root);
676 }
677
678 /// The path to the root of the Dart SDK used to compile.
679 ///
680 /// This is used to resolve the [platformDill].
681 final String sdkRoot;
682
683 /// The path to the platform dill file.
684 ///
685 /// This does not need to be provided for the normal Flutter workflow.
686 final String? platformDill;
687
688 Process? _server;
689 final StdoutHandler _stdoutHandler;
690 var _compileRequestNeedsConfirmation = false;
691
692 final _controller = StreamController<_CompilationRequest>();
693
694 @override
695 Future<CompilerOutput?> recompile(
696 Uri mainUri,
697 List<Uri>? invalidatedFiles, {
698 required String outputPath,
699 required PackageConfig packageConfig,
700 bool suppressErrors = false,
701 bool checkDartPluginRegistry = false,
702 File? dartPluginRegistrant,
703 String? projectRootPath,
704 FileSystem? fs,
705 Uri? nativeAssetsYaml,
706 bool recompileRestart = false,
707 }) async {
708 if (!_controller.hasListener) {
709 _controller.stream.listen(_handleCompilationRequest);
710 }
711 Uri? additionalSourceUri;
712 // `dart_plugin_registrant.dart` contains the Dart plugin registry.
713 if (checkDartPluginRegistry &&
714 dartPluginRegistrant != null &&
715 dartPluginRegistrant.existsSync()) {
716 additionalSourceUri = dartPluginRegistrant.uri;
717 }
718 final completer = Completer<CompilerOutput?>();
719 _controller.add(
720 _RecompileRequest(
721 completer,
722 mainUri,
723 invalidatedFiles,
724 outputPath,
725 packageConfig,
726 suppressErrors,
727 additionalSourceUri: additionalSourceUri,
728 nativeAssetsYamlUri: nativeAssetsYaml,
729 recompileRestart: recompileRestart,
730 ),
731 );
732 return completer.future;
733 }
734
735 Future<CompilerOutput?> _recompile(_RecompileRequest request) async {
736 _stdoutHandler.reset();
737 _compileRequestNeedsConfirmation = true;
738 _stdoutHandler._suppressCompilerMessages = request.suppressErrors;
739
740 final String mainUri =
741 request.packageConfig.toPackageUri(request.mainUri)?.toString() ??
742 toMultiRootPath(request.mainUri, fileSystemScheme, fileSystemRoots, _platform.isWindows);
743
744 String? additionalSourceUri;
745 if (request.additionalSourceUri != null) {
746 additionalSourceUri =
747 request.packageConfig.toPackageUri(request.additionalSourceUri!)?.toString() ??
748 toMultiRootPath(
749 request.additionalSourceUri!,
750 fileSystemScheme,
751 fileSystemRoots,
752 _platform.isWindows,
753 );
754 }
755
756 final nativeAssets = request.nativeAssetsYamlUri?.toString();
757 final Process? server = _server;
758 if (server == null) {
759 return _compile(
760 mainUri,
761 request.outputPath,
762 additionalSourceUri: additionalSourceUri,
763 nativeAssetsUri: nativeAssets,
764 );
765 }
766 final String inputKey = Uuid().generateV4();
767
768 if (nativeAssets != null && nativeAssets.isNotEmpty) {
769 server.stdin.writeln('native-assets $nativeAssets');
770 _logger.printTrace('<- native-assets $nativeAssets');
771 }
772 if (request.recompileRestart) {
773 server.stdin.writeln('recompile-restart $mainUri $inputKey');
774 } else {
775 server.stdin.writeln('recompile $mainUri $inputKey');
776 }
777 _logger.printTrace('<- recompile $mainUri $inputKey');
778 final List<Uri>? invalidatedFiles = request.invalidatedFiles;
779 if (invalidatedFiles != null) {
780 for (final Uri fileUri in invalidatedFiles) {
781 String message;
782 if (fileUri.scheme == 'package') {
783 message = fileUri.toString();
784 } else {
785 message =
786 request.packageConfig.toPackageUri(fileUri)?.toString() ??
787 toMultiRootPath(fileUri, fileSystemScheme, fileSystemRoots, _platform.isWindows);
788 }
789 server.stdin.writeln(message);
790 _logger.printTrace(message);
791 }
792 }
793 server.stdin.writeln(inputKey);
794 _logger.printTrace('<- $inputKey');
795
796 return _stdoutHandler.compilerOutput?.future;
797 }
798
799 final _compilationQueue = <_CompilationRequest>[];
800
801 Future<void> _handleCompilationRequest(_CompilationRequest request) async {
802 final bool isEmpty = _compilationQueue.isEmpty;
803 _compilationQueue.add(request);
804 // Only trigger processing if queue was empty - i.e. no other requests
805 // are currently being processed. This effectively enforces "one
806 // compilation request at a time".
807 if (isEmpty) {
808 while (_compilationQueue.isNotEmpty) {
809 final _CompilationRequest request = _compilationQueue.first;
810 await request.run(this);
811 _compilationQueue.removeAt(0);
812 }
813 }
814 }
815
816 Future<CompilerOutput?> _compile(
817 String scriptUri,
818 String? outputPath, {
819 String? additionalSourceUri,
820 String? nativeAssetsUri,
821 }) async {
822 final TargetPlatform? platform = (targetModel == TargetModel.dartdevc)
823 ? TargetPlatform.web_javascript
824 : null;
825 late final List<String> commandToStartFrontendServer;
826 if (frontendServerStarterPath != null && frontendServerStarterPath!.isNotEmpty) {
827 commandToStartFrontendServer = <String>[
828 artifacts.getArtifactPath(Artifact.engineDartBinary, platform: platform),
829 frontendServerStarterPath!,
830 ];
831 } else {
832 commandToStartFrontendServer = <String>[
833 artifacts.getArtifactPath(Artifact.engineDartAotRuntime, platform: platform),
834 artifacts.getArtifactPath(
835 Artifact.frontendServerSnapshotForEngineDartSdk,
836 platform: platform,
837 ),
838 ];
839 }
840
841 final List<String> command =
842 commandToStartFrontendServer +
843 <String>[
844 '--sdk-root',
845 sdkRoot,
846 '--incremental',
847 if (testCompilation) '--no-print-incremental-dependencies',
848 '--target=$targetModel',
849 // TODO(annagrin): remove once this becomes the default behavior
850 // in the frontend_server.
851 // https://github.com/flutter/flutter/issues/59902
852 '--experimental-emit-debug-metadata',
853 for (final Object dartDefine in dartDefines) '-D$dartDefine',
854 if (outputPath != null) ...<String>['--output-dill', outputPath],
855 // If we have a platform dill, we don't need to pass the libraries spec,
856 // since the information is embedded in the .dill file.
857 if (librariesSpec != null && platformDill == null) ...<String>[
858 '--libraries-spec',
859 librariesSpec!,
860 ],
861 if (packagesPath != null) ...<String>['--packages', packagesPath!],
862 ...buildModeOptions(buildMode, dartDefines),
863 if (trackWidgetCreation) '--track-widget-creation',
864 for (final String root in fileSystemRoots) ...<String>['--filesystem-root', root],
865 if (fileSystemScheme != null) ...<String>['--filesystem-scheme', fileSystemScheme!],
866 if (initializeFromDill != null) ...<String>[
867 '--initialize-from-dill',
868 initializeFromDill!,
869 ],
870 if (assumeInitializeFromDillUpToDate) '--assume-initialize-from-dill-up-to-date',
871 if (additionalSourceUri != null) ...<String>[
872 '--source',
873 additionalSourceUri,
874 '--source',
875 'package:flutter/src/dart_plugin_registrant.dart',
876 '-Dflutter.dart_plugin_registrant=$additionalSourceUri',
877 ],
878 if (nativeAssetsUri != null) ...<String>['--native-assets', nativeAssetsUri],
879 if (platformDill != null) ...<String>['--platform', platformDill!],
880 if (unsafePackageSerialization) '--unsafe-package-serialization',
881 // See: https://github.com/flutter/flutter/issues/103994
882 '--verbosity=error',
883 ...?extraFrontEndOptions,
884 ];
885 _logger.printTrace(command.join(' '));
886 _server = await _processManager.start(command);
887 _server?.stdout
888 .transform<String>(utf8.decoder)
889 .transform<String>(const LineSplitter())
890 .listen(
891 _stdoutHandler.handler,
892 onDone: () {
893 // when outputFilename future is not completed, but stdout is closed
894 // process has died unexpectedly.
895 if (_stdoutHandler.compilerOutput?.isCompleted == false) {
896 _stdoutHandler.compilerOutput?.complete();
897 throwToolExit('The Dart compiler exited unexpectedly.');
898 }
899 },
900 );
901
902 _server?.stderr
903 .transform<String>(utf8.decoder)
904 .transform<String>(const LineSplitter())
905 .listen(_logger.printError);
906
907 unawaited(
908 _server?.exitCode.then((int code) {
909 if (code != 0) {
910 throwToolExit('The Dart compiler exited unexpectedly.');
911 }
912 }),
913 );
914
915 if (nativeAssetsUri != null && nativeAssetsUri.isNotEmpty) {
916 _server?.stdin.writeln('native-assets $nativeAssetsUri');
917 _logger.printTrace('<- native-assets $nativeAssetsUri');
918 }
919
920 _server?.stdin.writeln('compile $scriptUri');
921 _logger.printTrace('<- compile $scriptUri');
922
923 return _stdoutHandler.compilerOutput?.future;
924 }
925
926 @override
927 Future<CompilerOutput?> compileExpression(
928 String expression,
929 List<String>? definitions,
930 List<String>? definitionTypes,
931 List<String>? typeDefinitions,
932 List<String>? typeBounds,
933 List<String>? typeDefaults,
934 String? libraryUri,
935 String? klass,
936 String? method,
937 bool isStatic,
938 ) async {
939 if (!_controller.hasListener) {
940 _controller.stream.listen(_handleCompilationRequest);
941 }
942
943 final completer = Completer<CompilerOutput?>();
944 final request = _CompileExpressionRequest(
945 completer,
946 expression,
947 definitions,
948 definitionTypes,
949 typeDefinitions,
950 typeBounds,
951 typeDefaults,
952 libraryUri,
953 klass,
954 method,
955 isStatic,
956 );
957 _controller.add(request);
958 return completer.future;
959 }
960
961 Future<CompilerOutput?> _compileExpression(_CompileExpressionRequest request) async {
962 _stdoutHandler.reset(suppressCompilerMessages: true, expectSources: false, readFile: true);
963
964 // 'compile-expression' should be invoked after compiler has been started,
965 // program was compiled.
966 final Process? server = _server;
967 if (server == null) {
968 return null;
969 }
970
971 final String inputKey = Uuid().generateV4();
972 server.stdin
973 ..writeln('compile-expression $inputKey')
974 ..writeln(request.expression);
975 request.definitions?.forEach(server.stdin.writeln);
976 server.stdin.writeln(inputKey);
977 request.definitionTypes?.forEach(server.stdin.writeln);
978 server.stdin.writeln(inputKey);
979 request.typeDefinitions?.forEach(server.stdin.writeln);
980 server.stdin.writeln(inputKey);
981 request.typeBounds?.forEach(server.stdin.writeln);
982 server.stdin.writeln(inputKey);
983 request.typeDefaults?.forEach(server.stdin.writeln);
984 server.stdin
985 ..writeln(inputKey)
986 ..writeln(request.libraryUri ?? '')
987 ..writeln(request.klass ?? '')
988 ..writeln(request.method ?? '')
989 ..writeln(request.isStatic);
990
991 return _stdoutHandler.compilerOutput?.future;
992 }
993
994 @override
995 Future<CompilerOutput?> compileExpressionToJs(
996 String libraryUri,
997 int line,
998 int column,
999 Map<String, String> jsModules,
1000 Map<String, String> jsFrameValues,
1001 String moduleName,
1002 String expression,
1003 ) {
1004 if (!_controller.hasListener) {
1005 _controller.stream.listen(_handleCompilationRequest);
1006 }
1007
1008 final completer = Completer<CompilerOutput?>();
1009 _controller.add(
1010 _CompileExpressionToJsRequest(
1011 completer,
1012 libraryUri,
1013 line,
1014 column,
1015 jsModules,
1016 jsFrameValues,
1017 moduleName,
1018 expression,
1019 ),
1020 );
1021 return completer.future;
1022 }
1023
1024 Future<CompilerOutput?> _compileExpressionToJs(_CompileExpressionToJsRequest request) async {
1025 _stdoutHandler.reset(suppressCompilerMessages: true, expectSources: false);
1026
1027 // 'compile-expression-to-js' should be invoked after compiler has been started,
1028 // program was compiled.
1029 final Process? server = _server;
1030 if (server == null) {
1031 return null;
1032 }
1033
1034 final String inputKey = Uuid().generateV4();
1035 server.stdin
1036 ..writeln('compile-expression-to-js $inputKey')
1037 ..writeln(request.libraryUri ?? '')
1038 ..writeln(request.line)
1039 ..writeln(request.column);
1040 request.jsModules?.forEach((String k, String v) {
1041 server.stdin.writeln('$k:$v');
1042 });
1043 server.stdin.writeln(inputKey);
1044 request.jsFrameValues?.forEach((String k, String v) {
1045 server.stdin.writeln('$k:$v');
1046 });
1047 server.stdin
1048 ..writeln(inputKey)
1049 ..writeln(request.moduleName ?? '')
1050 ..writeln(request.expression ?? '');
1051
1052 return _stdoutHandler.compilerOutput?.future;
1053 }
1054
1055 @override
1056 void accept() {
1057 if (_compileRequestNeedsConfirmation) {
1058 _server?.stdin.writeln('accept');
1059 _logger.printTrace('<- accept');
1060 }
1061 _compileRequestNeedsConfirmation = false;
1062 }
1063
1064 @override
1065 Future<CompilerOutput?> reject() {
1066 if (!_controller.hasListener) {
1067 _controller.stream.listen(_handleCompilationRequest);
1068 }
1069
1070 final completer = Completer<CompilerOutput?>();
1071 _controller.add(_RejectRequest(completer));
1072 return completer.future;
1073 }
1074
1075 Future<CompilerOutput?> _reject() async {
1076 if (!_compileRequestNeedsConfirmation) {
1077 return Future<CompilerOutput?>.value();
1078 }
1079 _stdoutHandler.reset(expectSources: false);
1080 _server?.stdin.writeln('reject');
1081 _logger.printTrace('<- reject');
1082 _compileRequestNeedsConfirmation = false;
1083 return _stdoutHandler.compilerOutput?.future;
1084 }
1085
1086 @override
1087 void reset() {
1088 _server?.stdin.writeln('reset');
1089 _logger.printTrace('<- reset');
1090 }
1091
1092 @override
1093 Future<Object> shutdown() async {
1094 // Server was never successfully created.
1095 final Process? server = _server;
1096 if (server == null) {
1097 return 0;
1098 }
1099 _logger.printTrace('killing pid ${server.pid}');
1100 server.kill();
1101 return server.exitCode;
1102 }
1103}
1104
1105/// Convert a file URI into a multi-root scheme URI if provided, otherwise
1106/// return unmodified.
1107@visibleForTesting
1108String toMultiRootPath(Uri fileUri, String? scheme, List<String> fileSystemRoots, bool windows) {
1109 if (scheme == null || fileSystemRoots.isEmpty || fileUri.scheme != 'file') {
1110 return fileUri.toString();
1111 }
1112 final String filePath = fileUri.toFilePath(windows: windows);
1113 for (final fileSystemRoot in fileSystemRoots) {
1114 if (filePath.startsWith(fileSystemRoot)) {
1115 return '$scheme://${filePath.substring(fileSystemRoot.length)}';
1116 }
1117 }
1118 return fileUri.toString();
1119}
1120