#!/usr/bin/env dart // Copyright (c) 2018, 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. /// Compiles code with DDC and runs the resulting code with either node or /// chrome. /// /// The first script supplied should be the one with `main()`. /// /// Saves the output in the same directory as the sources for convenient /// inspection, modification or rerunning the code. import 'dart:io'; import 'dart:ffi'; import 'package:args/args.dart' show ArgParser; import 'package:dev_compiler/src/compiler/js_names.dart' as js_names; import 'package:path/path.dart' as p; enum NullSafety { strict, weak, disabled } void main(List args) async { void printUsage() { print('Usage: ddb [options] \n'); print('Compiles with the dev_compiler and runs it on a ' 'JS platform.\n' 'Optionally, multiple Dart source files can be passed as arguments and ' 'they will be compiled into separate modules in the order provided. ' 'The order will be treated as an import DAG with the last file as the ' 'application entrypoint. The .dill file outputs from each compile will ' 'be collected and passed as dependency summaries for the next compile ' 'step.'); } // Parse flags. var parser = ArgParser(usageLineLength: 80) ..addOption('arch', abbr: 'a', help: "Target platform's machine architecture.", allowed: ['x64', 'arm64']) ..addOption('binary', abbr: 'b', help: 'Runtime binary path.') ..addOption('compile-vm-options', help: 'DART_VM_OPTIONS for the compilation VM.') ..addFlag('debug', abbr: 'd', help: 'Use current source instead of built SDK.', defaultsTo: false) ..addMultiOption('enable-experiment', help: 'Run with specified experiments enabled.') ..addFlag('help', abbr: 'h', help: 'Display this message.') ..addOption('mode', help: 'Option to (compile|run|all). Default is all (compile and run).', allowed: ['compile', 'run', 'all'], defaultsTo: 'all') ..addFlag('emit-debug-symbols', help: 'Pass through flag for DDC, emits debug symbols file along with ' 'the compiled module.', defaultsTo: false) ..addFlag('native-null-assertions', help: 'Run with assertions on non-nullable values returned from native ' 'APIs.', defaultsTo: true) ..addFlag('interop-null-assertions', help: 'Run with assertions on non-nullable values returned from ' 'non-static JavaScript interop APIs.', defaultsTo: true) ..addFlag('ddc-modules', help: 'Emit modules in the ddc format. Currently supported in Chrome ' '(amd modules by default) and D8 (already uses ddc modules by ' 'default).', defaultsTo: false) ..addFlag('canary', help: 'Enable all compiler features under active development.', defaultsTo: false) ..addFlag('observe', help: 'Run the compiler in the Dart VM with --observe. Implies --debug.', defaultsTo: false) ..addOption('out', help: 'Output file.') ..addOption('packages', help: 'Where to find a package spec file.') ..addOption('port', abbr: 'p', help: 'Run with the corresponding chrome/V8 debugging port open.', defaultsTo: '9222') ..addOption('runtime', abbr: 'r', help: 'Platform to run on (node|d8|chrome). Default is node.', allowed: ['node', 'd8', 'chrome'], defaultsTo: 'node') ..addFlag('summarize-text', help: 'Emit API summary in a .js.txt file.', defaultsTo: false) ..addMultiOption('summary', abbr: 's', help: 'summary file(s) of imported libraries, optionally with module ' 'import path: -s path.sum=js/import/path') ..addFlag('verbose', abbr: 'v', help: 'Echos the commands, arguments, and environment this script is ' 'running.', defaultsTo: false) ..addOption('vm-service-port', help: 'Specify the observatory port. Implied --observe.'); var parsedArgs = parser.parse(args); if (parsedArgs.flag('help')) { printUsage(); print('Available options:'); print(parser.usage); exit(0); } if (parsedArgs.rest.isEmpty) { print('Dart script file required.\n'); printUsage(); exit(1); } var arch = parsedArgs.option('arch'); var debug = parsedArgs.flag('debug') || parsedArgs.flag('observe') || parsedArgs.wasParsed('vm-service-port'); var summarizeText = parsedArgs.flag('summarize-text'); var binary = parsedArgs.option('binary'); var experiments = parsedArgs.multiOption('enable-experiment'); var summaries = parsedArgs.multiOption('summary'); var port = int.parse(parsedArgs.option('port')!); var mode = parsedArgs.option('mode')!; var compile = mode == 'compile' || mode == 'all'; var run = mode == 'run' || mode == 'all'; var verbose = parsedArgs.flag('verbose'); var emitDebugSymbols = parsedArgs.flag('emit-debug-symbols'); var nativeNonNullAsserts = parsedArgs.flag('native-null-assertions'); var jsInteropNonNullAsserts = parsedArgs.flag('interop-null-assertions'); var ddcModules = parsedArgs.flag('ddc-modules'); var canaryFeatures = parsedArgs.flag('canary'); var sourceFiles = parsedArgs.rest.map(p.canonicalize); var entryPoint = sourceFiles.last; var libRoot = p.dirname(entryPoint); var basename = p.basenameWithoutExtension(entryPoint); var libname = pathToLibName(entryPoint); // This is used in the DDC module system and usually corresponds to the // entrypoint's path. var appname = p.relative(p.withoutExtension(entryPoint)); // This is used in the DDC module system for multiapp workflows and is // stubbed in ddb. var uuid = '00000000-0000-0000-0000-000000000000'; // By default (no `-d`), we use the `dartdevc` binary on the user's path to // compute the SDK we use for execution. I.e., we assume that `dart` is // under `$DART_SDK/bin/dart` and use that to find `dartdevc` and related // artifacts. In this mode, this script can run against any installed SDK. // If you want to run against a freshly built SDK, that must be first on // your path. var dartBinary = Platform.resolvedExecutable; var dartSdk = p.dirname(p.dirname(dartBinary)); var dartAotBinary = p.absolute(dartSdk, 'bin', Platform.isWindows ? 'dartaotruntime.exe' : 'dartaotruntime'); var execBinary = debug ? dartBinary : dartAotBinary; // In debug mode (`-d`), we run from the `pkg/dev_compiler` sources. We // determine the location via this actual script (i.e., `-d` assumes // this script remains under to `tool` sub-directory). var toolPath = Platform.script.normalizePath().toFilePath(windows: Platform.isWindows); var ddcPath = p.dirname(p.dirname(toolPath)); var dartCheckoutPath = p.dirname(p.dirname(ddcPath)); /// Runs the [command] with [args] in [environment]. /// /// Will echo the commands to the console before running them when running in /// `verbose` mode. Future startProcess(String name, String command, List args, [Map environment = const {}]) { if (verbose) { print('Running $name:\n$command ${args.join(' ')}\n'); if (environment.isNotEmpty) { var environmentVariables = environment.entries.map((e) => '${e.key}: ${e.value}').join('\n'); print('With Environment:\n$environmentVariables\n'); } } return Process.start(command, args, mode: ProcessStartMode.inheritStdio, environment: environment); } Future runDdc(List ddcArgs) async { var observe = parsedArgs.wasParsed('vm-service-port') || parsedArgs.flag('observe'); var vmServicePort = parsedArgs.wasParsed('vm-service-port') ? '=${parsedArgs.option('vm-service-port')!}' : ''; var vmOptions = parsedArgs.option('compile-vm-options'); var args = [ ...?vmOptions?.split(' '), if (debug) ...[ if (observe) ...[ '--enable-vm-service$vmServicePort', '--pause-isolates-on-start', ], '--enable-asserts', // Use unbuilt script. This only works from a source checkout. p.join(ddcPath, 'bin', 'dartdevc.dart'), ] else // Use built snapshot. p.join(dartSdk, 'bin', 'snapshots', 'dartdevc_aot.dart.snapshot'), ...ddcArgs, ]; var process = await startProcess('DDC', execBinary, args); if (await process.exitCode != 0) exit(await process.exitCode); } String mod; var chrome = false; var node = false; var d8 = false; var runtime = parsedArgs.option('runtime'); switch (runtime) { case 'node': // TODO(nshahan): Cleanup after the ddc module format is used everywhere. if (ddcModules) { throw ('The combination of `--runtime=$runtime` and `--ddc-modules` ' 'is not supported at this time.'); } node = true; mod = 'common'; break; case 'd8': d8 = true; mod = 'ddc'; break; case 'chrome': chrome = true; // TODO(nshahan): Cleanup after the ddc module format is used everywhere. mod = ddcModules ? 'ddc' : 'amd'; break; default: throw Exception('Unexpected runtime: $runtime'); } var sdkRoot = p.dirname(p.dirname(ddcPath)); var buildDir = resolveBuildDir(sdkRoot, arch); var requirePath = p.join(sdkRoot, 'third_party', 'requirejs'); var sdkOutlineDill = p.join(buildDir, 'ddc_outline.dill'); var compileModeDir = canaryFeatures ? 'canary' : 'stable'; var sdkJsPath = p.join(buildDir, 'gen', 'utils', 'ddc', compileModeDir, 'sdk', mod); var preamblesDir = p.join( sdkRoot, 'sdk', 'lib', '_internal', 'js_runtime', 'lib', 'preambles'); // Print an initial empty line to separate the invocation from the output. if (verbose) { print(''); } var outputs = []; if (compile) { var packages = parsedArgs.option('packages'); var ddcArgs = [ if (summarizeText) '--summarize-text', '--modules=$mod', '--dart-sdk-summary=$sdkOutlineDill', for (var summary in summaries) '--summary=$summary', for (var experiment in experiments) '--enable-experiment=$experiment', if (packages != null) '--packages=$packages', if (emitDebugSymbols) '--emit-debug-symbols', if (canaryFeatures) '--canary', // Provide predictable library URIs for all libraries when using the // new ddc module format. if (canaryFeatures) '--multi-root-scheme=ddb', if (canaryFeatures) '--multi-root=/', ]; var summaryFiles = []; for (var sourceFile in sourceFiles) { var out = p.setExtension(sourceFile, '.js'); var requestArgs = [ for (var summary in summaryFiles) '--summary=$summary', '-o', out, if (canaryFeatures) 'ddb:$sourceFile' else sourceFile, ]; await runDdc([...ddcArgs, ...requestArgs]); outputs.add(out); var summaryFile = p.setExtension(out, '.dill'); var libname = canaryFeatures ? 'ddb:$sourceFile' : js_names.pathToJSIdentifier(p.basenameWithoutExtension(sourceFile)); summaryFiles.add('$summaryFile=$libname'); } } if (run) { var ddcModuleSdkOptions = canaryFeatures ? '''let sdkOptions = { nativeNonNullAsserts: $nativeNonNullAsserts, jsInteropNonNullAsserts: $jsInteropNonNullAsserts, };''' : ''' let sdk = dart_library.import("dart_sdk", "$appname"); sdk.dart.nativeNonNullAsserts($nativeNonNullAsserts); sdk.dart.jsInteropNonNullAsserts($jsInteropNonNullAsserts); '''; var ddcModuleRunMain = canaryFeatures ? 'dartDevEmbedder.runMain("ddb:$entryPoint", sdkOptions);' : 'dart_library.start(' '"$appname", "$uuid", "$basename", "$libname", false);'; if (chrome) { String chromeBinary; if (binary != null) { chromeBinary = binary; } else if (Platform.isWindows) { chromeBinary = 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'; } else if (Platform.isMacOS) { chromeBinary = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; } else { // Assume Linux chromeBinary = 'google-chrome'; } var amdModulePaths = [ for (var output in outputs) '"${pathToLibName(output)}": "${p.withoutExtension(output)}"' ].join(',\n '); // Seal the native JavaScript Object prototype to avoid pollution before // loading the Dart SDK module. var amdModulesHtml = ''' '''; var ddcModuleScriptTags = [ for (var output in outputs) '' ].join('\n'); var ddcModulesHtml = ''' $ddcModuleScriptTags '''; var htmlFile = p.setExtension(entryPoint, '.html'); File(htmlFile) .writeAsStringSync(ddcModules ? ddcModulesHtml : amdModulesHtml); var tmp = p.join(Directory.systemTemp.path, 'ddc'); var process = await startProcess('Chrome', chromeBinary, [ '--auto-open-devtools-for-tabs', '--allow-file-access-from-files', '--remote-debugging-port=$port', '--user-data-dir=$tmp', htmlFile ]); if (await process.exitCode != 0) exit(await process.exitCode); } else if (node) { var nodePath = '$sdkJsPath:$libRoot'; var runjs = ''' // Seal the native JavaScript Object prototype to avoid pollution before // loading the Dart SDK module. delete Object.prototype.__proto__; Object.seal(Object.prototype); let source_maps; try { source_maps = require('source-map-support'); source_maps.install(); } catch(e) { } let sdk = require(\"dart_sdk\"); // Create a self reference for JS interop tests that set fields on self. sdk.dart.global.self = sdk.dart.global; let main = require(\"./$basename\").$libname.main; try { sdk.dart.nativeNonNullAsserts($nativeNonNullAsserts); sdk.dart.jsInteropNonNullAsserts($jsInteropNonNullAsserts); sdk._isolate_helper.startRootIsolate(main, []); } catch(e) { if (!source_maps) { console.log('For Dart source maps: npm install source-map-support'); } sdk.core.print(sdk.dart.stackTrace(e)); process.exit(1); } '''; var nodeFile = p.setExtension(entryPoint, '.run.js'); File(nodeFile).writeAsStringSync(runjs); var nodeBinary = binary ?? 'node'; var process = await startProcess('Node', nodeBinary, ['--inspect=localhost:$port', nodeFile], {'NODE_PATH': nodePath}); if (await process.exitCode != 0) exit(await process.exitCode); } else if (d8) { var ddcModuleScriptTags = [for (var output in outputs) 'load("$output");'].join('\n'); var ddcModuleAssignGlobalSelf = canaryFeatures ? ''' let dart = dartDevEmbedder.importLibrary("dart:_runtime"); dart.global.self = dart.global;''' : 'sdk.dart.global.self = sdk.dart.global;'; var runjs = ''' load("$ddcPath/lib/js/ddc/ddc_module_loader.js"); load("$sdkJsPath/dart_sdk.js"); $ddcModuleScriptTags $ddcModuleSdkOptions // Create a self reference for JS interop tests that set fields on self. $ddcModuleAssignGlobalSelf // Invoke main through the d8 preamble to ensure the code is running // within the fake event loop. self.dartMainRunner(function () { $ddcModuleRunMain }); '''; var d8File = p.setExtension(entryPoint, '.d8.js'); File(d8File).writeAsStringSync(runjs); var d8Binary = binary ?? p.join(dartCheckoutPath, _d8executable); var process = await startProcess('D8', d8Binary, [ p.join(preamblesDir, 'seal_native_object.js'), p.join(preamblesDir, 'd8.js'), d8File ]); if (await process.exitCode != 0) exit(await process.exitCode); } } } /// Converts a path to the expected default library name DDC will use. String pathToLibName(String path) => js_names.pathToJSIdentifier(p.withoutExtension( p.isWithin(p.current, path) ? p.relative(path) : p.absolute(path))); String get _d8executable { final arch = Abi.current().toString().split('_')[1]; if (Platform.isWindows) { return p.join('third_party', 'd8', 'windows', arch, 'd8.exe'); } else if (Platform.isLinux) { return p.join('third_party', 'd8', 'linux', arch, 'd8'); } else if (Platform.isMacOS) { return p.join('third_party', 'd8', 'macos', arch, 'd8'); } throw UnsupportedError('Unsupported platform.'); } /// Maps supported unames to supported architectures. final _resolvedUnames = { 'arm64': 'arm64', 'x86_64': 'x64', 'x64': 'x64', }; /// Returns the location of the Dart SDK's build directory. String resolveBuildDir(String sdkRoot, String? architecture) { String platformString; String archString; if (Platform.isMacOS) { platformString = 'xcodebuild'; var resolvedArchitecture = architecture; if (architecture == null) { var result = Process.runSync('uname', ['-m']).stdout as String; final uname = result.trim(); resolvedArchitecture = _resolvedUnames[uname] ?? uname; } if (resolvedArchitecture == 'x64') { archString = 'ReleaseX64'; } else if (resolvedArchitecture == 'arm64') { archString = 'ReleaseARM64'; } else { throw UnsupportedError( 'DDB does not currently support the architecture $architecture ' 'on MacOS.'); } } else if (Platform.isLinux || Platform.isWindows) { platformString = 'out'; if (architecture != null && architecture != 'x64') { throw UnsupportedError( 'DDB does not currently support the architecture $architecture ' 'on Windows or Linux.'); } archString = 'ReleaseX64'; } else { throw UnsupportedError('Unsupported platform.'); } return p.join(sdkRoot, platformString, archString); }