// 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. // ignore_for_file: implementation_imports /// A library to invoke the CFE to compute kernel summary files. /// /// Used by `utils/bazel/kernel_worker.dart`. library; import 'dart:async'; import 'dart:io'; import 'package:args/args.dart'; import 'package:build_integration/file_system/multi_root.dart'; import 'package:compiler/src/kernel/dart2js_target.dart'; import 'package:dart2wasm/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/api_unstable/frontend_server.dart'; import 'package:kernel/ast.dart' show Component, Library; import 'package:kernel/target/targets.dart'; import 'package:vm/kernel_front_end.dart'; import 'package:vm/modular/target/flutter.dart'; import 'package:vm/modular/target/flutter_runner.dart'; import 'package:vm/modular/target/vm.dart'; import 'package:vm/native_assets/synthesizer.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.of(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 ArgParser 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', 'dart2wasm', 'ddc', ], help: 'Build kernel for the vm, flutter, flutter_runner, dart2js, ' 'dart2wasm 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-creation-locations', defaultsTo: false, aliases: [ // TODO(http://dartbug.com/63225): Remove this once flutter is migrated // to the new flag. 'track-widget-creation', ], ) ..addFlag('include-unsupported-platform-library-stubs', defaultsTo: false) ..addMultiOption( 'enable-experiment', help: 'Enable a language experiment when invoking the CFE.', ) ..addMultiOption('define', abbr: 'D') ..addFlag('verbose', defaultsTo: false) // TODO(jensj): Remove this. ..addFlag('sound-null-safety', defaultsTo: true) ..addFlag('null-environment', defaultsTo: false, negatable: 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, ) // TODO(johnniwinther): Remove the macros-related options. ..addFlag( 'require-prebuilt-macros', defaultsTo: false, help: 'Require that prebuilt macros for all macro applications be ' 'passed via --precompiled-macro. If not, fail the build.', ) ..addMultiOption( 'precompiled-macro', help: 'Configuration for precompiled macro binaries or kernel files.\n' 'The expected format of this option is as follows: ' ';\nFor example: ' '--precompiled-macro="/path/to/compiled/macro;' 'package:some_macro/some_macro.dart". Multiple library uris may be ' 'passed as well (separated by semicolons).', ) ..addOption( 'macro-serialization-mode', help: 'The serialization mode for communicating with macros.', allowed: ['bytedata', 'json'], defaultsTo: 'bytedata', ) ..addOption('native-assets', help: 'Path to native assets yaml file.'); class ComputeKernelResult { final bool succeeded; final fe.InitializedCompilerState? previousState; new(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 ??= >{}; StringSink out = outputBuffer ?? stderr; bool succeeded = true; ArgResults 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. List multiRoots = (parsedArgs['multi-root'] as List) .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; List sources = (parsedArgs['source'] as List) .map(toUri) .toList(); bool excludeNonSources = parsedArgs['exclude-non-sources'] as bool; bool summaryOnly = parsedArgs['summary-only'] as bool; bool summary = parsedArgs['summary'] as bool; if (summaryOnly && !summary) { throw new ArgumentError('--summary-only conflicts with --no-summary'); } bool trackCreationLocations = parsedArgs['track-creation-locations'] as bool; bool includeUnsupportedPlatformLibraryStubs = parsedArgs['include-unsupported-platform-library-stubs'] as bool; // TODO(sigmund,jakemac): make target mandatory. We allow null to be backwards // compatible while we migrate existing clients of this tool. String targetName = (parsedArgs['target'] as String?) ?? (summaryOnly ? 'ddc' : 'vm'); TargetFlags targetFlags = new TargetFlags( trackCreationLocations: trackCreationLocations, includeUnsupportedPlatformLibraryStubs: includeUnsupportedPlatformLibraryStubs, ); 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; case 'dart2wasm': target = new WasmTarget(); break; default: out.writeln('error: unsupported target: $targetName'); return new 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 = parsedArgs['use-incremental-compiler']; bool recordUsedInputs = parsedArgs["used-inputs"] != null; bool usingNullEnvironment = parsedArgs['null-environment']; Map? nullableEnvironmentDefines; Map environmentDefines = _parseEnvironmentDefines( parsedArgs['define'], ); if (usingNullEnvironment) { if (environmentDefines.isNotEmpty) { throw new ArgumentError( '`--null-environment` not supported with defines.', ); } else if (!target.constantsBackend.supportsUnevaluatedConstants) { throw new ArgumentError( '`--null-environment` not supported on `$targetName`.', ); } else if (usingIncrementalCompiler) { throw new ArgumentError( '`--null-environment` not supported with incremental compilation.', ); } } else { nullableEnvironmentDefines = environmentDefines; } bool verbose = parsedArgs['verbose'] as bool; fe.Verbosity 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, nullableEnvironmentDefines, verbose: verbose, ); UriTranslator uriTranslator = await helper.processedOpts.getUriTranslator(); _FakeFileSystem fakeFileSystem = fileSystem = new _FakeFileSystem( fileSystem, ); for (MapEntry entry in redirectsToFrom.entries) { fakeFileSystem.addRedirect( uriTranslator.translate(entry.value, false) ?? entry.value, uriTranslator.translate(entry.key, false) ?? entry.key, ); } } if (usingIncrementalCompiler) { // 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", // trackCreationLocations is in TargetFlags. "trackCreationLocations=$trackCreationLocations", // includeUnsupportedPlatformLibraryStubs is in TargetFlags. "includeUnsupportedPlatformLibraryStubs=" "$includeUnsupportedPlatformLibraryStubs", "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, nullableEnvironmentDefines!, trackNeededDillLibraries: recordUsedInputs, verbose: verbose, ); } 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, nullableEnvironmentDefines, verbose: verbose, ); } void onDiagnostic(fe.CfeDiagnosticMessage message) { if (fe.Verbosity.shouldPrint(verbosity, message)) { fe.printDiagnosticMessage(message, out.writeln); } if (message.severity == fe.CfeSeverity.error) { succeeded = false; } } List? kernel; bool wroteUsedDills = false; String? nativeAssets = parsedArgs['native-assets']; Library? nativeAssetsLibrary; if (nativeAssets != null) { Uri nativeAssetsUri = Uri.base.resolve(nativeAssets); nativeAssetsLibrary = await NativeAssetsSynthesizer.synthesizeLibraryFromYamlFile( nativeAssetsUri, new ErrorDetector(), ); } 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); } File 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); makeStable(incrementalComponent); return new Future.value( fe.serializeComponent( incrementalComponent, includeSources: false, includeOffsets: false, ), ); } makeStable(incrementalComponent); setNativeAssetsLibrary(incrementalComponent, nativeAssetsLibrary); return new Future.value( fe.serializeComponent( incrementalComponent, filter: excludeNonSources ? (library) => sources.contains(library.importUri) || library == nativeAssetsLibrary : 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) { setNativeAssetsLibrary(component, nativeAssetsLibrary); kernel = fe.serializeComponent( component, filter: excludeNonSources ? (library) => sources.contains(library.importUri) || library == nativeAssetsLibrary : 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. File outputUsedFile = new File(parsedArgs["used-inputs"]); outputUsedFile.createSync(recursive: true); Set allFiles = {...summaryInputs, ...linkedInputs}; outputUsedFile.writeAsStringSync(allFiles.join("\n")); wroteUsedDills = true; } if (kernel != null) { File outputFile = new File(parsedArgs['output']); outputFile.createSync(recursive: true); outputFile.writeAsBytesSync(kernel); } else { assert(!succeeded); } return new ComputeKernelResult(succeeded, state); } final Uri _nativeAssetsLibraryUri = Uri.parse('vm:ffi:native-assets'); void setNativeAssetsLibrary(Component component, Library? nativeAssetsLibrary) { if (nativeAssetsLibrary == null) { return; } assert(nativeAssetsLibrary.importUri == _nativeAssetsLibraryUri); component.libraries.removeWhere( (l) => l.importUri == _nativeAssetsLibraryUri, ); component.libraries.add(nativeAssetsLibrary..parent = component); } /// 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(); library.problemsAsJson?.sort(); } } class _FakeFileSystem extends FileSystem { final Map redirectsFromTo = {}; final Set redirectsTo = {}; final FileSystem fs; new(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 { @override final List sources; @override final bool excludeNonSources; new(this.sources, this.excludeNonSources, TargetFlags targetFlags) : super(targetFlags); @override bool isModularlyCompatibleWith(Target other) { if (other is! DevCompilerSummaryTarget) return false; if (excludeNonSources != other.excludeNonSources) return false; return true; } @override void updateModularCompatibilityAs(Target other) { assert(other is DevCompilerSummaryTarget); sources.clear(); sources.addAll((other as DevCompilerSummaryTarget).sources); } } 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) { Map environment = {}; for (String arg in args) { int eq = arg.indexOf('='); if (eq <= 0) { String kind = eq == 0 ? 'name' : 'value'; throw new FormatException('no $kind given to -D option `$arg`'); } String name = arg.substring(0, eq); String value = arg.substring(eq + 1); environment[name] = value; } return environment; }