// 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:package_config/package_config.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/kernel_helpers.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 compile( ParsedArguments args, { fe.InitializedCompilerState? compilerState, Map>? 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...] \n\n' '${ddcArgParser.usage}'; Future _compile( List args, { fe.InitializedCompilerState? compilerState, bool isWorker = false, bool useIncrementalCompiler = false, Map>? inputDigests, }) async { var argParser = Options.nonSdkArgParser(); 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.flag('sound-null-safety'); 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.multiOption('out'); 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.flag('help') || 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 .multiOption('multi-root') .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(), options.summaryModules.values, ); var sdkSummaryPath = argResults.option('dart-sdk-summary'); var librarySpecPath = argResults.option('libraries-file'); var compileSdk = argResults.flag('compile-sdk'); 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.option('packages') ?? _findPackagesFilePath(); var succeeded = true; void diagnosticMessageHandler(fe.CfeDiagnosticMessage message) { if (message.severity == fe.CfeSeverity.error) { succeeded = false; } fe.printDiagnosticMessage(message, print); } var explicitExperimentalFlags = fe.parseExperimentalFlags( options.experiments, onError: stderr.writeln, onWarning: print, ); var trackWidgetCreation = argResults.flag('track-widget-creation'); var oldCompilerState = compilerState; var recordUsedInputs = argResults.option('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 'reload-delta-kernel' if " "'reload-last-accepted-kernel' provided.", ); } } // Output files can be written in parallel, so collect the futures. var outFiles = []; if (argResults.flag('summarize')) { 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.flag('experimental-output-compiled-kernel')) { 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.flag('summarize-text')) { 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.identity(); final summaryToModule = Map.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)}\n'), ); } if (jsCode.metadata != null) { outFiles.add( File( '$output.metadata', ).writeAsString('${json.encode(jsCode.metadata)}\n'), ); } if (jsCode.symbols != null) { outFiles.add( File( '$output.symbols', ).writeAsString('${json.encode(jsCode.symbols)}\n'), ); } } if (recordUsedInputs) { var usedOutlines = {}; 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.option('used-inputs-file')!); 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 compileSdkFromDill(List 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.multiOption('out'); 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 = []; 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 = []; // 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; for (var lib in component.libraries) { dartSize += lib.dartSize; } 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? 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], /// [multiRootOutputPath] and [packageConfig] 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, PackageConfig? packageConfig, }) { 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? builtMap; if (buildSourceMap && sourceMap != null) { builtMap = placeSourceMap( sourceMap.build(jsUrl!), mapUrl!, customScheme, multiRootOutputPath: multiRootOutputPath, sourceMapBase: sourceMapBase, packageConfig: packageConfig, ); 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: // - sourceMapSize: // // 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. if (ModuleFormat.ddcLibraryBundle == format) { // TODO(nshahan): Find a better solution for the size of the source map // per library. For now we just use the size of the full source map and only // count it once in the runtime when the first library gets defined. text = text.replaceFirst( ProgramCompiler.metricsLocationID, '${encodedMap.length}', ); text = text.replaceAll(LibraryCompiler.metricsLocationID, '0'); } else { 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 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 = { for (var e in compiler.classIdentifiers.entries) e.key: lookupName(e.value), }; var procedureJsNames = { for (var e in compiler.procedureIdentifiers.entries) e.key: lookupName(e.value), }; var variableJsNames = { 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 parseAndRemoveDeclaredVariables(List args) { var declaredVariables = {}; 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 addGeneratedVariables( Map 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 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; } /// Pattern to match when a relative path appears to leave the `lib/` directory /// of one package to enter the `lib/` directory of another package. final _crossPackageLib = RegExp( // Two or more '../'. r'\.\.\/(?:\.\.\/)+' // The package name (captured). r'([^\/]*)' // The `/lib/` directory. r'\/lib\/', ); /// 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). /// /// Passing a [packageConfig] will enable the rewriting of original source paths /// when the sourcemap contains files from `lib/` directories across multiple /// packages. The rewriting assumes that the sources will be served in a package /// directory structure without `lib/` directories. /// // 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 placeSourceMap( Map sourceMap, String sourceMapPath, String? multiRootScheme, { String? multiRootOutputPath, String? sourceMapBase, PackageConfig? packageConfig, }) { 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 basePackageName = packageConfig?.packageOf(p.toUri(sourceMapBase))?.name; 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. var relativeUriPath = p.toUri(sourcePath).path; // Handle source paths when the original sources come from the `lib/` // directories from different packages. // // This assumes that the original source files and source map will be hosted // in a packages directory structure with all of the `lib/` directories // removed. // For reference: // - https://github.com/dart-lang/sdk/issues/40251 // - https://github.com/dart-lang/webdev/issues/1692 if (basePackageName != null && uri.isScheme('file')) { var sourcePackage = packageConfig?.packageOf(uri); if (sourcePackage != null && basePackageName != sourcePackage.name) { // The source location is part of a different package. var match = _crossPackageLib.matchAsPrefix(relativeUriPath); if (match != null) { var crossPackageName = match[1]; return relativeUriPath.replaceFirst( '../../$crossPackageName/lib/', '../$crossPackageName/', ); } } } return relativeUriPath; } 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; }