compile.dart 32.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6
import 'dart:typed_data';
7

8
import 'package:meta/meta.dart';
9
import 'package:package_config/package_config.dart';
10
import 'package:process/process.dart';
11 12 13
import 'package:usage/uuid/uuid.dart';

import 'artifacts.dart';
14
import 'base/common.dart';
15
import 'base/file_system.dart';
16
import 'base/io.dart';
17
import 'base/logger.dart';
18
import 'base/platform.dart';
19
import 'build_info.dart';
20
import 'convert.dart';
21

22 23 24 25
/// Opt-in changes to the dart compilers.
const List<String> kDartCompilerExperiments = <String>[
];

Chris Bracken's avatar
Chris Bracken committed
26
/// The target model describes the set of core libraries that are available within
27 28 29 30
/// the SDK.
class TargetModel {
  /// Parse a [TargetModel] from a raw string.
  ///
31 32
  /// Throws an exception if passed a value other than 'flutter',
  /// 'flutter_runner', 'vm', or 'dartdevc'.
33 34 35 36 37 38
  factory TargetModel(String rawValue) {
    switch (rawValue) {
      case 'flutter':
        return flutter;
      case 'flutter_runner':
        return flutterRunner;
39 40
      case 'vm':
        return vm;
41 42
      case 'dartdevc':
        return dartdevc;
43
    }
44
    throw Exception('Unexpected target model $rawValue');
45 46 47 48
  }

  const TargetModel._(this._value);

49
  /// The Flutter patched Dart SDK.
50 51
  static const TargetModel flutter = TargetModel._('flutter');

52
  /// The Fuchsia patched SDK.
53 54
  static const TargetModel flutterRunner = TargetModel._('flutter_runner');

55
  /// The Dart VM.
56 57
  static const TargetModel vm = TargetModel._('vm');

58 59 60
  /// The development compiler for JavaScript.
  static const TargetModel dartdevc = TargetModel._('dartdevc');

61 62 63 64 65 66
  final String _value;

  @override
  String toString() => _value;
}

67
class CompilerOutput {
68
  const CompilerOutput(this.outputFilename, this.errorCount, this.sources, {this.expressionData});
69

70 71
  final String outputFilename;
  final int errorCount;
72
  final List<Uri> sources;
73 74

  /// This field is only non-null for expression compilation requests.
75
  final Uint8List? expressionData;
76 77
}

78 79
enum StdoutState { CollectDiagnostic, CollectDependencies }

80 81
/// Handles stdin/stdout communication with the frontend server.
class StdoutHandler {
82
  StdoutHandler({
83 84
    required Logger logger,
    required FileSystem fileSystem,
85 86
  }) : _logger = logger,
       _fileSystem = fileSystem {
87 88 89
    reset();
  }

90
  final Logger _logger;
91
  final FileSystem _fileSystem;
92

93
  String? boundaryKey;
94
  StdoutState state = StdoutState.CollectDiagnostic;
95
  Completer<CompilerOutput?>? compilerOutput;
96
  final List<Uri> sources = <Uri>[];
97

98 99 100
  bool _suppressCompilerMessages = false;
  bool _expectSources = true;
  bool _readFile = false;
101

102
  void handler(String message) {
103
    const String kResultPrefix = 'result ';
104
    if (boundaryKey == null && message.startsWith(kResultPrefix)) {
105
      boundaryKey = message.substring(kResultPrefix.length);
106 107
      return;
    }
108 109
    final String? messageBoundaryKey = boundaryKey;
    if (messageBoundaryKey != null && message.startsWith(messageBoundaryKey)) {
110 111 112 113 114 115
      if (_expectSources) {
        if (state == StdoutState.CollectDiagnostic) {
          state = StdoutState.CollectDependencies;
          return;
        }
      }
116 117
      if (message.length <= messageBoundaryKey.length) {
        compilerOutput?.complete(null);
118 119
        return;
      }
120
      final int spaceDelimiter = message.lastIndexOf(' ');
121
      final String fileName = message.substring(messageBoundaryKey.length + 1, spaceDelimiter);
122
      final int errorCount = int.parse(message.substring(spaceDelimiter + 1).trim());
123
      Uint8List? expressionData;
124 125 126 127 128 129 130 131 132
      if (_readFile) {
        expressionData = _fileSystem.file(fileName).readAsBytesSync();
      }
      final CompilerOutput output = CompilerOutput(
        fileName,
        errorCount,
        sources,
        expressionData: expressionData,
      );
133
      compilerOutput?.complete(output);
134 135 136 137
      return;
    }
    if (state == StdoutState.CollectDiagnostic) {
      if (!_suppressCompilerMessages) {
138 139 140
        _logger.printError(message);
      } else {
        _logger.printTrace(message);
141 142 143 144 145 146 147 148 149
      }
    } 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:
150
          _logger.printTrace('Unexpected prefix for $message uri - ignoring');
151
      }
152
    }
153
  }
154 155 156

  // This is needed to get ready to process next compilation result output,
  // with its own boundary key and new completer.
157
  void reset({ bool suppressCompilerMessages = false, bool expectSources = true, bool readFile = false }) {
158
    boundaryKey = null;
159
    compilerOutput = Completer<CompilerOutput?>();
160
    _suppressCompilerMessages = suppressCompilerMessages;
161
    _expectSources = expectSources;
162
    _readFile = readFile;
163
    state = StdoutState.CollectDiagnostic;
164
  }
165 166
}

167
/// List the preconfigured build options for a given build mode.
168
List<String> buildModeOptions(BuildMode mode, List<String> dartDefines) {
169 170 171
  switch (mode) {
    case BuildMode.debug:
      return <String>[
172
        // These checks allow the CLI to override the value of this define for unit
173
        // testing the framework.
174 175
        if (!dartDefines.any((String define) => define.startsWith('dart.vm.profile')))
          '-Ddart.vm.profile=false',
176 177
        if (!dartDefines.any((String define) => define.startsWith('dart.vm.product')))
          '-Ddart.vm.product=false',
178 179 180 181
        '--enable-asserts',
      ];
    case BuildMode.profile:
      return <String>[
182 183 184 185 186 187
        // 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',
188
        ...kDartCompilerExperiments,
189 190 191 192 193
      ];
    case BuildMode.release:
      return <String>[
        '-Ddart.vm.profile=false',
        '-Ddart.vm.product=true',
194
        ...kDartCompilerExperiments,
195 196 197 198 199
      ];
  }
  throw Exception('Unknown BuildMode: $mode');
}

200
/// A compiler interface for producing single (non-incremental) kernel files.
201
class KernelCompiler {
202
  KernelCompiler({
203 204 205 206 207
    required FileSystem fileSystem,
    required Logger logger,
    required ProcessManager processManager,
    required Artifacts artifacts,
    required List<String> fileSystemRoots,
208
    String? fileSystemScheme,
209
    @visibleForTesting StdoutHandler? stdoutHandler,
210 211 212 213 214
  }) : _logger = logger,
       _fileSystem = fileSystem,
       _artifacts = artifacts,
       _processManager = processManager,
       _fileSystemScheme = fileSystemScheme,
215
       _fileSystemRoots = fileSystemRoots,
216
       _stdoutHandler = stdoutHandler ?? StdoutHandler(logger: logger, fileSystem: fileSystem);
217 218 219 220 221

  final FileSystem _fileSystem;
  final Artifacts _artifacts;
  final ProcessManager _processManager;
  final Logger _logger;
222
  final String? _fileSystemScheme;
223
  final List<String> _fileSystemRoots;
224
  final StdoutHandler _stdoutHandler;
225

226 227 228 229 230
  Future<CompilerOutput?> compile({
    required String sdkRoot,
    String? mainPath,
    String? outputFilePath,
    String? depFilePath,
231
    TargetModel targetModel = TargetModel.flutter,
232 233
    bool linkPlatformKernelIn = false,
    bool aot = false,
234 235 236 237 238 239
    List<String>? extraFrontEndOptions,
    List<String>? fileSystemRoots,
    String? fileSystemScheme,
    String? initializeFromDill,
    String? platformDill,
    Directory? buildDir,
240
    bool checkDartPluginRegistry = false,
241 242 243 244 245
    required String? packagesPath,
    required BuildMode buildMode,
    required bool trackWidgetCreation,
    required List<String> dartDefines,
    required PackageConfig packageConfig,
246
  }) async {
247
    final TargetPlatform? platform = targetModel == TargetModel.dartdevc ? TargetPlatform.web_javascript : null;
248
    final String frontendServer = _artifacts.getArtifactPath(
249 250
      Artifact.frontendServerSnapshotForEngineDartSdk,
      platform: platform,
251 252
    );
    // This is a URI, not a file path, so the forward slash is correct even on Windows.
253
    if (!sdkRoot.endsWith('/')) {
254
      sdkRoot = '$sdkRoot/';
255
    }
256
    final String engineDartPath = _artifacts.getArtifactPath(Artifact.engineDartBinary, platform: platform);
257
    if (!_processManager.canRun(engineDartPath)) {
258
      throwToolExit('Unable to find Dart binary at $engineDartPath');
259
    }
260
    String? mainUri;
261 262
    final File mainFile = _fileSystem.file(mainPath);
    final Uri mainFileUri = mainFile.uri;
263
    if (packagesPath != null) {
264
      mainUri = packageConfig.toPackageUri(mainFileUri)?.toString();
265
    }
266
    mainUri ??= toMultiRootPath(mainFileUri, _fileSystemScheme, _fileSystemRoots, _fileSystem.path.separator == r'\');
267 268
    if (outputFilePath != null && !_fileSystem.isFileSync(outputFilePath)) {
      _fileSystem.file(outputFilePath).createSync(recursive: true);
269
    }
270 271 272 273 274 275

    // 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;
276

277 278 279 280 281 282 283
    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'\');
    }

284 285
    final List<String> command = <String>[
      engineDartPath,
286
      '--disable-dart-dev',
287 288 289
      frontendServer,
      '--sdk-root',
      sdkRoot,
290
      '--target=$targetModel',
291
      '--no-print-incremental-dependencies',
292
      for (final Object dartDefine in dartDefines)
293
        '-D$dartDefine',
294
      ...buildModeOptions(buildMode, dartDefines),
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
      if (trackWidgetCreation) '--track-widget-creation',
      if (!linkPlatformKernelIn) '--no-link-platform',
      if (aot) ...<String>[
        '--aot',
        '--tfa',
      ],
      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)
314
        for (final String root in fileSystemRoots) ...<String>[
315 316 317 318 319 320 321 322
          '--filesystem-root',
          root,
        ],
      if (fileSystemScheme != null) ...<String>[
        '--filesystem-scheme',
        fileSystemScheme,
      ],
      if (initializeFromDill != null) ...<String>[
323
        '--incremental',
324 325 326 327 328 329 330
        '--initialize-from-dill',
        initializeFromDill,
      ],
      if (platformDill != null) ...<String>[
        '--platform',
        platformDill,
      ],
331
      if (dartPluginRegistrantUri != null) ...<String>[
332
        '--source',
333
        dartPluginRegistrantUri,
334 335
        '--source',
        'package:flutter/src/dart_plugin_registrant.dart',
336
        '-Dflutter.dart_plugin_registrant=$dartPluginRegistrantUri',
337
      ],
338 339
      // See: https://github.com/flutter/flutter/issues/103994
      '--verbosity=error',
340
      ...?extraFrontEndOptions,
341
      mainUri,
342
    ];
343

344 345
    _logger.printTrace(command.join(' '));
    final Process server = await _processManager.start(command);
346 347

    server.stderr
348
      .transform<String>(utf8.decoder)
349
      .listen(_logger.printError);
350
    server.stdout
351 352
      .transform<String>(utf8.decoder)
      .transform<String>(const LineSplitter())
353
      .listen(_stdoutHandler.handler);
354
    final int exitCode = await server.exitCode;
355
    if (exitCode == 0) {
356
      return _stdoutHandler.compilerOutput?.future;
357 358
    }
    return null;
359
  }
360 361
}

362 363 364 365
/// Class that allows to serialize compilation requests to the compiler.
abstract class _CompilationRequest {
  _CompilationRequest(this.completer);

366
  Completer<CompilerOutput?> completer;
367

368
  Future<CompilerOutput?> _run(DefaultResidentCompiler compiler);
369

370
  Future<void> run(DefaultResidentCompiler compiler) async {
371 372 373 374 375
    completer.complete(await _run(compiler));
  }
}

class _RecompileRequest extends _CompilationRequest {
376
  _RecompileRequest(
377
    super.completer,
378
    this.mainUri,
379 380
    this.invalidatedFiles,
    this.outputPath,
381
    this.packageConfig,
382
    this.suppressErrors,
383
    {this.additionalSourceUri}
384
  );
385

386
  Uri mainUri;
387
  List<Uri>? invalidatedFiles;
388
  String outputPath;
389
  PackageConfig packageConfig;
390
  bool suppressErrors;
391
  final Uri? additionalSourceUri;
392 393

  @override
394
  Future<CompilerOutput?> _run(DefaultResidentCompiler compiler) async =>
395 396 397 398
      compiler._recompile(this);
}

class _CompileExpressionRequest extends _CompilationRequest {
399
  _CompileExpressionRequest(
400
    super.completer,
401 402 403 404 405
    this.expression,
    this.definitions,
    this.typeDefinitions,
    this.libraryUri,
    this.klass,
406
    this.isStatic,
407
  );
408 409

  String expression;
410 411 412 413
  List<String>? definitions;
  List<String>? typeDefinitions;
  String? libraryUri;
  String? klass;
414 415 416
  bool isStatic;

  @override
417
  Future<CompilerOutput?> _run(DefaultResidentCompiler compiler) async =>
418 419 420
      compiler._compileExpression(this);
}

421 422
class _CompileExpressionToJsRequest extends _CompilationRequest {
  _CompileExpressionToJsRequest(
423
    super.completer,
424 425 426 427 428 429 430
    this.libraryUri,
    this.line,
    this.column,
    this.jsModules,
    this.jsFrameValues,
    this.moduleName,
    this.expression,
431
  );
432

433
  final String? libraryUri;
434 435
  final int line;
  final int column;
436 437 438 439
  final Map<String, String>? jsModules;
  final Map<String, String>? jsFrameValues;
  final String? moduleName;
  final String? expression;
440 441

  @override
442
  Future<CompilerOutput?> _run(DefaultResidentCompiler compiler) async =>
443 444 445
      compiler._compileExpressionToJs(this);
}

446
class _RejectRequest extends _CompilationRequest {
447
  _RejectRequest(super.completer);
448 449

  @override
450
  Future<CompilerOutput?> _run(DefaultResidentCompiler compiler) async =>
451 452 453
      compiler._reject();
}

454 455 456 457 458
/// 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.
459 460
abstract class ResidentCompiler {
  factory ResidentCompiler(String sdkRoot, {
461 462 463 464 465 466
    required BuildMode buildMode,
    required Logger logger,
    required ProcessManager processManager,
    required Artifacts artifacts,
    required Platform platform,
    required FileSystem fileSystem,
467
    bool testCompilation,
468
    bool trackWidgetCreation,
469 470
    String packagesPath,
    List<String> fileSystemRoots,
471
    String? fileSystemScheme,
472
    String initializeFromDill,
473
    bool assumeInitializeFromDillUpToDate,
474
    TargetModel targetModel,
475
    bool unsafePackageSerialization,
476
    List<String> extraFrontEndOptions,
477
    String platformDill,
478
    List<String>? dartDefines,
479
    String librariesSpec,
480
  }) = DefaultResidentCompiler;
481

482
  // TODO(zanderso): find a better way to configure additional file system
483 484 485 486
  // roots from the runner.
  // See: https://github.com/flutter/flutter/issues/50494
  void addFileSystemRoot(String root);

487 488
  /// If invoked for the first time, it compiles Dart script identified by
  /// [mainPath], [invalidatedFiles] list is ignored.
489 490 491
  /// 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.
492
  /// Binary file name is returned if compilation was successful, otherwise
493
  /// null is returned.
494 495 496 497
  ///
  /// 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].
498
  Future<CompilerOutput?> recompile(
499
    Uri mainUri,
500 501 502 503
    List<Uri>? invalidatedFiles, {
    required String outputPath,
    required PackageConfig packageConfig,
    required FileSystem fs,
504
    String? projectRootPath,
505
    bool suppressErrors = false,
506
    bool checkDartPluginRegistry = false,
507
    File? dartPluginRegistrant,
508 509
  });

510
  Future<CompilerOutput?> compileExpression(
511
    String expression,
512 513 514 515
    List<String>? definitions,
    List<String>? typeDefinitions,
    String? libraryUri,
    String? klass,
516 517 518
    bool isStatic,
  );

519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537
  /// 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
538
  /// compilation result and a number of errors.
539
  Future<CompilerOutput?> compileExpressionToJs(
540 541 542 543 544 545 546 547 548
    String libraryUri,
    int line,
    int column,
    Map<String, String> jsModules,
    Map<String, String> jsFrameValues,
    String moduleName,
    String expression,
  );

549 550 551 552 553 554 555 556
  /// 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.
557
  Future<CompilerOutput?> reject();
558 559 560 561 562 563

  /// 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();

564
  Future<Object> shutdown();
565 566 567 568 569 570
}

@visibleForTesting
class DefaultResidentCompiler implements ResidentCompiler {
  DefaultResidentCompiler(
    String sdkRoot, {
571 572 573 574 575 576
    required this.buildMode,
    required Logger logger,
    required ProcessManager processManager,
    required Artifacts artifacts,
    required Platform platform,
    required FileSystem fileSystem,
577
    this.testCompilation = false,
578
    this.trackWidgetCreation = true,
579
    this.packagesPath,
580
    List<String> fileSystemRoots = const <String>[],
581 582
    this.fileSystemScheme,
    this.initializeFromDill,
583
    this.assumeInitializeFromDillUpToDate = false,
584
    this.targetModel = TargetModel.flutter,
585
    this.unsafePackageSerialization = false,
586
    this.extraFrontEndOptions,
587
    this.platformDill,
588
    List<String>? dartDefines,
589
    this.librariesSpec,
590
    @visibleForTesting StdoutHandler? stdoutHandler,
591
  }) : _logger = logger,
592 593
       _processManager = processManager,
       _artifacts = artifacts,
594
       _stdoutHandler = stdoutHandler ?? StdoutHandler(logger: logger, fileSystem: fileSystem),
595
       _platform = platform,
596
       dartDefines = dartDefines ?? const <String>[],
597
       // This is a URI, not a file path, so the forward slash is correct even on Windows.
598 599 600
       sdkRoot = sdkRoot.endsWith('/') ? sdkRoot : '$sdkRoot/',
       // Make a copy, we might need to modify it later.
       fileSystemRoots = List<String>.from(fileSystemRoots);
601

602 603 604
  final Logger _logger;
  final ProcessManager _processManager;
  final Artifacts _artifacts;
605
  final Platform _platform;
606

607
  final bool testCompilation;
608 609
  final BuildMode buildMode;
  final bool trackWidgetCreation;
610
  final String? packagesPath;
611 612
  final TargetModel targetModel;
  final List<String> fileSystemRoots;
613 614
  final String? fileSystemScheme;
  final String? initializeFromDill;
615
  final bool assumeInitializeFromDillUpToDate;
616
  final bool unsafePackageSerialization;
617
  final List<String>? extraFrontEndOptions;
618
  final List<String> dartDefines;
619
  final String? librariesSpec;
620

621 622 623 624 625
  @override
  void addFileSystemRoot(String root) {
    fileSystemRoots.add(root);
  }

626 627 628 629 630 631 632 633
  /// 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.
634
  final String? platformDill;
635

636
  Process? _server;
637 638 639 640 641 642
  final StdoutHandler _stdoutHandler;
  bool _compileRequestNeedsConfirmation = false;

  final StreamController<_CompilationRequest> _controller = StreamController<_CompilationRequest>();

  @override
643
  Future<CompilerOutput?> recompile(
644
    Uri mainUri,
645 646 647
    List<Uri>? invalidatedFiles, {
    required String outputPath,
    required PackageConfig packageConfig,
648
    bool suppressErrors = false,
649
    bool checkDartPluginRegistry = false,
650
    File? dartPluginRegistrant,
651 652
    String? projectRootPath,
    FileSystem? fs,
653
  }) async {
654 655 656
    if (!_controller.hasListener) {
      _controller.stream.listen(_handleCompilationRequest);
    }
657
    Uri? additionalSourceUri;
658
    // `dart_plugin_registrant.dart` contains the Dart plugin registry.
659 660
    if (checkDartPluginRegistry && dartPluginRegistrant != null && dartPluginRegistrant.existsSync()) {
      additionalSourceUri = dartPluginRegistrant.uri;
661
    }
662
    final Completer<CompilerOutput?> completer = Completer<CompilerOutput?>();
663 664 665 666 667 668 669
    _controller.add(_RecompileRequest(
      completer,
      mainUri,
      invalidatedFiles,
      outputPath,
      packageConfig,
      suppressErrors,
670
      additionalSourceUri: additionalSourceUri,
671
    ));
672 673 674
    return completer.future;
  }

675
  Future<CompilerOutput?> _recompile(_RecompileRequest request) async {
676
    _stdoutHandler.reset();
677
    _compileRequestNeedsConfirmation = true;
678
    _stdoutHandler._suppressCompilerMessages = request.suppressErrors;
679

680 681 682
    final String mainUri = request.packageConfig.toPackageUri(request.mainUri)?.toString() ??
      toMultiRootPath(request.mainUri, fileSystemScheme, fileSystemRoots, _platform.isWindows);

683 684 685 686 687 688
    String? additionalSourceUri;
    if (request.additionalSourceUri != null) {
      additionalSourceUri = request.packageConfig.toPackageUri(request.additionalSourceUri!)?.toString() ??
        toMultiRootPath(request.additionalSourceUri!, fileSystemScheme, fileSystemRoots, _platform.isWindows);
    }

689 690
    final Process? server = _server;
    if (server == null) {
691
      return _compile(mainUri, request.outputPath, additionalSourceUri: additionalSourceUri);
692
    }
693
    final String inputKey = Uuid().generateV4();
694

695
    server.stdin.writeln('recompile $mainUri $inputKey');
696
    _logger.printTrace('<- recompile $mainUri $inputKey');
697 698 699 700 701 702 703 704 705 706 707 708
    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);
709
      }
710
    }
711
    server.stdin.writeln(inputKey);
712
    _logger.printTrace('<- $inputKey');
713

714
    return _stdoutHandler.compilerOutput?.future;
715 716
  }

717
  final List<_CompilationRequest> _compilationQueue = <_CompilationRequest>[];
718

719
  Future<void> _handleCompilationRequest(_CompilationRequest request) async {
720 721
    final bool isEmpty = _compilationQueue.isEmpty;
    _compilationQueue.add(request);
722 723 724 725
    // 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) {
726 727
      while (_compilationQueue.isNotEmpty) {
        final _CompilationRequest request = _compilationQueue.first;
728
        await request.run(this);
729
        _compilationQueue.removeAt(0);
730 731 732 733
      }
    }
  }

734
  Future<CompilerOutput?> _compile(
735
    String scriptUri,
736
    String? outputPath,
737
    {String? additionalSourceUri}
738
  ) async {
739
    final TargetPlatform? platform = (targetModel == TargetModel.dartdevc) ? TargetPlatform.web_javascript : null;
740
    final String frontendServer = _artifacts.getArtifactPath(
741 742
      Artifact.frontendServerSnapshotForEngineDartSdk,
      platform: platform,
743
    );
744
    final List<String> command = <String>[
745
      _artifacts.getArtifactPath(Artifact.engineDartBinary, platform: platform),
746
      '--disable-dart-dev',
747 748
      frontendServer,
      '--sdk-root',
749
      sdkRoot,
750
      '--incremental',
751 752
      if (testCompilation)
        '--no-print-incremental-dependencies',
753
      '--target=$targetModel',
754 755 756 757
      // TODO(annagrin): remove once this becomes the default behavior
      // in the frontend_server.
      // https://github.com/flutter/flutter/issues/59902
      '--experimental-emit-debug-metadata',
758
      for (final Object dartDefine in dartDefines)
759
        '-D$dartDefine',
760 761 762 763
      if (outputPath != null) ...<String>[
        '--output-dill',
        outputPath,
      ],
764 765 766
      // 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>[
767
        '--libraries-spec',
768
        librariesSpec!,
769
      ],
770
      if (packagesPath != null) ...<String>[
771
        '--packages',
772
        packagesPath!,
773
      ],
774
      ...buildModeOptions(buildMode, dartDefines),
775
      if (trackWidgetCreation) '--track-widget-creation',
776 777 778 779
      for (final String root in fileSystemRoots) ...<String>[
        '--filesystem-root',
        root,
      ],
780
      if (fileSystemScheme != null) ...<String>[
781
        '--filesystem-scheme',
782
        fileSystemScheme!,
783
      ],
784
      if (initializeFromDill != null) ...<String>[
785
        '--initialize-from-dill',
786
        initializeFromDill!,
787
      ],
788
      if (assumeInitializeFromDillUpToDate) '--assume-initialize-from-dill-up-to-date',
789
      if (additionalSourceUri != null) ...<String>[
790
        '--source',
791
        additionalSourceUri,
792 793
        '--source',
        'package:flutter/src/dart_plugin_registrant.dart',
794
        '-Dflutter.dart_plugin_registrant=$additionalSourceUri',
795
      ],
796 797
      if (platformDill != null) ...<String>[
        '--platform',
798
        platformDill!,
799 800
      ],
      if (unsafePackageSerialization == true) '--unsafe-package-serialization',
801 802
      // See: https://github.com/flutter/flutter/issues/103994
      '--verbosity=error',
803
      ...?extraFrontEndOptions,
804
    ];
805 806
    _logger.printTrace(command.join(' '));
    _server = await _processManager.start(command);
807
    _server?.stdout
808 809
      .transform<String>(utf8.decoder)
      .transform<String>(const LineSplitter())
810
      .listen(
811
        _stdoutHandler.handler,
812 813 814
        onDone: () {
          // when outputFilename future is not completed, but stdout is closed
          // process has died unexpectedly.
815 816
          if (_stdoutHandler.compilerOutput?.isCompleted == false) {
            _stdoutHandler.compilerOutput?.complete(null);
817
            throwToolExit('the Dart compiler exited unexpectedly.');
818 819 820
          }
        });

821
    _server?.stderr
822 823
      .transform<String>(utf8.decoder)
      .transform<String>(const LineSplitter())
824
      .listen(_logger.printError);
825

826
    unawaited(_server?.exitCode.then((int code) {
827 828 829 830 831
      if (code != 0) {
        throwToolExit('the Dart compiler exited unexpectedly.');
      }
    }));

832
    _server?.stdin.writeln('compile $scriptUri');
833
    _logger.printTrace('<- compile $scriptUri');
834

835
    return _stdoutHandler.compilerOutput?.future;
836 837
  }

838
  @override
839
  Future<CompilerOutput?> compileExpression(
840
    String expression,
841 842 843 844
    List<String>? definitions,
    List<String>? typeDefinitions,
    String? libraryUri,
    String? klass,
845
    bool isStatic,
846
  ) async {
847 848 849 850
    if (!_controller.hasListener) {
      _controller.stream.listen(_handleCompilationRequest);
    }

851
    final Completer<CompilerOutput?> completer = Completer<CompilerOutput?>();
852 853 854
    final _CompileExpressionRequest request =  _CompileExpressionRequest(
        completer, expression, definitions, typeDefinitions, libraryUri, klass, isStatic);
    _controller.add(request);
855 856 857
    return completer.future;
  }

858
  Future<CompilerOutput?> _compileExpression(_CompileExpressionRequest request) async {
859
    _stdoutHandler.reset(suppressCompilerMessages: true, expectSources: false, readFile: true);
860 861 862

    // 'compile-expression' should be invoked after compiler has been started,
    // program was compiled.
863 864
    final Process? server = _server;
    if (server == null) {
865
      return null;
866
    }
867

868
    final String inputKey = Uuid().generateV4();
869
    server.stdin
870 871
      ..writeln('compile-expression $inputKey')
      ..writeln(request.expression);
872 873 874 875
    request.definitions?.forEach(server.stdin.writeln);
    server.stdin.writeln(inputKey);
    request.typeDefinitions?.forEach(server.stdin.writeln);
    server.stdin
876 877 878
      ..writeln(inputKey)
      ..writeln(request.libraryUri ?? '')
      ..writeln(request.klass ?? '')
879
      ..writeln(request.isStatic);
880

881
    return _stdoutHandler.compilerOutput?.future;
882
  }
883

884
  @override
885
  Future<CompilerOutput?> compileExpressionToJs(
886 887 888 889 890 891 892 893 894 895 896 897
    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);
    }

898
    final Completer<CompilerOutput?> completer = Completer<CompilerOutput?>();
899 900 901 902 903 904 905
    _controller.add(
        _CompileExpressionToJsRequest(
            completer, libraryUri, line, column, jsModules, jsFrameValues, moduleName, expression)
    );
    return completer.future;
  }

906
  Future<CompilerOutput?> _compileExpressionToJs(_CompileExpressionToJsRequest request) async {
907
    _stdoutHandler.reset(suppressCompilerMessages: true, expectSources: false);
908 909 910

    // 'compile-expression-to-js' should be invoked after compiler has been started,
    // program was compiled.
911 912
    final Process? server = _server;
    if (server == null) {
913 914 915 916
      return null;
    }

    final String inputKey = Uuid().generateV4();
917
    server.stdin
918 919 920 921
      ..writeln('compile-expression-to-js $inputKey')
      ..writeln(request.libraryUri ?? '')
      ..writeln(request.line)
      ..writeln(request.column);
922 923 924 925
    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
926 927 928
      ..writeln(inputKey)
      ..writeln(request.moduleName ?? '')
      ..writeln(request.expression ?? '');
929

930
    return _stdoutHandler.compilerOutput?.future;
931 932
  }

933
  @override
934
  void accept() {
935
    if (_compileRequestNeedsConfirmation) {
936
      _server?.stdin.writeln('accept');
937
      _logger.printTrace('<- accept');
938 939
    }
    _compileRequestNeedsConfirmation = false;
940 941
  }

942
  @override
943
  Future<CompilerOutput?> reject() {
944 945 946 947
    if (!_controller.hasListener) {
      _controller.stream.listen(_handleCompilationRequest);
    }

948
    final Completer<CompilerOutput?> completer = Completer<CompilerOutput?>();
949 950 951 952
    _controller.add(_RejectRequest(completer));
    return completer.future;
  }

953
  Future<CompilerOutput?> _reject() async {
954
    if (!_compileRequestNeedsConfirmation) {
955
      return Future<CompilerOutput?>.value();
956
    }
957
    _stdoutHandler.reset(expectSources: false);
958
    _server?.stdin.writeln('reject');
959
    _logger.printTrace('<- reject');
960
    _compileRequestNeedsConfirmation = false;
961
    return _stdoutHandler.compilerOutput?.future;
962
  }
963

964
  @override
965
  void reset() {
966
    _server?.stdin.writeln('reset');
967
    _logger.printTrace('<- reset');
968
  }
969

970
  @override
971
  Future<Object> shutdown() async {
Chris Bracken's avatar
Chris Bracken committed
972
    // Server was never successfully created.
973 974
    final Process? server = _server;
    if (server == null) {
975 976
      return 0;
    }
977 978 979
    _logger.printTrace('killing pid ${server.pid}');
    server.kill();
    return server.exitCode;
980
  }
981
}
982

983
/// Convert a file URI into a multi-root scheme URI if provided, otherwise
984 985
/// return unmodified.
@visibleForTesting
986
String toMultiRootPath(Uri fileUri, String? scheme, List<String> fileSystemRoots, bool windows) {
987 988 989 990 991 992
  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)) {
993
      return '$scheme://${filePath.substring(fileSystemRoot.length)}';
994 995 996 997
    }
  }
  return fileUri.toString();
}