// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. /// A library to invoke the CFE to compute kernel summary files. /// /// Used by `utils/bazel/kernel_worker.dart`. import 'dart:async'; import 'dart:io'; import 'package:_fe_analyzer_shared/src/macros/executor/isolated_executor.dart' as isolatedExecutor; import 'package:_fe_analyzer_shared/src/macros/executor/process_executor.dart' as processExecutor; import 'package:_fe_analyzer_shared/src/macros/executor/serialization.dart' show SerializationMode; import 'package:args/args.dart'; import 'package:build_integration/file_system/multi_root.dart'; import 'package:compiler/src/kernel/dart2js_target.dart'; import 'package:dev_compiler/src/kernel/target.dart'; import 'package:front_end/src/api_prototype/file_system.dart'; import 'package:front_end/src/api_prototype/incremental_kernel_generator.dart'; import 'package:front_end/src/api_unstable/bazel_worker.dart' as fe; import 'package:front_end/src/fasta/kernel/macro/macro.dart'; import 'package:kernel/ast.dart' show Component, Library, Reference; import 'package:kernel/target/targets.dart'; import 'package:vm/target/flutter.dart'; import 'package:vm/target/flutter_runner.dart'; import 'package:vm/target/vm.dart'; /// If the last arg starts with `@`, this reads the file it points to and treats /// each line as an additional arg. /// /// This is how individual work request args are differentiated from startup /// args in bazel (individual work request args go in that file). List preprocessArgs(List args) { args = new List.from(args); if (args.isEmpty) { return args; } String lastArg = args.last; if (lastArg.startsWith('@')) { File argsFile = new File(lastArg.substring(1)); try { args.removeLast(); args.addAll(argsFile.readAsLinesSync()); } on FileSystemException catch (e) { throw new Exception('Failed to read file specified by $lastArg : $e'); } } return args; } /// An [ArgParser] for generating kernel summaries. final summaryArgsParser = new ArgParser() ..addFlag('help', negatable: false, abbr: 'h') ..addFlag('exclude-non-sources', negatable: false, help: 'Whether source files loaded implicitly should be included as ' 'part of the summary.') ..addFlag('summary-only', defaultsTo: true, negatable: true, help: 'Whether to only build summary files.') ..addFlag('summary', defaultsTo: true, negatable: true, help: 'Whether or not to build summary files.') ..addOption('target', allowed: const [ 'vm', 'flutter', 'flutter_runner', 'dart2js', 'dart2js_summary', 'ddc', ], help: 'Build kernel for the vm, flutter, flutter_runner, dart2js or ddc') ..addOption('dart-sdk-summary') ..addMultiOption('redirect') ..addMultiOption('input-summary') ..addMultiOption('input-linked') ..addMultiOption('multi-root') ..addOption('multi-root-scheme', defaultsTo: 'org-dartlang-multi-root') ..addOption('libraries-file') ..addOption('packages-file') ..addMultiOption('source') ..addOption('output') ..addFlag('reuse-compiler-result', defaultsTo: false) ..addFlag('use-incremental-compiler', defaultsTo: false) ..addOption('used-inputs') ..addFlag('track-widget-creation', defaultsTo: false) ..addMultiOption('enable-experiment', help: 'Enable a language experiment when invoking the CFE.') ..addMultiOption('define', abbr: 'D') ..addFlag('verbose', defaultsTo: false) ..addFlag('sound-null-safety', defaultsTo: false) ..addOption('verbosity', defaultsTo: fe.Verbosity.defaultValue, help: 'Sets the verbosity level used for filtering messages during ' 'compilation.', allowed: fe.Verbosity.allowedValues, allowedHelp: fe.Verbosity.allowedValuesHelp) ..addMultiOption('precompiled-macro', help: 'Configuration for precompiled macro binaries or kernel files.\n' 'Must be used in combination with --precompiled-macro-format.\n' 'The expected format of this option is as follows: ' ';\nFor example: ' '--precompiled-macro="package:some_macro/some_macro.dart;' '/path/to/compiled/macro"') ..addOption('precompiled-macro-format', help: 'The format for precompiled macros.', allowed: ['aot', 'kernel'], defaultsTo: 'aot') ..addOption('macro-serialization-mode', help: 'The serialization mode for communicating with macros.', allowed: ['bytedata', 'json'], defaultsTo: 'bytedata'); class ComputeKernelResult { final bool succeeded; final fe.InitializedCompilerState? previousState; ComputeKernelResult(this.succeeded, this.previousState); } /// Computes a kernel file based on [args]. /// /// If [isWorker] is true then exit codes will not be set on failure. /// /// If [outputBuffer] is provided then messages will be written to that buffer /// instead of printed to the console. /// /// Returns whether or not the summary was successfully output. Future computeKernel(List args, {bool isWorker = false, StringBuffer? outputBuffer, Map>? inputDigests, fe.InitializedCompilerState? previousState}) async { inputDigests ??= >{}; dynamic out = outputBuffer ?? stderr; bool succeeded = true; var parsedArgs = summaryArgsParser.parse(args); if (parsedArgs['help']) { out.writeln(summaryArgsParser.usage); if (!isWorker) exit(0); return new ComputeKernelResult(false, previousState); } // Bazel creates an overlay file system where some files may be located in the // source tree, some in a gendir, and some in a bindir. The multi-root file // system hides this from the front end. var multiRoots = parsedArgs['multi-root'].map(Uri.base.resolve).toList(); if (multiRoots.isEmpty) multiRoots.add(Uri.base); MultiRootFileSystem mrfs = new MultiRootFileSystem( parsedArgs['multi-root-scheme'], multiRoots, fe.StandardFileSystem.instance); FileSystem fileSystem = mrfs; var sources = (parsedArgs['source'] as List).map(toUri).toList(); var excludeNonSources = parsedArgs['exclude-non-sources'] as bool; var nnbdMode = parsedArgs['sound-null-safety'] as bool ? fe.NnbdMode.Strong : fe.NnbdMode.Weak; var summaryOnly = parsedArgs['summary-only'] as bool; var summary = parsedArgs['summary'] as bool; if (summaryOnly && !summary) { throw new ArgumentError('--summary-only conflicts with --no-summary'); } var trackWidgetCreation = parsedArgs['track-widget-creation'] as bool; // TODO(sigmund,jakemac): make target mandatory. We allow null to be backwards // compatible while we migrate existing clients of this tool. var targetName = (parsedArgs['target'] as String?) ?? (summaryOnly ? 'ddc' : 'vm'); var targetFlags = new TargetFlags( trackWidgetCreation: trackWidgetCreation, enableNullSafety: nnbdMode == fe.NnbdMode.Strong); Target target; switch (targetName) { case 'vm': target = new VmTarget(targetFlags); if (summaryOnly) { out.writeln('error: --summary-only not supported for the vm target'); } break; case 'flutter': target = new FlutterTarget(targetFlags); if (summaryOnly) { throw new ArgumentError( 'error: --summary-only not supported for the flutter target'); } break; case 'flutter_runner': target = new FlutterRunnerTarget(targetFlags); if (summaryOnly) { throw new ArgumentError('error: --summary-only not supported for the ' 'flutter_runner target'); } break; case 'dart2js': target = new Dart2jsTarget('dart2js', targetFlags); if (summaryOnly) { out.writeln( 'error: --summary-only not supported for the dart2js target'); } break; case 'dart2js_summary': target = new Dart2jsSummaryTarget( 'dart2js', sources, excludeNonSources, targetFlags); if (!summaryOnly) { out.writeln( 'error: --no-summary-only not supported for the dart2js summary target'); } break; case 'ddc': // TODO(jakemac):If `generateKernel` changes to return a summary // component, process the component instead. target = new DevCompilerSummaryTarget(sources, excludeNonSources, targetFlags); if (!summaryOnly) { out.writeln('error: --no-summary-only not supported for the ' 'ddc target'); } break; default: out.writeln('error: unsupported target: $targetName'); return ComputeKernelResult(false, previousState); } List linkedInputs = (parsedArgs['input-linked'] as List).map(toUri).toList(); List summaryInputs = (parsedArgs['input-summary'] as List).map(toUri).toList(); fe.InitializedCompilerState state; bool usingIncrementalCompiler = false; bool recordUsedInputs = parsedArgs["used-inputs"] != null; var environmentDefines = _parseEnvironmentDefines(parsedArgs['define']); var verbose = parsedArgs['verbose'] as bool; var verbosity = fe.Verbosity.parseArgument(parsedArgs['verbosity']); Uri? sdkSummaryUri = toUriNullable(parsedArgs['dart-sdk-summary']); Map redirectsToFrom = {}; for (String redirect in parsedArgs['redirect']) { List split = redirect.split("|"); if (split.length != 2) throw "Invalid redirect input: '$redirect'"; redirectsToFrom[toUri(split[1])] = toUri(split[0]); } if (redirectsToFrom.isNotEmpty) { // If redirecting from a->b and we were asked to compile b, we want // the output to look like we compiled a. List newSources = []; for (Uri source in sources) { newSources.add(redirectsToFrom[source] ?? source); } // Dart2jsSummaryTarget and DevCompilerSummaryTarget has a pointer to // sources, so to keep it up to date we'll clear and add instead of // overwriting. sources.clear(); sources.addAll(newSources); // Make the filesystem map from a to b, so that if asked to read a, // actually return data from b. If asked to read b throw. fe.InitializedCompilerState helper = fe.initializeCompiler( null, sdkSummaryUri, toUriNullable(parsedArgs['libraries-file']), toUriNullable(parsedArgs['packages-file']), [...summaryInputs, ...linkedInputs], target, fileSystem, parsedArgs['enable-experiment'] as List, environmentDefines, verbose: verbose, nnbdMode: nnbdMode); var uriTranslator = await helper.processedOpts.getUriTranslator(); _FakeFileSystem ffs = fileSystem = new _FakeFileSystem(fileSystem); for (MapEntry entry in redirectsToFrom.entries) { ffs.addRedirect( uriTranslator.translate(entry.value, false) ?? entry.value, uriTranslator.translate(entry.key, false) ?? entry.key); } } if (parsedArgs['use-incremental-compiler']) { usingIncrementalCompiler = true; // If digests weren't given and if not in worker mode, create fake data and // ensure we don't have a previous state (as that wouldn't be safe with // fake input digests). if (!isWorker && inputDigests.isEmpty) { previousState = null; if (sdkSummaryUri != null) { inputDigests[sdkSummaryUri] = const [0]; } for (Uri uri in summaryInputs) { inputDigests[uri] = const [0]; } for (Uri uri in linkedInputs) { inputDigests[uri] = const [0]; } } state = await fe.initializeIncrementalCompiler( previousState, { "target=$targetName", "trackWidgetCreation=$trackWidgetCreation", "multiRootScheme=${mrfs.markerScheme}", "multiRootRoots=${mrfs.roots}", }, sdkSummaryUri, toUriNullable(parsedArgs['packages-file']), toUriNullable(parsedArgs['libraries-file']), [...summaryInputs, ...linkedInputs], inputDigests, target, fileSystem, (parsedArgs['enable-experiment'] as List), summaryOnly, environmentDefines, trackNeededDillLibraries: recordUsedInputs, verbose: verbose, nnbdMode: nnbdMode); } else { state = fe.initializeCompiler( // TODO(sigmund): pass an old state once we can make use of it. null, sdkSummaryUri, toUriNullable(parsedArgs['libraries-file']), toUriNullable(parsedArgs['packages-file']), [...summaryInputs, ...linkedInputs], target, fileSystem, parsedArgs['enable-experiment'] as List, environmentDefines, verbose: verbose, nnbdMode: nnbdMode); } // Either set up or reset the state for macros based on experiment status. // TODO: Make this a part of `initializeCompiler`, if/when we want to make it // more widely supported. if (state.processedOpts.globalFeatures.macros.isEnabled) { enableMacros = true; forceEnableMacros = true; SerializationMode serializationMode; switch (parsedArgs['macro-serialization-mode']) { case 'json': serializationMode = SerializationMode.jsonServer; break; case 'bytedata': serializationMode = SerializationMode.byteDataServer; break; default: throw ArgumentError('Unrecognized macro serialization mode ' '${parsedArgs['macro-serialization-mode']}'); } // TODO: Handle invalidation of precompiled macros. // TODO: Handle multiple macro libraries compiled to a single precompiled // kernel file. var macroExecutor = state.processedOpts.macroExecutor; var format = parsedArgs['precompiled-macro-format']; for (var parts in (parsedArgs['precompiled-macro'] as List) .map((arg) => arg.split(';'))) { var library = Uri.parse(parts[0]); if (macroExecutor.libraryIsRegistered(library)) { continue; } var programUri = toUri(parts[1]); switch (format) { case 'kernel': macroExecutor.registerExecutorFactory( () => isolatedExecutor.start(serializationMode, programUri), {library}); break; case 'aot': macroExecutor.registerExecutorFactory( () => processExecutor.start( serializationMode, processExecutor.CommunicationChannel.socket, programUri.toFilePath()), {library}); break; default: throw ArgumentError('Unrecognized precompiled macro format $format'); } } } else { enableMacros = false; forceEnableMacros = false; await state.options.macroExecutor?.close(); state.options.macroExecutor = null; } void onDiagnostic(fe.DiagnosticMessage message) { if (fe.Verbosity.shouldPrint(verbosity, message)) { fe.printDiagnosticMessage(message, out.writeln); } if (message.severity == fe.Severity.error) { succeeded = false; } } List? kernel; bool wroteUsedDills = false; if (usingIncrementalCompiler) { state.options.onDiagnostic = onDiagnostic; IncrementalCompilerResult incrementalCompilerResult = await state.incrementalCompiler!.computeDelta( entryPoints: sources, fullComponent: true, trackNeededDillLibraries: recordUsedInputs); Component incrementalComponent = incrementalCompilerResult.component; if (recordUsedInputs) { Set usedOutlines = {}; for (Library lib in incrementalCompilerResult.neededDillLibraries!) { if (lib.importUri.isScheme("dart")) continue; Uri? uri = state.libraryToInputDill![lib.importUri]; if (uri == null) { throw new StateError("Library ${lib.importUri} was recorded as used, " "but was not in the list of known libraries."); } usedOutlines.add(uri); } var outputUsedFile = new File(parsedArgs["used-inputs"]); outputUsedFile.createSync(recursive: true); outputUsedFile.writeAsStringSync(usedOutlines.join("\n")); wroteUsedDills = true; } kernel = await state.incrementalCompiler!.context.runInContext((_) { if (summaryOnly) { incrementalComponent.uriToSource.clear(); incrementalComponent.problemsAsJson = null; incrementalComponent.setMainMethodAndMode( null, true, incrementalComponent.mode); target.performOutlineTransformations(incrementalComponent); makeStable(incrementalComponent); return Future.value(fe.serializeComponent(incrementalComponent, includeSources: false, includeOffsets: false)); } makeStable(incrementalComponent); return Future.value(fe.serializeComponent(incrementalComponent, filter: excludeNonSources ? (library) => sources.contains(library.importUri) : null, includeOffsets: true)); }); } else if (summaryOnly) { kernel = await fe.compileSummary(state, sources, onDiagnostic, includeOffsets: false); } else { Component? component = await fe .compileComponent(state, sources, onDiagnostic, buildSummary: summary); if (component != null) { kernel = fe.serializeComponent(component, filter: excludeNonSources ? (library) => sources.contains(library.importUri) : null, includeOffsets: true); } } state.options.onDiagnostic = null; // See http://dartbug.com/36983. if (!wroteUsedDills && recordUsedInputs) { // The path taken didn't record inputs used: Say we used everything. var outputUsedFile = new File(parsedArgs["used-inputs"]); outputUsedFile.createSync(recursive: true); Set allFiles = {...summaryInputs, ...linkedInputs}; outputUsedFile.writeAsStringSync(allFiles.join("\n")); wroteUsedDills = true; } if (kernel != null) { var outputFile = new File(parsedArgs['output']); outputFile.createSync(recursive: true); outputFile.writeAsBytesSync(kernel); } else { assert(!succeeded); } return new ComputeKernelResult(succeeded, state); } /// Make sure the output is stable by sorting libraries and additional exports. void makeStable(Component c) { // Make sure the output is stable. c.libraries.sort((l1, l2) { return "${l1.fileUri}".compareTo("${l2.fileUri}"); }); c.problemsAsJson?.sort(); c.computeCanonicalNames(); for (Library library in c.libraries) { library.additionalExports.sort((Reference r1, Reference r2) { return "${r1.canonicalName}".compareTo("${r2.canonicalName}"); }); library.problemsAsJson?.sort(); } } class _FakeFileSystem extends FileSystem { final Map redirectsFromTo = {}; final Set redirectsTo = {}; final FileSystem fs; _FakeFileSystem(this.fs); void addRedirect(Uri from, Uri to) { redirectsTo.add(to); redirectsFromTo[from] = to; } @override FileSystemEntity entityForUri(Uri uri) { if (redirectsTo.contains(uri)) throw "$uri is a redirection target."; uri = redirectsFromTo[uri] ?? uri; return fs.entityForUri(uri); } } class DevCompilerSummaryTarget extends DevCompilerTarget with SummaryMixin { final List sources; final bool excludeNonSources; DevCompilerSummaryTarget( this.sources, this.excludeNonSources, TargetFlags targetFlags) : super(targetFlags); } Uri? toUriNullable(String? uriString) { if (uriString == null) return null; return toUri(uriString); } Uri toUri(String uriString) { // Windows-style paths use '\', so convert them to '/' in case they've been // concatenated with Unix-style paths. return Uri.base.resolve(uriString.replaceAll("\\", "/")); } Map _parseEnvironmentDefines(List args) { var environment = {}; for (var arg in args) { var eq = arg.indexOf('='); if (eq <= 0) { var kind = eq == 0 ? 'name' : 'value'; throw FormatException('no $kind given to -D option `$arg`'); } var name = arg.substring(0, eq); var value = arg.substring(eq + 1); environment[name] = value; } return environment; }