compile.dart 15.6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
// Copyright 2017 The Chromium 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:convert';

import 'package:usage/uuid/uuid.dart';

import 'artifacts.dart';
11
import 'base/common.dart';
12
import 'base/context.dart';
13
import 'base/fingerprint.dart';
14 15 16 17
import 'base/io.dart';
import 'base/process_manager.dart';
import 'globals.dart';

18 19
KernelCompiler get kernelCompiler => context[KernelCompiler];

20
typedef void CompilerMessageConsumer(String message);
21

22
class CompilerOutput {
23 24
  final String outputFilename;
  final int errorCount;
25

26
  const CompilerOutput(this.outputFilename, this.errorCount);
27 28
}

29
class _StdoutHandler {
30
  _StdoutHandler({this.consumer = printError}) {
31 32 33
    reset();
  }

34
  final CompilerMessageConsumer consumer;
35
  String boundaryKey;
36
  Completer<CompilerOutput> compilerOutput;
37

38 39
  bool _suppressCompilerMessages;

40 41 42 43 44
  void handler(String string) {
    const String kResultPrefix = 'result ';
    if (boundaryKey == null) {
      if (string.startsWith(kResultPrefix))
        boundaryKey = string.substring(kResultPrefix.length);
45 46 47 48 49 50 51 52 53 54 55
    } else if (string.startsWith(boundaryKey)) {
      if (string.length <= boundaryKey.length) {
        compilerOutput.complete(null);
        return;
      }
      final int spaceDelimiter = string.lastIndexOf(' ');
      compilerOutput.complete(
        new CompilerOutput(
          string.substring(boundaryKey.length + 1, spaceDelimiter),
          int.parse(string.substring(spaceDelimiter + 1).trim())));
    }
56
    else if (!_suppressCompilerMessages) {
57
      consumer('compiler message: $string');
58
    }
59
  }
60 61 62

  // This is needed to get ready to process next compilation result output,
  // with its own boundary key and new completer.
63
  void reset({bool suppressCompilerMessages = false}) {
64
    boundaryKey = null;
65
    compilerOutput = new Completer<CompilerOutput>();
66
    _suppressCompilerMessages = suppressCompilerMessages;
67
  }
68 69
}

70 71 72
class KernelCompiler {
  const KernelCompiler();

73 74
  Future<CompilerOutput> compile({
    String sdkRoot,
75
    String mainPath,
76
    String outputFilePath,
77
    String depFilePath,
78 79
    bool linkPlatformKernelIn = false,
    bool aot = false,
80
    List<String> entryPointsJsonFiles,
81
    bool trackWidgetCreation = false,
82
    List<String> extraFrontEndOptions,
83
    String incrementalCompilerByteStorePath,
84 85
    String packagesPath,
    List<String> fileSystemRoots,
86
    String fileSystemScheme,
87
    bool targetProductVm = false,
88
  }) async {
89 90 91 92
    final String frontendServer = artifacts.getArtifactPath(
      Artifact.frontendServerSnapshotForEngineDartSdk
    );

93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
    // TODO(cbracken) eliminate pathFilter.
    // Currently the compiler emits buildbot paths for the core libs in the
    // depfile. None of these are available on the local host.
    Fingerprinter fingerprinter;
    if (depFilePath != null) {
      fingerprinter = new Fingerprinter(
        fingerprintPath: '$depFilePath.fingerprint',
        paths: <String>[mainPath],
        properties: <String, String>{
          'entryPoint': mainPath,
          'trackWidgetCreation': trackWidgetCreation.toString(),
        },
        depfilePaths: <String>[depFilePath],
        pathFilter: (String path) => !path.startsWith('/b/build/slave/'),
      );

      if (await fingerprinter.doesFingerprintMatch()) {
        printTrace('Skipping kernel compilation. Fingerprint match.');
        return new CompilerOutput(outputFilePath, 0);
      }
    }

115 116 117 118 119 120
    // 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);
    if (!processManager.canRun(engineDartPath)) {
      throwToolExit('Unable to find Dart binary at $engineDartPath');
121
    }
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
    final List<String> command = <String>[
      engineDartPath,
      frontendServer,
      '--sdk-root',
      sdkRoot,
      '--strong',
      '--target=flutter',
    ];
    if (trackWidgetCreation)
      command.add('--track-widget-creation');
    if (!linkPlatformKernelIn)
      command.add('--no-link-platform');
    if (aot) {
      command.add('--aot');
      command.add('--tfa');
    }
138 139 140
    if (targetProductVm) {
      command.add('-Ddart.vm.product=true');
    }
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
    if (entryPointsJsonFiles != null) {
      for (String entryPointsJson in entryPointsJsonFiles) {
        command.addAll(<String>['--entry-points', entryPointsJson]);
      }
    }
    if (incrementalCompilerByteStorePath != null) {
      command.add('--incremental');
    }
    if (packagesPath != null) {
      command.addAll(<String>['--packages', packagesPath]);
    }
    if (outputFilePath != null) {
      command.addAll(<String>['--output-dill', outputFilePath]);
    }
    if (depFilePath != null && (fileSystemRoots == null || fileSystemRoots.isEmpty)) {
      command.addAll(<String>['--depfile', depFilePath]);
    }
    if (fileSystemRoots != null) {
      for (String root in fileSystemRoots) {
        command.addAll(<String>['--filesystem-root', root]);
      }
    }
    if (fileSystemScheme != null) {
      command.addAll(<String>['--filesystem-scheme', fileSystemScheme]);
165
    }
166

167 168 169 170 171 172 173 174 175 176
    if (extraFrontEndOptions != null)
      command.addAll(extraFrontEndOptions);
    command.add(mainPath);
    printTrace(command.join(' '));
    final Process server = await processManager
        .start(command)
        .catchError((dynamic error, StackTrace stack) {
      printError('Failed to start frontend server $error, $stack');
    });

177
    final _StdoutHandler _stdoutHandler = new _StdoutHandler();
178 179 180 181 182 183 184

    server.stderr
      .transform(utf8.decoder)
      .listen((String s) { printError('compiler message: $s'); });
    server.stdout
      .transform(utf8.decoder)
      .transform(const LineSplitter())
185
      .listen(_stdoutHandler.handler);
186
    final int exitCode = await server.exitCode;
187 188 189 190
    if (exitCode == 0) {
      if (fingerprinter != null) {
        await fingerprinter.writeFingerprint();
      }
191
      return _stdoutHandler.compilerOutput.future;
192 193
    }
    return null;
194
  }
195 196
}

197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
/// Class that allows to serialize compilation requests to the compiler.
abstract class _CompilationRequest {
  Completer<CompilerOutput> completer;

  _CompilationRequest(this.completer);

  Future<CompilerOutput> _run(ResidentCompiler compiler);

  Future<void> run(ResidentCompiler compiler) async {
    completer.complete(await _run(compiler));
  }
}

class _RecompileRequest extends _CompilationRequest {
  _RecompileRequest(Completer<CompilerOutput> completer, this.mainPath,
      this.invalidatedFiles, this.outputPath, this.packagesFilePath) :
      super(completer);

  String mainPath;
  List<String> invalidatedFiles;
  String outputPath;
  String packagesFilePath;

  @override
  Future<CompilerOutput> _run(ResidentCompiler compiler) async =>
      compiler._recompile(this);
}

class _CompileExpressionRequest extends _CompilationRequest {
  _CompileExpressionRequest(Completer<CompilerOutput> completer, this.expression, this.definitions,
      this.typeDefinitions, this.libraryUri, this.klass, this.isStatic) :
      super(completer);

  String expression;
  List<String> definitions;
  List<String> typeDefinitions;
  String libraryUri;
  String klass;
  bool isStatic;

  @override
  Future<CompilerOutput> _run(ResidentCompiler compiler) async =>
      compiler._compileExpression(this);
}

242 243 244 245 246 247
/// 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.
class ResidentCompiler {
248
  ResidentCompiler(this._sdkRoot, {bool trackWidgetCreation = false,
249
      String packagesPath, List<String> fileSystemRoots, String fileSystemScheme ,
250
      CompilerMessageConsumer compilerMessageConsumer = printError})
251
    : assert(_sdkRoot != null),
252
      _trackWidgetCreation = trackWidgetCreation,
253
      _packagesPath = packagesPath,
254 255
      _fileSystemRoots = fileSystemRoots,
      _fileSystemScheme = fileSystemScheme,
256 257
      _stdoutHandler = new _StdoutHandler(consumer: compilerMessageConsumer),
      _controller = new StreamController<_CompilationRequest>() {
258 259 260 261 262
    // This is a URI, not a file path, so the forward slash is correct even on Windows.
    if (!_sdkRoot.endsWith('/'))
      _sdkRoot = '$_sdkRoot/';
  }

263
  final bool _trackWidgetCreation;
264
  final String _packagesPath;
265 266
  final List<String> _fileSystemRoots;
  final String _fileSystemScheme;
267 268
  String _sdkRoot;
  Process _server;
269
  final _StdoutHandler _stdoutHandler;
270

271 272
  final StreamController<_CompilationRequest> _controller;

273 274
  /// If invoked for the first time, it compiles Dart script identified by
  /// [mainPath], [invalidatedFiles] list is ignored.
275 276 277
  /// 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.
278
  /// Binary file name is returned if compilation was successful, otherwise
279
  /// null is returned.
280
  Future<CompilerOutput> recompile(String mainPath, List<String> invalidatedFiles,
281
      {String outputPath, String packagesFilePath}) async {
282 283 284 285 286 287 288 289 290 291 292 293
    if (!_controller.hasListener) {
      _controller.stream.listen(_handleCompilationRequest);
    }

    final Completer<CompilerOutput> completer = new Completer<CompilerOutput>();
    _controller.add(
        new _RecompileRequest(completer, mainPath, invalidatedFiles, outputPath, packagesFilePath)
    );
    return completer.future;
  }

  Future<CompilerOutput> _recompile(_RecompileRequest request) async {
294
    _stdoutHandler.reset();
295

296 297
    // First time recompile is called we actually have to compile the app from
    // scratch ignoring list of invalidated files.
298 299 300 301
    if (_server == null) {
      return _compile(_mapFilename(request.mainPath),
          request.outputPath, _mapFilename(request.packagesFilePath));
    }
302 303

    final String inputKey = new Uuid().generateV4();
304 305
    _server.stdin.writeln('recompile ${request.mainPath != null ? _mapFilename(request.mainPath) + " ": ""}$inputKey');
    for (String fileUri in request.invalidatedFiles) {
306
      _server.stdin.writeln(_mapFileUri(fileUri));
307
    }
308 309
    _server.stdin.writeln(inputKey);

310
    return _stdoutHandler.compilerOutput.future;
311 312
  }

313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329
  final List<_CompilationRequest> compilationQueue = <_CompilationRequest>[];

  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);
      }
    }
  }

330 331
  Future<CompilerOutput> _compile(String scriptFilename, String outputPath,
      String packagesFilePath) async {
332 333 334
    final String frontendServer = artifacts.getArtifactPath(
      Artifact.frontendServerSnapshotForEngineDartSdk
    );
335
    final List<String> command = <String>[
336
      artifacts.getArtifactPath(Artifact.engineDartBinary),
337 338 339
      frontendServer,
      '--sdk-root',
      _sdkRoot,
340
      '--incremental',
341 342
      '--strong',
      '--target=flutter',
343
      '--initialize-from-dill=foo' // TODO(aam): remove once dartbug.com/33087 fixed
344
    ];
345
    if (outputPath != null) {
346
      command.addAll(<String>['--output-dill', outputPath]);
347
    }
348
    if (packagesFilePath != null) {
349
      command.addAll(<String>['--packages', packagesFilePath]);
350
    }
351
    if (_trackWidgetCreation) {
352
      command.add('--track-widget-creation');
353
    }
354
    if (_packagesPath != null) {
355
      command.addAll(<String>['--packages', _packagesPath]);
356
    }
357 358
    if (_fileSystemRoots != null) {
      for (String root in _fileSystemRoots) {
359
        command.addAll(<String>['--filesystem-root', root]);
360 361 362
      }
    }
    if (_fileSystemScheme != null) {
363
      command.addAll(<String>['--filesystem-scheme', _fileSystemScheme]);
364
    }
365 366
    printTrace(command.join(' '));
    _server = await processManager.start(command);
367
    _server.stdout
368
      .transform(utf8.decoder)
369
      .transform(const LineSplitter())
370
      .listen(
371
        _stdoutHandler.handler,
372 373 374
        onDone: () {
          // when outputFilename future is not completed, but stdout is closed
          // process has died unexpectedly.
375 376
          if (!_stdoutHandler.compilerOutput.isCompleted) {
            _stdoutHandler.compilerOutput.complete(null);
377 378 379
          }
        });

380
    _server.stderr
381
      .transform(utf8.decoder)
382
      .transform(const LineSplitter())
383
      .listen((String s) { printError('compiler message: $s'); });
384 385 386

    _server.stdin.writeln('compile $scriptFilename');

387
    return _stdoutHandler.compilerOutput.future;
388 389
  }

390 391
  Future<CompilerOutput> compileExpression(String expression, List<String> definitions,
      List<String> typeDefinitions, String libraryUri, String klass, bool isStatic) {
392 393 394 395 396 397 398 399 400 401 402 403 404 405
    if (!_controller.hasListener) {
      _controller.stream.listen(_handleCompilationRequest);
    }

    final Completer<CompilerOutput> completer = new Completer<CompilerOutput>();
    _controller.add(
        new _CompileExpressionRequest(
            completer, expression, definitions, typeDefinitions, libraryUri, klass, isStatic)
    );
    return completer.future;
  }

  Future<CompilerOutput> _compileExpression(
      _CompileExpressionRequest request) async {
406
    _stdoutHandler.reset(suppressCompilerMessages: true);
407 408 409 410 411 412 413 414

    // 'compile-expression' should be invoked after compiler has been started,
    // program was compiled.
    if (_server == null)
      return null;

    final String inputKey = new Uuid().generateV4();
    _server.stdin.writeln('compile-expression $inputKey');
415 416
    _server.stdin.writeln(request.expression);
    request.definitions?.forEach(_server.stdin.writeln);
417
    _server.stdin.writeln(inputKey);
418
    request.typeDefinitions?.forEach(_server.stdin.writeln);
419
    _server.stdin.writeln(inputKey);
420 421 422
    _server.stdin.writeln(request.libraryUri ?? '');
    _server.stdin.writeln(request.klass ?? '');
    _server.stdin.writeln(request.isStatic ?? false);
423

424
    return _stdoutHandler.compilerOutput.future;
425
  }
426 427 428 429 430 431 432 433 434 435 436 437 438 439

  /// 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() {
    _server.stdin.writeln('accept');
  }

  /// Should be invoked when results of compilation are rejected by the client.
  ///
  /// Either [accept] or [reject] should be called after every [recompile] call.
  void reject() {
    _server.stdin.writeln('reject');
  }
440 441 442 443 444 445 446

  /// 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() {
    _server.stdin.writeln('reset');
  }
447

448 449 450 451 452 453 454 455 456 457 458 459 460
  String _mapFilename(String filename) {
    if (_fileSystemRoots != null) {
      for (String root in _fileSystemRoots) {
        if (filename.startsWith(root)) {
          return new Uri(
              scheme: _fileSystemScheme, path: filename.substring(root.length))
              .toString();
        }
      }
    }
    return filename;
  }

461 462 463 464 465 466 467 468 469 470 471 472 473 474
  String _mapFileUri(String fileUri) {
    if (_fileSystemRoots != null) {
      final String filename = Uri.parse(fileUri).toFilePath();
      for (String root in _fileSystemRoots) {
        if (filename.startsWith(root)) {
          return new Uri(
              scheme: _fileSystemScheme, path: filename.substring(root.length))
              .toString();
        }
      }
    }
    return fileUri;
  }

475 476 477 478
  Future<dynamic> shutdown() {
    _server.kill();
    return _server.exitCode;
  }
479
}