| // 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<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 { | |
| 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<Uri>(), | |
| 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 trackCreationLocations = argResults.flag('track-creation-locations'); | |
| var deprecatedJsInterop = argResults.flag('deprecated-js-interop'); | |
| var target = DevCompilerTarget( | |
| TargetFlags(trackCreationLocations: trackCreationLocations), | |
| deprecatedJsInterop: deprecatedJsInterop, | |
| ); | |
| var oldCompilerState = _reusableCompilerState(compilerState, target); | |
| var recordUsedInputs = argResults.option('used-inputs-file') != null; | |
| var additionalDillModules = 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), | |
| additionalDillModules, | |
| target, | |
| 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 doneAdditionalDillModules = List.filled( | |
| summaryModules.length, | |
| dummyComponent, | |
| ); | |
| compilerState = await fe.initializeIncrementalCompiler( | |
| oldCompilerState, | |
| { | |
| 'trackCreationLocations=$trackCreationLocations', | |
| 'deprecatedJsInterop=$deprecatedJsInterop', | |
| 'multiRootScheme=${fileSystem.markerScheme}', | |
| 'multiRootRoots=${fileSystem.roots}', | |
| }, | |
| doneAdditionalDillModules, | |
| compileSdk, | |
| sourcePathToUri(getSdkPath()), | |
| compileSdk ? null : sourcePathToUri(sdkSummaryPath!), | |
| packageFile != null ? sourcePathToUri(packageFile) : null, | |
| sourcePathToUri(librarySpecPath), | |
| additionalDillModules, | |
| inputDigests, | |
| target, | |
| 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, | |
| doneAdditionalDillModules, | |
| 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 = <Future>[]; | |
| 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<Library, Component>.identity(); | |
| final summaryToModule = Map<Component, String>.identity(); | |
| for (var i = 0; i < result.additionalDillModules.length; i++) { | |
| var additionalDillModule = result.additionalDillModules[i]; | |
| var moduleImport = summaryModules[additionalDillModules[i]]!; | |
| for (var l in additionalDillModule.libraries) { | |
| assert(!importToSummary.containsKey(l)); | |
| importToSummary[l] = additionalDillModule; | |
| summaryToModule[additionalDillModule] = 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 `additionalDillModules`. | |
| if (!compileSdk) { | |
| for (var l in result.component.libraries) { | |
| // Don't override libraries already recorded in `additionalDillModules`. | |
| 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)); | |
| var encodedSourceMap = jsCode.encodedSourceMap; | |
| if (encodedSourceMap != null) { | |
| outFiles.add(File('$output.map').writeAsString('$encodedSourceMap\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 = <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.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<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.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 = <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)); | |
| var encodedSourceMap = jsCode.encodedSourceMap; | |
| if (encodedSourceMap != null) { | |
| outFiles.add(File('$output.map').writeAsString(encodedSourceMap)); | |
| } | |
| } | |
| 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-encoded source map, if generated, otherwise `null`. | |
| final String? encodedSourceMap; | |
| /// The decoded JSON of [encodedSourceMap], if generated, otherwise `null`. | |
| /// | |
| /// Decoded lazily on first access. Prefer [encodedSourceMap] to avoid | |
| /// unnecessary decoding. | |
| late final Map<String, Object?>? sourceMap = encodedSourceMap == null | |
| ? null | |
| : json.decode(encodedSourceMap!) as Map<String, Object?>; | |
| /// 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.encodedSourceMap, {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<String, Object?>? 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); | |
| // 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. | |
| 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 | |
| // attach it to the last library in the bundle. | |
| text = _replaceLast( | |
| text, | |
| LibraryCompiler.metricsLocationID, | |
| '${encodedMap.length}', | |
| ); | |
| } else { | |
| var compileTimeStatistics = { | |
| 'dartSize': _computeDartSize(component!), | |
| 'sourceMapSize': encodedMap.length, | |
| }; | |
| text = _replaceLast( | |
| text, | |
| ProgramCompiler.metricsLocationID, | |
| '$compileTimeStatistics', | |
| ); | |
| } | |
| var rawSourceMap = inlineSourceMap | |
| ? js.escapedString(encodedMap, "'").value | |
| : 'null'; | |
| text = _replaceLast(text, ProgramCompiler.sourceMapLocationID, rawSourceMap); | |
| 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 == null ? null : encodedMap, | |
| symbols: debugSymbols, | |
| metadata: debugMetadata, | |
| ); | |
| } | |
| /// Replaces the last occurrence of [from] with [to] in [text]. | |
| String _replaceLast(String text, String from, String to) { | |
| var index = text.lastIndexOf(from); | |
| if (index == -1) return text; | |
| return text.replaceRange(index, index + from.length, to); | |
| } | |
| /// 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 = <Variable, 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; | |
| } | |
| } | |
| /// Returns [state] if it can be reused when compiling with [target], or `null` | |
| /// otherwise. | |
| /// | |
| /// The CFE reuses non-incremental compiler state, including its target, | |
| /// without comparing targets. State created for a target with a different | |
| /// [DevCompilerTarget.deprecatedJsInterop] value would resolve `dart.library.*` | |
| /// conditions incorrectly, so it must be discarded. | |
| fe.InitializedCompilerState? _reusableCompilerState( | |
| fe.InitializedCompilerState? state, | |
| DevCompilerTarget target, | |
| ) { | |
| var oldTarget = state?.options.target; | |
| if (oldTarget is DevCompilerTarget && | |
| oldTarget.deprecatedJsInterop != target.deprecatedJsInterop) { | |
| return null; | |
| } | |
| return state; | |
| } | |
| /// 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; | |
| } | |
| /// 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<String, Object?> placeSourceMap( | |
| Map<String, Object?> 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; | |
| } |