blob: a45e6a01d36eb95ce6e7cc84804ad21265f50915 [file] [log] [blame]
// Copyright (c) 2024, 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.
import 'dart:async';
import 'dart:convert' show json;
import 'dart:io';
import 'package:args/args.dart';
import 'package:build_integration/file_system/multi_root.dart';
import 'package:front_end/src/api_unstable/ddc.dart' as fe;
import 'package:kernel/binary/ast_from_binary.dart' as kernel
show BinaryBuilder;
import 'package:kernel/binary/ast_to_binary.dart' as kernel show BinaryPrinter;
import 'package:kernel/class_hierarchy.dart';
import 'package:kernel/core_types.dart';
import 'package:kernel/kernel.dart';
import 'package:kernel/target/targets.dart';
import 'package:kernel/text/ast_to_text.dart' as kernel show Printer;
import 'package:kernel/text/debug_printer.dart';
import 'package:path/path.dart' as p;
import 'package:source_maps/source_maps.dart' show SourceMapBuilder;
import '../compiler/js_names.dart' as js_ast;
import '../compiler/module_builder.dart';
import '../js_ast/js_ast.dart' as js_ast;
import '../js_ast/js_ast.dart' show js;
import '../js_ast/source_map_printer.dart' show SourceMapPrintingContext;
import '../kernel/compiler.dart';
import '../kernel/compiler_new.dart';
import '../kernel/hot_reload_delta_inspector.dart';
import '../kernel/module_metadata.dart';
import '../kernel/module_symbols.dart';
import '../kernel/module_symbols_collector.dart';
import '../kernel/target.dart';
import 'arguments.dart';
import 'options.dart';
import 'result.dart';
const _binaryName = 'dartdevc';
/// Invoke the compiler with [args].
///
/// Returns a [CompilerResult], with a success flag indicating whether the
/// program compiled without any fatal errors.
///
/// The result may also contain a [previousResult], which can be passed back in
/// for batch/worker executions to attempt to existing state.
Future<CompilerResult> compile(ParsedArguments args,
{fe.InitializedCompilerState? compilerState,
Map<Uri, List<int>>? inputDigests}) {
if (compilerState != null && !args.isBatchOrWorker) {
throw ArgumentError(
'previousResult requires --batch or --bazel_worker mode/');
}
try {
return _compile(args.rest,
compilerState: compilerState,
isWorker: args.isWorker,
useIncrementalCompiler: args.useIncrementalCompiler,
inputDigests: inputDigests);
} catch (error, stackTrace) {
print('''
We're sorry, you've found a bug in our compiler.
You can report this bug at:
https://github.com/dart-lang/sdk/issues/labels/web-dev-compiler
Please include the information below in your report, along with
any other information that may help us track it down. Thanks!
-------------------- %< --------------------
$_binaryName arguments: ${args.rest.join(' ')}
dart --version: ${Platform.version}
$error
$stackTrace
''');
return Future.value(CompilerResult(70));
}
}
String _usageMessage(ArgParser ddcArgParser) =>
'The Dart Development Compiler compiles Dart sources into a JavaScript '
'module.\n\n'
'Usage: $_binaryName [options...] <sources...>\n\n'
'${ddcArgParser.usage}';
Future<CompilerResult> _compile(List<String> args,
{fe.InitializedCompilerState? compilerState,
bool isWorker = false,
bool useIncrementalCompiler = false,
Map<Uri, List<int>>? inputDigests}) async {
// TODO(jmesserly): refactor options to share code with dartdevc CLI.
var argParser = ArgParser(allowTrailingOptions: true)
..addFlag('help',
abbr: 'h', help: 'Display this message.', negatable: false)
..addOption('packages', help: 'The package spec file to use.')
// TODO(jmesserly): is this still useful for us, or can we remove it now?
..addFlag('summarize-text',
help: 'Emit API summary and AST in .js.txt and .ast.xml files.',
defaultsTo: false,
hide: true)
..addFlag('track-widget-creation',
help: 'Enable inspecting of Flutter widgets.',
defaultsTo: false,
hide: true)
// TODO(jmesserly): add verbose help to show hidden options
..addOption('dart-sdk-summary',
help: 'The path to the Dart SDK summary file.', hide: true)
..addMultiOption('multi-root',
help: 'The directories to search when encountering uris with the '
'specified multi-root scheme.',
defaultsTo: [Uri.base.path])
..addFlag('compile-sdk',
help: 'Build an SDK module.', defaultsTo: false, hide: true)
..addOption('libraries-file',
help: 'The path to the libraries.json file for the sdk.')
..addOption('used-inputs-file',
help: 'If set, the file to record inputs used.', hide: true);
Options.addArguments(argParser);
var declaredVariables = parseAndRemoveDeclaredVariables(args);
ArgResults argResults;
try {
argResults = argParser.parse(filterUnknownArguments(args, argParser));
} on FormatException catch (error) {
print(error);
print(_usageMessage(argParser));
return CompilerResult(64);
}
if (argResults.wasParsed('sound-null-safety')) {
var soundNullSafety = argResults['sound-null-safety'] as bool;
print('Dart 3 only supports sound null safety, '
'see https://dart.dev/null-safety.\n'
'The `--sound-null-safety` flag is ignored '
'and will be removed in a future version.');
if (!soundNullSafety) {
return CompilerResult(64);
}
}
var outPaths = argResults['out'] as List<String>;
var moduleFormats = parseModuleFormatOption(argResults);
if (outPaths.isEmpty) {
print('Please specify the output file location. For example:\n'
' -o PATH/TO/OUTPUT_FILE.js');
return CompilerResult(64);
} else if (outPaths.length != moduleFormats.length) {
print('Number of output files (${outPaths.length}) must match '
'number of module formats (${moduleFormats.length}).');
return CompilerResult(64);
}
if (argResults['help'] as bool || args.isEmpty) {
print(_usageMessage(argParser));
return CompilerResult(0);
}
var options = Options.fromArguments(argResults);
addGeneratedVariables(declaredVariables,
enableAsserts: options.enableAsserts);
Uri toCustomUri(Uri uri) {
if (!uri.hasScheme) {
return Uri(scheme: options.multiRootScheme, path: '/${uri.path}');
}
return uri;
}
// TODO(jmesserly): this is a workaround for the CFE, which does not
// understand relative URIs, and we'd like to avoid absolute file URIs
// being placed in the summary if possible.
// TODO(jmesserly): investigate if Analyzer has a similar issue.
Uri sourcePathToCustomUri(String source) {
return toCustomUri(sourcePathToRelativeUri(source));
}
// Compile SDK module directly from a provided .dill file.
var inputs = [for (var arg in argResults.rest) sourcePathToCustomUri(arg)];
if (inputs.length == 1 && inputs.single.path.endsWith('.dill')) {
return compileSdkFromDill(args);
}
// To make the output .dill agnostic of the current working directory,
// we use a custom-uri scheme for all app URIs (these are files outside the
// lib folder). The following [FileSystem] will resolve those references to
// the correct location and keeps the real file location hidden from the
// front end.
var multiRootPaths = (argResults['multi-root'] as Iterable<String>)
.map(Uri.base.resolve)
.toList();
var multiRootOutputPath = options.multiRootOutputPath;
if (multiRootOutputPath == null) {
if (outPaths.length > 1) {
print(
'If multiple output files (found ${outPaths.length}) are specified, '
'then --multi-root-output-path must be explicitly provided.');
return CompilerResult(64);
}
var jsOutputUri = sourcePathToUri(p.absolute(outPaths.first));
multiRootOutputPath = _longestPrefixingPath(jsOutputUri, multiRootPaths);
}
var fileSystem = MultiRootFileSystem(
options.multiRootScheme, multiRootPaths, fe.StandardFileSystem.instance);
var summaryPaths = options.summaryModules.keys.toList();
var summaryModules = Map.fromIterables(
summaryPaths.map(sourcePathToUri).cast<Uri>(),
options.summaryModules.values);
var sdkSummaryPath = argResults['dart-sdk-summary'] as String?;
var librarySpecPath = argResults['libraries-file'] as String?;
var compileSdk = argResults['compile-sdk'] == true;
if (sdkSummaryPath == null) {
if (!compileSdk) {
sdkSummaryPath = defaultSdkSummaryPath;
librarySpecPath ??= defaultLibrarySpecPath;
}
// Compiling without manually passing or getting a default SDK summary is
// only allowed when `compileSdk` is true.
}
var invalidSummary = summaryPaths.any((s) => !s.endsWith('.dill')) ||
(sdkSummaryPath != null && !sdkSummaryPath.endsWith('.dill'));
if (invalidSummary) {
throw StateError('Non-dill file detected in input: $summaryPaths');
}
if (librarySpecPath == null) {
// TODO(jmesserly): the `isSupported` bit should be included in the SDK
// summary, but front_end requires a separate file, so we have to work
// around that, while not requiring yet another command line option.
//
// Right now we search two locations: one level above the SDK summary
// (this works for the build and SDK layouts) or next to the SDK summary
// (if the user is doing something custom).
//
// Another option: we could make an in-memory file with the relevant info.
librarySpecPath = p.join(
p.dirname(p.dirname(sdkSummaryPath ?? defaultSdkSummaryPath)),
'libraries.json');
if (!File(librarySpecPath).existsSync()) {
librarySpecPath = p.join(
p.dirname(sdkSummaryPath ?? defaultSdkSummaryPath), 'libraries.json');
}
}
/// The .dart_tool/package_config.json file path provided by the user.
//
// TODO(jmesserly): the default location is based on the current working
// directory, to match the behavior of dartanalyzer/dartdevc. However the
// Dart VM, CFE (and dart2js?) use the script file location instead. The
// difference may be due to the lack of a single entry point for Analyzer.
// Ultimately this is just the default behavior; in practice users call DDC
// through a build tool, which generally passes in `--packages=`.
//
// TODO(jmesserly): conceptually CFE should not need a
// .dart_tool/package_config.json file to resolve package URIs that are in the
// input summaries, but it seems to.
// This needs further investigation.
var packageFile =
argResults['packages'] as String? ?? _findPackagesFilePath();
var succeeded = true;
void diagnosticMessageHandler(fe.DiagnosticMessage message) {
if (message.severity == fe.Severity.error) {
succeeded = false;
}
fe.printDiagnosticMessage(message, print);
}
var explicitExperimentalFlags = fe.parseExperimentalFlags(options.experiments,
onError: stderr.writeln, onWarning: print);
var trackWidgetCreation = argResults['track-widget-creation'] as bool;
var oldCompilerState = compilerState;
var recordUsedInputs = argResults['used-inputs-file'] != null;
var additionalDills = summaryModules.keys.toList();
fe.DdcResult? result;
// TODO(jmesserly): is there a cleaner way to do this?
//
// Ideally we'd manage our own batch compilation caching rather than rely on
// `initializeCompiler`. Also we should be able to pass down Components for
// SDK and summaries.
if (!useIncrementalCompiler) {
compilerState = fe.initializeCompiler(
oldCompilerState,
compileSdk,
sourcePathToUri(getSdkPath()),
compileSdk ? null : sourcePathToUri(sdkSummaryPath!),
packageFile != null ? sourcePathToUri(packageFile) : null,
sourcePathToUri(librarySpecPath),
additionalDills,
DevCompilerTarget(
TargetFlags(trackWidgetCreation: trackWidgetCreation)),
fileSystem: fileSystem,
explicitExperimentalFlags: explicitExperimentalFlags,
environmentDefines: declaredVariables);
result = await fe.compile(compilerState, inputs, diagnosticMessageHandler);
} else {
// 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).
inputDigests ??= {};
if (!isWorker && inputDigests.isEmpty) {
oldCompilerState = null;
if (!compileSdk) {
inputDigests[sourcePathToUri(sdkSummaryPath!)] = const [0];
}
for (var uri in summaryModules.keys) {
inputDigests[uri] = const [0];
}
}
var doneAdditionalDills =
List.filled(summaryModules.length, dummyComponent);
compilerState = await fe.initializeIncrementalCompiler(
oldCompilerState,
{
'trackWidgetCreation=$trackWidgetCreation',
'multiRootScheme=${fileSystem.markerScheme}',
'multiRootRoots=${fileSystem.roots}',
},
doneAdditionalDills,
compileSdk,
sourcePathToUri(getSdkPath()),
compileSdk ? null : sourcePathToUri(sdkSummaryPath!),
packageFile != null ? sourcePathToUri(packageFile) : null,
sourcePathToUri(librarySpecPath),
additionalDills,
inputDigests,
DevCompilerTarget(
TargetFlags(trackWidgetCreation: trackWidgetCreation)),
fileSystem: fileSystem,
explicitExperimentalFlags: explicitExperimentalFlags,
environmentDefines: declaredVariables,
trackNeededDillLibraries: recordUsedInputs);
var incrementalCompiler = compilerState.incrementalCompiler!;
var cachedSdkInput = compileSdk
? null
: compilerState.workerInputCache![sourcePathToUri(sdkSummaryPath!)];
compilerState.options.onDiagnostic = diagnosticMessageHandler;
var incrementalCompilerResult = await incrementalCompiler.computeDelta(
entryPoints: inputs,
fullComponent: true,
trackNeededDillLibraries: recordUsedInputs);
result = fe.DdcResult(
incrementalCompilerResult.component,
cachedSdkInput?.component,
doneAdditionalDills,
incrementalCompilerResult.classHierarchy,
incrementalCompilerResult.neededDillLibraries);
}
compilerState.options.onDiagnostic = null; // See http://dartbug.com/36983.
if (result == null || !succeeded) {
return CompilerResult(1, kernelState: compilerState);
}
var component = result.component;
var compiledLibraries = result.compiledLibraries;
final reloadDeltaKernel = options.reloadDeltaKernel;
final reloadLastAcceptedKernel = options.reloadLastAcceptedKernel;
if (reloadDeltaKernel != null) {
if (reloadLastAcceptedKernel != null) {
final lastAcceptedComponent = Component();
kernel.BinaryBuilder((await File(reloadLastAcceptedKernel).readAsBytes()))
.readComponent(lastAcceptedComponent);
final deltaInspector = HotReloadDeltaInspector(
nonHotReloadablePackages: options.nonHotReloadablePackages);
final rejectionReasons = deltaInspector.compareGenerations(
lastAcceptedComponent, compiledLibraries);
if (rejectionReasons.isNotEmpty) {
throw StateError(
'Hot reload rejected due to:\n${rejectionReasons.join('\n')}\n'
'Try performing a hot restart instead.');
}
}
var sink = File(reloadDeltaKernel).openWrite();
kernel.BinaryPrinter(sink, includeSources: false, includeSourceBytes: false)
.writeComponentFile(compiledLibraries);
await sink.flush();
await sink.close();
} else {
if (reloadLastAcceptedKernel != null) {
throw ArgumentError("Must provide 'new-reload-delta-kernel' if "
"'old-reload-delta-kernel' provided.");
}
}
// Output files can be written in parallel, so collect the futures.
var outFiles = <Future>[];
if (argResults['summarize'] as bool) {
if (outPaths.length > 1) {
print(
'If multiple output files (found ${outPaths.length}) are specified, '
'the --summarize option is not supported.');
return CompilerResult(64);
}
// Note: CFE mutates the Kernel tree, so we can't save the dill
// file if we successfully reused a cached library. If compiler state is
// unchanged, it means we used the cache.
//
// In that case, we need to unbind canonical names, because they could be
// bound already from the previous compile.
if (identical(compilerState, oldCompilerState)) {
component.unbindCanonicalNames();
}
var sink = File('${p.withoutExtension(outPaths.first)}.dill').openWrite();
// TODO(jmesserly): this appears to save external libraries.
// Do we need to run them through an outlining step so they can be saved?
kernel.BinaryPrinter(sink).writeComponentFile(component);
outFiles.add(sink.flush().then((_) => sink.close()));
}
String? fullDillUri;
if (argResults['experimental-output-compiled-kernel'] as bool) {
if (outPaths.length > 1) {
print(
'If multiple output files (found ${outPaths.length}) are specified, '
'the --experimental-output-compiled-kernel option is not supported.');
return CompilerResult(64);
}
// Note: CFE mutates the Kernel tree, so we can't save the dill
// file if we successfully reused a cached library. If compiler state is
// unchanged, it means we used the cache.
//
// In that case, we need to unbind canonical names, because they could be
// bound already from the previous compile.
if (identical(compilerState, oldCompilerState)) {
compiledLibraries.unbindCanonicalNames();
}
fullDillUri = '${p.withoutExtension(outPaths.first)}.full.dill';
var sink = File(fullDillUri).openWrite();
kernel.BinaryPrinter(sink).writeComponentFile(compiledLibraries);
outFiles.add(sink.flush().then((_) => sink.close()));
}
if (argResults['summarize-text'] as bool) {
if (outPaths.length > 1) {
print(
'If multiple output files (found ${outPaths.length}) are specified, '
'the --summarize-text option is not supported.');
return CompilerResult(64);
}
var sb = StringBuffer();
kernel.Printer(sb).writeComponentFile(component);
outFiles.add(File('${outPaths.first}.txt').writeAsString(sb.toString()));
outFiles.add(File('${outPaths.first.split('.')[0]}.ast.xml')
.writeAsString(DebugPrinter.prettyPrint(compiledLibraries)));
}
final importToSummary = Map<Library, Component>.identity();
final summaryToModule = Map<Component, String>.identity();
for (var i = 0; i < result.additionalDills.length; i++) {
var additionalDill = result.additionalDills[i];
var moduleImport = summaryModules[additionalDills[i]]!;
for (var l in additionalDill.libraries) {
assert(!importToSummary.containsKey(l));
importToSummary[l] = additionalDill;
summaryToModule[additionalDill] = moduleImport;
}
}
// Add main component libraries to import-to-module resolution.
//
// This is only required for non-SDK modules, as SDK modules are all bundled
// in the same module and are never deferred.
// `result.component` contains all the compiled libraries as well as libraries
// already seen in `additionalDills`.
if (!compileSdk) {
for (var l in result.component.libraries) {
// Don't override libraries already recorded in `additionalDills`.
if (importToSummary.containsKey(l)) {
continue;
}
final isDartLibrary = l.importUri.isScheme('dart');
final resolvedModuleName =
isDartLibrary ? js_ast.dartSdkModule : options.moduleName;
final resolvedComponent =
isDartLibrary ? result.sdkSummary! : result.component;
importToSummary[l] = resolvedComponent;
summaryToModule.putIfAbsent(resolvedComponent, () => resolvedModuleName);
}
}
var compiler = options.emitLibraryBundle
? LibraryBundleCompiler(component, result.classHierarchy, options,
importToSummary, summaryToModule)
: ProgramCompiler(component, result.classHierarchy, options,
importToSummary, summaryToModule);
var jsModule = compiler.emitModule(compiledLibraries);
// Also the old Analyzer backend had some code to make debugging better when
// --single-out-file is used, but that option does not appear to be used by
// any of our build systems.
for (var i = 0; i < outPaths.length; ++i) {
var output = outPaths[i];
var moduleFormat = moduleFormats[i];
var file = File(output);
await file.parent.create(recursive: true);
var mapUrl = p.toUri('$output.map').toString();
var jsCode = jsProgramToCode(
jsModule,
options.emitLibraryBundle
? ModuleFormat.ddcLibraryBundle
: moduleFormat,
buildSourceMap: options.sourceMap,
inlineSourceMap: options.inlineSourceMap,
emitDebugMetadata: options.emitDebugMetadata,
emitDebugSymbols: options.emitDebugSymbols,
jsUrl: p.toUri(output).toString(),
mapUrl: mapUrl,
fullDillUri: fullDillUri,
customScheme: options.multiRootScheme,
multiRootOutputPath: multiRootOutputPath,
compiler: compiler,
component: compiledLibraries);
outFiles.add(file.writeAsString(jsCode.code));
if (jsCode.sourceMap != null) {
outFiles.add(
File('$output.map').writeAsString(json.encode(jsCode.sourceMap)));
}
if (jsCode.metadata != null) {
outFiles.add(
File('$output.metadata').writeAsString(json.encode(jsCode.metadata)));
}
if (jsCode.symbols != null) {
outFiles.add(
File('$output.symbols').writeAsString(json.encode(jsCode.symbols)));
}
}
if (recordUsedInputs) {
var usedOutlines = <Uri>{};
if (useIncrementalCompiler) {
var neededDillLibraries = result.neededDillLibraries!;
compilerState.incrementalCompiler!.updateNeededDillLibrariesWithHierarchy(
neededDillLibraries, result.classHierarchy);
for (var lib in neededDillLibraries) {
if (lib.importUri.isScheme('dart')) continue;
var uri = compilerState.libraryToInputDill![lib.importUri];
if (uri == null) {
throw StateError('Library ${lib.importUri} was recorded as used, '
'but was not in the list of known libraries.');
}
usedOutlines.add(uri);
}
} else {
// Used inputs wasn't recorded: Say we used everything.
usedOutlines.addAll(summaryModules.keys);
}
var outputUsedFile = File(argResults['used-inputs-file'] as String);
outputUsedFile.createSync(recursive: true);
outputUsedFile.writeAsStringSync(usedOutlines.join('\n'));
}
await Future.wait(outFiles);
return CompilerResult(0, kernelState: compilerState);
}
// A simplified entrypoint similar to `_compile` that only supports building the
// sdk. Note that some changes in `_compile_` might need to be copied here as
// well.
// TODO(sigmund): refactor the underlying pieces to reduce the code duplication.
Future<CompilerResult> compileSdkFromDill(List<String> args) async {
var argParser = ArgParser(allowTrailingOptions: true);
Options.addSdkRequiredArguments(argParser);
ArgResults argResults;
try {
argResults = argParser.parse(filterUnknownArguments(args, argParser));
} on FormatException catch (error) {
print(error);
print(_usageMessage(argParser));
return CompilerResult(64);
}
var inputs = argResults.rest.toList();
if (inputs.length != 1) {
print('Only a single input file is supported to compile the sdk from dill'
'but found: \n${inputs.join('\n')}');
return CompilerResult(64);
}
if (!inputs.single.endsWith('.dill')) {
print('Input must be a .dill file: ${inputs.single}');
return CompilerResult(64);
}
var outPaths = argResults['out'] as List<String>;
var moduleFormats = parseModuleFormatOption(argResults);
if (outPaths.isEmpty) {
print('Please specify the output file location. For example:\n'
' -o PATH/TO/OUTPUT_FILE.js');
return CompilerResult(64);
} else if (outPaths.length != moduleFormats.length) {
print('Number of output files (${outPaths.length}) must match '
'number of module formats (${moduleFormats.length}).');
return CompilerResult(64);
}
var component = loadComponentFromBinary(inputs.single);
var invalidLibraries = <Uri>[];
for (var library in component.libraries) {
if (!library.importUri.isScheme('dart')) {
invalidLibraries.add(library.importUri);
}
}
if (invalidLibraries.isNotEmpty) {
print('Only the SDK libraries can be compiled from .dill but found:\n'
'${invalidLibraries.join('\n')}');
return CompilerResult(64);
}
var coreTypes = CoreTypes(component);
var hierarchy = ClassHierarchy(component, coreTypes);
var options = Options.fromSdkRequiredArguments(argResults);
var compiler = options.emitLibraryBundle
? LibraryBundleCompiler(component, hierarchy, options, const {}, const {},
coreTypes: coreTypes)
: ProgramCompiler(component, hierarchy, options, const {}, const {},
coreTypes: coreTypes);
var jsModule = compiler.emitModule(component);
var outFiles = <Future>[];
// Also the old Analyzer backend had some code to make debugging better when
// --single-out-file is used, but that option does not appear to be used by
// any of our build systems.
for (var i = 0; i < outPaths.length; ++i) {
var output = outPaths[i];
var moduleFormat = moduleFormats[i];
var file = File(output);
await file.parent.create(recursive: true);
var jsCode = jsProgramToCode(
jsModule,
options.emitLibraryBundle
? ModuleFormat.ddcLibraryBundle
: moduleFormat,
buildSourceMap: options.sourceMap,
inlineSourceMap: options.inlineSourceMap,
jsUrl: p.toUri(output).toString(),
mapUrl: p.toUri('$output.map').toString(),
customScheme: options.multiRootScheme,
multiRootOutputPath: options.multiRootOutputPath,
component: component);
outFiles.add(file.writeAsString(jsCode.code));
if (jsCode.sourceMap != null) {
outFiles.add(
File('$output.map').writeAsString(json.encode(jsCode.sourceMap)));
}
}
await Future.wait(outFiles);
return CompilerResult(0);
}
/// Compute code size to embed in the generated JavaScript for this module.
int _computeDartSize(Component component) {
var dartSize = 0;
var uriToSource = component.uriToSource;
for (var lib in component.libraries) {
var libUri = lib.fileUri;
var importUri = lib.importUri;
var source = uriToSource[libUri];
if (source == null) {
// Sources that only contain external declarations have nothing to add to
// the sum.
continue;
}
dartSize += source.source.length;
for (var part in lib.parts) {
var partUri = part.partUri;
if (partUri.startsWith(importUri.scheme)) {
// Convert to a relative-to-library uri in order to compute a file uri.
partUri = p.relative(partUri, from: p.dirname('${lib.importUri}'));
}
var fileUri = libUri.resolve(partUri);
var partSource = uriToSource[fileUri];
if (partSource == null) {
// Sources that only contain external declarations have nothing to add
// to the sum.
continue;
}
dartSize += partSource.source.length;
}
}
return dartSize;
}
/// The output of compiling a JavaScript module in a particular format.
/// This was copied from module_compiler.dart class "JSModuleCode".
class JSCode {
/// The JavaScript code for this module.
///
/// If a [sourceMap] is available, this will include the `sourceMappingURL`
/// comment at end of the file.
final String code;
/// The JSON of the source map, if generated, otherwise `null`.
///
/// The source paths will initially be absolute paths. They can be adjusted
/// using [placeSourceMap].
final Map<String, Object?>? sourceMap;
/// Module and library information
///
/// The [metadata] is a contract between compiler and the debugger,
/// helping the debugger map between libraries, modules, source paths.
/// see: https://goto.google.com/dart-web-debugger-metadata
final ModuleMetadata? metadata;
/// Module debug symbols.
///
/// The [symbols] is a contract between compiler and the debugger,
/// helping the debugger map between dart and JS objects.
final ModuleSymbols? symbols;
JSCode(this.code, this.sourceMap, {this.symbols, this.metadata});
}
/// Converts [moduleTree] to [JSCode], using [format].
///
/// See [placeSourceMap] for a description of [sourceMapBase], [customScheme],
/// and [multiRootOutputPath] arguments.
JSCode jsProgramToCode(js_ast.Program moduleTree, ModuleFormat format,
{bool buildSourceMap = false,
bool inlineSourceMap = false,
bool emitDebugMetadata = false,
bool emitDebugSymbols = false,
String? jsUrl,
String? mapUrl,
String? fullDillUri,
String? sourceMapBase,
String? customScheme,
String? multiRootOutputPath,
Compiler? compiler,
Component? component}) {
var opts = js_ast.JavaScriptPrintingOptions(
allowKeywordsInProperties: true, allowSingleLineIfStatements: true);
js_ast.SimpleJavaScriptPrintingContext printer;
SourceMapBuilder? sourceMap;
if (buildSourceMap) {
var sourceMapContext = SourceMapPrintingContext();
sourceMap = sourceMapContext.sourceMap;
printer = sourceMapContext;
} else {
printer = js_ast.SimpleJavaScriptPrintingContext();
}
var tree = transformModuleFormat(format, moduleTree);
var nameListener = emitDebugSymbols ? js_ast.NameListener() : null;
tree.accept(js_ast.Printer(opts, printer,
localNamer: js_ast.ScopedNamer(tree, nameListener)));
Map<String, Object?>? builtMap;
if (buildSourceMap && sourceMap != null) {
builtMap = placeSourceMap(sourceMap.build(jsUrl!), mapUrl!, customScheme,
multiRootOutputPath: multiRootOutputPath, sourceMapBase: sourceMapBase);
var jsDir = p.dirname(p.fromUri(jsUrl));
var relative = p.relative(p.fromUri(mapUrl), from: jsDir);
var relativeMapUrl = p.toUri(relative).toString();
assert(p.dirname(jsUrl) == p.dirname(mapUrl));
printer.emit('\n//# sourceMappingURL=');
printer.emit(relativeMapUrl);
printer.emit('\n');
}
var text = printer.getText();
var encodedMap = json.encode(builtMap);
var rawSourceMap =
inlineSourceMap ? js.escapedString(encodedMap, "'").value : 'null';
text = text.replaceFirst(ProgramCompiler.sourceMapLocationID, rawSourceMap);
// This is intended to be used by our build/debug tools to gather metrics.
// See pkg/dev_compiler/lib/js/ddc/ddc_module_loader.js for runtime code that
// reads this.
//
// These keys (see corresponding logic in ddc_module_loader.js) include:
// - dartSize: <size of Dart input code in bytes>
// - sourceMapSize: <size of JS source map in bytes>
//
// TODO(vsm): Ideally, this information is never sent to the browser. I.e.,
// our runtime metrics gathering would obtain this information from the
// compilation server, not the browser. We don't yet have the infra for that.
var compileTimeStatistics = {
'dartSize': _computeDartSize(component!),
'sourceMapSize': encodedMap.length
};
text = text.replaceFirst(
ProgramCompiler.metricsLocationID, '$compileTimeStatistics');
var debugMetadata = emitDebugMetadata
? _emitMetadata(moduleTree, component, mapUrl!, jsUrl!, fullDillUri)
: null;
var debugSymbols = emitDebugSymbols
? _emitSymbols(
compiler!, moduleTree.name!, nameListener!.identifierNames, component)
: null;
return JSCode(text, builtMap, symbols: debugSymbols, metadata: debugMetadata);
}
/// Assembles symbol information describing the nodes from the AST [component]
/// and their representation in JavaScript.
///
/// Uses information from the [compiler] used to compile the JS module combined
/// with [identifierNames] that maps JavaScript identifier nodes to their actual
/// names used when outputting the JavaScript.
ModuleSymbols _emitSymbols(Compiler compiler, String moduleName,
Map<js_ast.Identifier, String> identifierNames, Component component) {
/// Returns the name selected in the final JavaScript for [id].
String lookupName(js_ast.Identifier id) {
var name = identifierNames[id];
if (name == null) {
throw Exception('No recorded naming decision found for Identifier with '
'name: ${id.name}');
}
return name;
}
var classJsNames = <Class, String>{
for (var e in compiler.classIdentifiers.entries) e.key: lookupName(e.value),
};
var procedureJsNames = <Procedure, String>{
for (var e in compiler.procedureIdentifiers.entries)
e.key: lookupName(e.value),
};
var variableJsNames = <VariableDeclaration, String>{
for (var e in compiler.variableIdentifiers.entries)
e.key: lookupName(e.value),
};
return ModuleSymbolsCollector(moduleName, classJsNames, compiler.memberNames,
procedureJsNames, variableJsNames)
.collectSymbolInfo(component);
}
ModuleMetadata _emitMetadata(js_ast.Program program, Component component,
String sourceMapUri, String moduleUri, String? fullDillUri) {
var metadata = ModuleMetadata(program.name!, loadFunctionName(program.name!),
sourceMapUri, moduleUri, fullDillUri);
for (var lib in component.libraries) {
metadata.addLibrary(LibraryMetadata(
libraryUriToJsIdentifier(lib.importUri),
lib.importUri.toString(),
lib.fileUri.toString(),
[...lib.parts.map((p) => p.partUri)]));
}
return metadata;
}
/// Parses Dart's non-standard `-Dname=value` syntax for declared variables,
/// and removes them from [args] so the result can be parsed normally.
Map<String, String> parseAndRemoveDeclaredVariables(List<String> args) {
var declaredVariables = <String, String>{};
for (var i = 0; i < args.length;) {
var arg = args[i];
String? rest;
const defineFlag = '--define';
if (arg.startsWith('-D') && arg.length > 2) {
rest = arg.substring(2);
} else if (arg.startsWith('$defineFlag=') &&
arg.length > defineFlag.length + 1) {
rest = arg.substring(defineFlag.length + 1);
} else if (arg == defineFlag) {
i++;
rest = args[i];
}
if (rest != null) {
var eq = rest.indexOf('=');
if (eq <= 0) {
var kind = eq == 0 ? 'name' : 'value';
throw FormatException('no $kind given to -D option `$arg`');
}
var name = rest.substring(0, eq);
var value = rest.substring(eq + 1);
declaredVariables[name] = value;
args.removeAt(i);
} else {
i++;
}
}
return declaredVariables;
}
/// Adds all synthesized environment variables to [variables].
Map<String, String> addGeneratedVariables(Map<String, String> variables,
{required bool enableAsserts}) {
variables['dart.web.assertions_enabled'] = '$enableAsserts';
return variables;
}
/// The default path of the kernel summary for the Dart SDK.
final defaultSdkSummaryPath =
p.join(getSdkPath(), 'lib', '_internal', 'ddc_outline.dill');
final defaultLibrarySpecPath = p.join(getSdkPath(), 'lib', 'libraries.json');
/// Return the path to the runtime Dart SDK.
String getSdkPath() {
// Support explicit sdk location through an environment variable.
var resolvedExecutable = Platform.environment['resolvedExecutable'];
return p
.dirname(p.dirname(resolvedExecutable ?? Platform.resolvedExecutable));
}
/// Returns the absolute path to the default `package_config.json` file, or
/// `null` if one could not be found.
///
/// Checks for a `.dart_tool/package_config.json` file in the current working
/// directory, or in any parent directory.
String? _findPackagesFilePath() {
// TODO(jmesserly): this was copied from package:package_config/discovery.dart
// Unfortunately the relevant function is not public. CFE APIs require a URI
// to the .dart_tool/package_config.json file, rather than letting us provide
// the package map data.
var dir = Directory.current;
if (!dir.isAbsolute) dir = dir.absolute;
if (!dir.existsSync()) return null;
// Check for $cwd/.dart_tool/package_config.json
while (true) {
var file = File.fromUri(dir.uri.resolve('.dart_tool/package_config.json'));
if (file.existsSync()) return file.path;
// If we didn't find it, search the parent directory.
// Stop the search if we're already at the root.
var parent = dir.parent;
if (dir.path == parent.path) return null;
dir = parent;
}
}
/// Inputs must be absolute paths. Returns null if no prefixing path is found.
String? _longestPrefixingPath(Uri baseUri, List<Uri> prefixingPaths) {
var basePath = baseUri.path;
return prefixingPaths.fold(null, (String? previousValue, Uri element) {
if (basePath.startsWith(element.path) &&
(previousValue == null || previousValue.length < element.path.length)) {
return element.path;
}
return previousValue;
});
}
/// Convert a [source] string to a Uri, where the source may be a
/// dart/file/package URI or a local win/mac/linux path.
Uri sourcePathToUri(String source, {bool? windows}) {
if (windows == null) {
// Running on the web the Platform check will fail, and we can't use
// fromEnvironment because internally it's set to true for dart.library.io.
// So just catch the exception and if it fails then we're definitely not on
// Windows.
try {
windows = Platform.isWindows;
} catch (e) {
windows = false;
}
}
if (windows) {
source = source.replaceAll('\\', '/');
}
var result = Uri.base.resolve(source);
if (windows && result.scheme.length == 1) {
// Assume c: or similar --- interpret as file path.
return Uri.file(source, windows: true);
}
return result;
}
Uri sourcePathToRelativeUri(String source, {bool? windows}) {
var uri = sourcePathToUri(source, windows: windows);
if (uri.isScheme('file')) {
var uriPath = uri.path;
var root = Uri.base.path;
if (uriPath.startsWith(root)) {
return p.toUri(uriPath.substring(root.length));
}
}
return uri;
}
/// Adjusts the source uris in [sourceMap] to be relative uris, and returns
/// the new map.
///
/// Source uris show up in two forms, absolute `file:` uris and custom
/// [multiRootScheme] uris (also "absolute" uris, but always relative to some
/// multi-root).
///
/// - `file:` uris are converted to be relative to [sourceMapBase], which
/// defaults to the dirname of [sourceMapPath] if not provided.
///
/// - [multiRootScheme] uris are prefixed by [multiRootOutputPath]. If the
/// path starts with `/lib`, then we strip that before making it relative
/// to the [multiRootOutputPath], and assert that [multiRootOutputPath]
/// starts with `/packages` (more explanation inline).
///
// TODO(#40251): Remove this logic from dev_compiler itself, push it to the
// invokers of dev_compiler which have more knowledge about how they want
// source paths to look.
Map<String, Object?> placeSourceMap(Map<String, Object?> sourceMap,
String sourceMapPath, String? multiRootScheme,
{String? multiRootOutputPath, String? sourceMapBase}) {
var map = Map.of(sourceMap);
// Convert to a local file path if it's not.
sourceMapPath = sourcePathToUri(p.absolute(p.fromUri(sourceMapPath))).path;
var sourceMapDir = p.url.dirname(sourceMapPath);
sourceMapBase ??= sourceMapDir;
var list = (map['sources'] as List).toList();
String makeRelative(String sourcePath) {
var uri = sourcePathToUri(sourcePath);
var scheme = uri.scheme;
if (scheme == 'dart' || scheme == 'package' || scheme == multiRootScheme) {
if (scheme == multiRootScheme) {
// TODO(sigmund): extract all source-map normalization outside ddc. This
// custom logic is BUILD specific and could be shared with other tools
// like dart2js.
var shortPath = uri.path.replaceAll('/sdk/', '/dart-sdk/');
var multiRootPath = "${multiRootOutputPath ?? ''}$shortPath";
multiRootPath = p.url
.joinAll(p.split(p.relative(multiRootPath, from: sourceMapDir)));
return multiRootPath;
}
return sourcePath;
}
if (uri.isScheme('http')) return sourcePath;
// Convert to a local file path if it's not.
sourcePath = sourcePathToUri(p.absolute(p.fromUri(uri))).path;
// Fall back to a relative path against the source map itself.
sourcePath = p.url.relative(sourcePath, from: sourceMapBase);
// Convert from relative local path to relative URI.
return p.toUri(sourcePath).path;
}
for (var i = 0; i < list.length; i++) {
list[i] = makeRelative(list[i] as String);
}
map['sources'] = list;
map['file'] =
map['file'] != null ? makeRelative(map['file'] as String) : null;
return map;
}