Add a new dartdev compile verb.

Supports compiling to snapshots, JS, native executables, and kernel:

$ dart help compile
Compile Dart to various formats

Usage: dart compile <subcommand> [arguments]
-h, --help    Print this usage information.

Available subcommands:
  aot      Compile Dart to an AOT snapshot
  exe      Compile Dart to a self-contained executable
  jit      Compile Dart to a JIT snapshot
  js       Compile Dart to JavaScript
  kernel   Compile Dart to Kernel intermediate format

Change-Id: I1143f8dd0eec6f87a8271144f92dedc1fcf82923
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/146806
Commit-Queue: Michael Thomsen <mit@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
Reviewed-by: Jaime Wren <jwren@google.com>
This commit is contained in:
Michael Thomsen 2020-06-23 08:33:17 +00:00 committed by commit-bot@chromium.org
parent f008738abd
commit b54a95b0bb
11 changed files with 557 additions and 108 deletions

View file

@ -0,0 +1,13 @@
include: package:pedantic/analysis_options.1.8.0.yaml
analyzer:
errors:
# Increase the severity of several hints.
prefer_single_quotes: warning
unused_import: warning
linter:
rules:
- directives_ordering
- prefer_relative_imports
- prefer_single_quotes

View file

@ -6,83 +6,7 @@
import 'dart:io';
import 'package:args/args.dart';
import 'package:dart2native/dart2native.dart';
import 'package:path/path.dart' as path;
final String executableSuffix = Platform.isWindows ? '.exe' : '';
final String snapshotDir = path.dirname(Platform.script.toFilePath());
final String binDir = path.canonicalize(path.join(snapshotDir, '..'));
final String sdkDir = path.canonicalize(path.join(binDir, '..'));
final String dart = path.join(binDir, 'dart${executableSuffix}');
final String genKernel = path.join(snapshotDir, 'gen_kernel.dart.snapshot');
final String dartaotruntime =
path.join(binDir, 'dartaotruntime${executableSuffix}');
final String genSnapshot =
path.join(binDir, 'utils', 'gen_snapshot${executableSuffix}');
final String productPlatformDill =
path.join(sdkDir, 'lib', '_internal', 'vm_platform_strong_product.dill');
Future<void> generateNative(
Kind kind,
String sourceFile,
String outputFile,
String debugFile,
String packages,
List<String> defines,
bool enableAsserts,
bool verbose,
List<String> extraGenSnapshotOptions) async {
final Directory tempDir = Directory.systemTemp.createTempSync();
try {
final String kernelFile = path.join(tempDir.path, 'kernel.dill');
final String snapshotFile = (kind == Kind.aot
? outputFile
: path.join(tempDir.path, 'snapshot.aot'));
if (verbose) {
print('Generating AOT kernel dill.');
}
final kernelResult = await generateAotKernel(dart, genKernel,
productPlatformDill, sourceFile, kernelFile, packages, defines);
if (kernelResult.exitCode != 0) {
stderr.writeln(kernelResult.stdout);
stderr.writeln(kernelResult.stderr);
await stderr.flush();
throw 'Generating AOT kernel dill failed!';
}
if (verbose) {
print('Generating AOT snapshot.');
}
final snapshotResult = await generateAotSnapshot(genSnapshot, kernelFile,
snapshotFile, debugFile, enableAsserts, extraGenSnapshotOptions);
if (snapshotResult.exitCode != 0) {
stderr.writeln(snapshotResult.stdout);
stderr.writeln(snapshotResult.stderr);
await stderr.flush();
throw 'Generating AOT snapshot failed!';
}
if (kind == Kind.exe) {
if (verbose) {
print('Generating executable.');
}
await writeAppendedExecutable(dartaotruntime, snapshotFile, outputFile);
if (Platform.isLinux || Platform.isMacOS) {
if (verbose) {
print('Marking binary executable.');
}
await markExecutable(outputFile);
}
}
print('Generated: ${outputFile}');
} finally {
tempDir.deleteSync(recursive: true);
}
}
import 'package:dart2native/generate.dart';
void printUsage(final ArgParser parser) {
print('''
@ -95,7 +19,7 @@ Generates an executable or an AOT snapshot from <main-dart-file>.
Future<void> main(List<String> args) async {
// If we're outputting to a terminal, wrap usage text to that width.
int outputLineWidth = null;
int outputLineWidth;
try {
outputLineWidth = stdout.terminalColumns;
} catch (_) {/* Ignore. */}
@ -159,42 +83,25 @@ Remove debugging information from the output and save it separately to the speci
exit(1);
}
final Kind kind = {
'aot': Kind.aot,
'exe': Kind.exe,
}[parsedArgs['output-kind']];
final sourcePath = path.canonicalize(path.normalize(parsedArgs.rest[0]));
final sourceWithoutDart = sourcePath.replaceFirst(new RegExp(r'\.dart$'), '');
final outputPath =
path.canonicalize(path.normalize(parsedArgs['output'] != null
? parsedArgs['output']
: {
Kind.aot: '${sourceWithoutDart}.aot',
Kind.exe: '${sourceWithoutDart}.exe',
}[kind]));
final debugPath = parsedArgs['save-debugging-info'] != null
? path.canonicalize(path.normalize(parsedArgs['save-debugging-info']))
: null;
if (!FileSystemEntity.isFileSync(sourcePath)) {
final String sourceFile = parsedArgs.rest[0];
if (!FileSystemEntity.isFileSync(sourceFile)) {
stderr.writeln(
'"${sourcePath}" is not a file. See \'--help\' for more information.');
'"${sourceFile}" is not a file. See \'--help\' for more information.');
await stderr.flush();
exit(1);
}
try {
await generateNative(
kind,
sourcePath,
outputPath,
debugPath,
parsedArgs['packages'],
parsedArgs['define'],
parsedArgs['enable-asserts'],
parsedArgs['verbose'],
parsedArgs['extra-gen-snapshot-options']);
kind: parsedArgs['output-kind'],
sourceFile: sourceFile,
outputFile: parsedArgs['output'],
debugFile: parsedArgs['save-debugging-info'],
packages: parsedArgs['packages'],
defines: parsedArgs['define'],
enableAsserts: parsedArgs['enable-asserts'],
verbose: parsedArgs['verbose'],
extraOptions: parsedArgs['extra-gen-snapshot-options']);
} catch (e) {
stderr.writeln('Failed to generate native files:');
stderr.writeln(e);

View file

@ -21,7 +21,7 @@ Future writeAppendedExecutable(
final offset = dartaotruntimeLength + padding;
// Note: The offset is always Little Endian regardless of host.
final offsetBytes = new ByteData(8) // 64 bit in bytes.
final offsetBytes = ByteData(8) // 64 bit in bytes.
..setUint64(0, offset, Endian.little);
final outputFile = File(outputPath).openWrite();

View file

@ -0,0 +1,97 @@
// Copyright (c) 2020, 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:io';
import 'package:path/path.dart' as path;
import 'dart2native.dart';
final Directory binDir = File(Platform.resolvedExecutable).parent;
final String executableSuffix = Platform.isWindows ? '.exe' : '';
final String dartaotruntime =
path.join(binDir.path, 'dartaotruntime${executableSuffix}');
final String genKernel =
path.join(binDir.path, 'snapshots', 'gen_kernel.dart.snapshot');
final String genSnapshot =
path.join(binDir.path, 'utils', 'gen_snapshot${executableSuffix}');
final String productPlatformDill = path.join(
binDir.parent.path, 'lib', '_internal', 'vm_platform_strong_product.dill');
Future<void> generateNative({
String kind = 'exe',
String sourceFile,
String outputFile,
String debugFile,
String packages,
List<String> defines,
bool enableAsserts = false,
bool verbose = false,
List<String> extraOptions = const [],
}) async {
final Directory tempDir = Directory.systemTemp.createTempSync();
try {
final sourcePath = path.canonicalize(path.normalize(sourceFile));
final Kind outputKind = {
'aot': Kind.aot,
'exe': Kind.exe,
}[kind];
final sourceWithoutDart = sourcePath.replaceFirst(RegExp(r'\.dart$'), '');
final outputPath = path.canonicalize(path.normalize(outputFile != null
? outputFile
: {
Kind.aot: '${sourceWithoutDart}.aot',
Kind.exe: '${sourceWithoutDart}.exe',
}[outputKind]));
final debugPath =
debugFile != null ? path.canonicalize(path.normalize(debugFile)) : null;
if (verbose) {
print('Compiling $sourcePath to $outputPath using format $kind:');
print('Generating AOT kernel dill.');
}
final String kernelFile = path.join(tempDir.path, 'kernel.dill');
final kernelResult = await generateAotKernel(Platform.executable, genKernel,
productPlatformDill, sourcePath, kernelFile, packages, defines);
if (kernelResult.exitCode != 0) {
stderr.writeln(kernelResult.stdout);
stderr.writeln(kernelResult.stderr);
await stderr.flush();
throw 'Generating AOT kernel dill failed!';
}
if (verbose) {
print('Generating AOT snapshot.');
}
final String snapshotFile = (outputKind == Kind.aot
? outputPath
: path.join(tempDir.path, 'snapshot.aot'));
final snapshotResult = await generateAotSnapshot(genSnapshot, kernelFile,
snapshotFile, debugPath, enableAsserts, extraOptions);
if (snapshotResult.exitCode != 0) {
stderr.writeln(snapshotResult.stdout);
stderr.writeln(snapshotResult.stderr);
await stderr.flush();
throw 'Generating AOT snapshot failed!';
}
if (outputKind == Kind.exe) {
if (verbose) {
print('Generating executable.');
}
await writeAppendedExecutable(dartaotruntime, snapshotFile, outputPath);
if (Platform.isLinux || Platform.isMacOS) {
if (verbose) {
print('Marking binary executable.');
}
await markExecutable(outputPath);
}
}
print('Generated: ${outputPath}');
} finally {
tempDir.deleteSync(recursive: true);
}
}

View file

@ -43,6 +43,7 @@ linter:
- prefer_inlined_adds
- prefer_interpolation_to_compose_strings
- prefer_is_not_operator
- prefer_final_fields
- prefer_null_aware_operators
- prefer_relative_imports
- prefer_typing_uninitialized_variables

View file

@ -12,6 +12,7 @@ import 'package:usage/usage.dart';
import 'src/analytics.dart';
import 'src/commands/analyze.dart';
import 'src/commands/compile.dart';
import 'src/commands/create.dart';
import 'src/commands/format.dart';
import 'src/commands/pub.dart';
@ -160,6 +161,7 @@ class DartdevRunner<int> extends CommandRunner {
addCommand(AnalyzeCommand(verbose: verbose));
addCommand(CreateCommand(verbose: verbose));
addCommand(CompileCommand(verbose: verbose));
addCommand(FormatCommand(verbose: verbose));
addCommand(MigrateCommand(verbose: verbose));
addCommand(PubCommand(verbose: verbose));

View file

@ -0,0 +1,244 @@
// Copyright (c) 2020, 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:io';
import 'package:args/command_runner.dart';
import 'package:dart2native/generate.dart';
import 'package:path/path.dart' as path;
import '../core.dart';
import '../sdk.dart';
const int compileErrorExitCode = 64;
class Option {
final String flag;
final String help;
final String abbr;
Option({this.flag, this.help, this.abbr});
}
final Map<String, Option> commonOptions = {
'outputFile': Option(
flag: 'output',
abbr: 'o',
help: '''
Write the output to <file name>.
This can be an absolute or reletive path.
''',
),
};
bool checkFile(String sourcePath) {
if (!FileSystemEntity.isFileSync(sourcePath)) {
stderr.writeln(
'"$sourcePath" is not a file. See \'--help\' for more information.');
stderr.flush();
return false;
}
return true;
}
class CompileJSCommand extends DartdevCommand<int> {
bool verbose = false;
CompileJSCommand({
this.verbose,
}) : super('js', 'Compile Dart to JavaScript') {
argParser
..addOption(
commonOptions['outputFile'].flag,
help: commonOptions['outputFile'].help,
abbr: commonOptions['outputFile'].abbr,
)
..addFlag(
'minified',
help: 'Generate minified output.',
abbr: 'm',
negatable: false,
);
}
@override
String get invocation => '${super.invocation} <dart entry point>';
@override
FutureOr<int> run() async {
// We expect a single rest argument; the dart entry point.
if (argResults.rest.length != 1) {
log.stderr('Missing Dart entry point.');
printUsage();
return compileErrorExitCode;
}
final String sourcePath = argResults.rest[0];
if (!checkFile(sourcePath)) {
return -1;
}
final process = await startProcess(sdk.dart2js, argResults.arguments);
routeToStdout(process);
return process.exitCode;
}
}
class CompileSnapshotCommand extends DartdevCommand<int> {
bool verbose = false;
final String commandName;
final String help;
final String fileExt;
final String formatName;
CompileSnapshotCommand({
this.verbose,
this.commandName,
this.help,
this.fileExt,
this.formatName,
}) : super(commandName, 'Compile Dart $help') {
argParser
..addOption(
commonOptions['outputFile'].flag,
help: commonOptions['outputFile'].help,
abbr: commonOptions['outputFile'].abbr,
);
}
@override
String get invocation => '${super.invocation} <dart entry point>';
@override
FutureOr<int> run() async {
// We expect a single rest argument; the dart entry point.
if (argResults.rest.length != 1) {
log.stderr('Missing Dart entry point.');
printUsage();
return compileErrorExitCode;
}
final String sourcePath = argResults.rest[0];
if (!checkFile(sourcePath)) {
return -1;
}
// Determine output file name.
String outputFile = argResults[commonOptions['outputFile'].flag];
if (outputFile == null) {
final inputWithoutDart = sourcePath.replaceFirst(RegExp(r'\.dart$'), '');
outputFile = '$inputWithoutDart.$fileExt';
}
// Build arguments.
List<String> args = [];
args.add('--snapshot-kind=$formatName');
args.add('--snapshot=${path.canonicalize(outputFile)}');
if (verbose) {
args.add('-v');
}
args.add(path.canonicalize(sourcePath));
log.stdout('Compiling $sourcePath to $commandName file $outputFile.');
final process = await startProcess(sdk.dart, args);
routeToStdout(process);
return process.exitCode;
}
}
class CompileNativeCommand extends DartdevCommand<int> {
bool verbose = false;
final String commandName;
final String format;
final String help;
CompileNativeCommand({
this.verbose,
this.commandName,
this.format,
this.help,
}) : super(commandName, 'Compile Dart $help') {
argParser
..addOption(
commonOptions['outputFile'].flag,
help: commonOptions['outputFile'].help,
abbr: commonOptions['outputFile'].abbr,
)
..addMultiOption('define', abbr: 'D', valueHelp: 'key=value', help: '''
Set values of environment variables. To specify multiple variables, use multiple options or use commas to separate key-value pairs.
E.g.: dart2native -Da=1,b=2 main.dart''')
..addFlag('enable-asserts',
negatable: false, help: 'Enable assert statements.')
..addOption('packages', abbr: 'p', valueHelp: 'path', help: '''
Get package locations from the specified file instead of .packages. <path> can be relative or absolute.
E.g.: dart2native --packages=/tmp/pkgs main.dart
''')
..addOption('save-debugging-info', abbr: 'S', valueHelp: 'path', help: '''
Remove debugging information from the output and save it separately to the specified file. <path> can be relative or absolute.
''');
}
@override
String get invocation => '${super.invocation} <dart entry point>';
@override
FutureOr<int> run() async {
// We expect a single rest argument; the dart entry point.
if (argResults.rest.length != 1) {
log.stderr('Missing Dart entry point.');
printUsage();
return compileErrorExitCode;
}
final String sourcePath = argResults.rest[0];
if (!checkFile(sourcePath)) {
return -1;
}
try {
await generateNative(
kind: format,
sourceFile: sourcePath,
outputFile: argResults['output'],
defines: argResults['define'],
packages: argResults['packages'],
enableAsserts: argResults['enable-asserts'],
debugFile: argResults['save-debugging-info'],
verbose: verbose,
);
return 0;
} catch (e) {
log.stderr('Error: AOT compilation failed');
log.stderr(e);
return compileErrorExitCode;
}
}
}
class CompileCommand extends Command {
@override
String get description => 'Compile Dart to various formats.';
@override
String get name => 'compile';
CompileCommand({bool verbose = false}) {
addSubcommand(CompileJSCommand());
addSubcommand(CompileSnapshotCommand(
commandName: 'jit-snapshot',
help: 'to a JIT snapshot',
fileExt: 'jit',
formatName: 'app-jit',
verbose: verbose,
));
addSubcommand(CompileNativeCommand(
commandName: 'exe',
help: 'to a self-contained executable',
format: 'exe',
verbose: verbose,
));
addSubcommand(CompileNativeCommand(
commandName: 'aot-snapshot',
help: 'to an AOT snapshot',
format: 'aot',
verbose: verbose,
));
}
}

View file

@ -48,6 +48,8 @@ class Sdk {
String get analysisServerSnapshot => path.absolute(
sdkPath, 'bin', 'snapshots', 'analysis_server.dart.snapshot');
String get dart2js => path.absolute(sdkPath, 'bin', _binName('dart2js'));
String get dartfmt => path.absolute(sdkPath, 'bin', _binName('dartfmt'));
String get pub => path.absolute(sdkPath, 'bin', _binName('pub'));

View file

@ -0,0 +1,180 @@
// Copyright (c) 2020, 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:io';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
import '../utils.dart';
const int compileErrorExitCode = 64;
void main() {
group('compile', defineCompileTests, timeout: longTimeout);
}
void defineCompileTests() {
// *** NOTE ***: These tests *must* be run with the `--use-sdk` option
// as they depend on a fully built SDK to resolve various snapshot files
// used by compilation.
test('Running from built SDK', () {
final Directory binDir = File(Platform.resolvedExecutable).parent;
expect(binDir.path, contains('bin'));
});
test('Implicit --help', () {
final p = project();
var result = p.runSync(
'compile',
[],
);
expect(result.stderr, contains('Compile Dart'));
expect(result.exitCode, compileErrorExitCode);
});
test('--help', () {
final p = project();
final result = p.runSync(
'compile',
['--help'],
);
expect(result.stdout, contains('Compile Dart'));
expect(result.exitCode, 0);
});
test('Compile and run jit snapshot', () {
final p = project(mainSrc: 'void main() { print("I love jit"); }');
final outFile = path.join(p.dirPath, 'main.jit');
var result = p.runSync(
'compile',
[
'jit-snapshot',
'-o',
outFile,
p.relativeFilePath,
],
);
expect(File(outFile).existsSync(), true,
reason: 'File not found: $outFile');
expect(result.stderr, isEmpty);
expect(result.exitCode, 0);
result = p.runSync('run', ['main.jit']);
expect(result.stdout, contains('I love jit'));
expect(result.stderr, isEmpty);
expect(result.exitCode, 0);
});
test('Compile and run executable', () {
final p = project(mainSrc: 'void main() { print("I love executables"); }');
final inFile = path.canonicalize(path.join(p.dirPath, p.relativeFilePath));
final outFile = path.canonicalize(path.join(p.dirPath, 'lib', 'main.exe'));
var result = p.runSync(
'compile',
[
'exe',
inFile,
],
);
expect(File(outFile).existsSync(), true,
reason: 'File not found: $outFile');
expect(result.stderr, isEmpty);
expect(result.exitCode, 0);
result = Process.runSync(
outFile,
[],
);
expect(result.stdout, contains('I love executables'));
expect(result.stderr, isEmpty);
expect(result.exitCode, 0);
});
test('Compile and run executable with options', () {
final p = project(
mainSrc: 'void main() {print(const String.fromEnvironment("life"));}');
final inFile = path.canonicalize(path.join(p.dirPath, p.relativeFilePath));
final outFile = path.canonicalize(path.join(p.dirPath, 'myexe'));
var result = p.runSync(
'compile',
[
'exe',
'--define',
'life=42',
'-o',
outFile,
inFile,
],
);
expect(File(outFile).existsSync(), true,
reason: 'File not found: $outFile');
expect(result.stderr, isEmpty);
expect(result.exitCode, 0);
result = Process.runSync(
outFile,
[],
);
expect(result.stdout, contains('42'));
expect(result.stderr, isEmpty);
expect(result.exitCode, 0);
});
test('Compile and run aot snapshot', () {
final p = project(mainSrc: 'void main() { print("I love AOT"); }');
final inFile = path.canonicalize(path.join(p.dirPath, p.relativeFilePath));
final outFile = path.canonicalize(path.join(p.dirPath, 'main.aot'));
var result = p.runSync(
'compile',
[
'aot-snapshot',
'-o',
'main.aot',
inFile,
],
);
expect(File(outFile).existsSync(), true,
reason: 'File not found: $outFile');
expect(result.stderr, isEmpty);
expect(result.exitCode, 0);
final Directory binDir = File(Platform.resolvedExecutable).parent;
result = Process.runSync(
path.join(binDir.path, 'dartaotruntime'),
[outFile],
);
expect(result.stdout, contains('I love AOT'));
expect(result.stderr, isEmpty);
expect(result.exitCode, 0);
});
test('Compile JS', () {
final p = project(mainSrc: "void main() { print('Hello from JS'); }");
final inFile = path.canonicalize(path.join(p.dirPath, p.relativeFilePath));
final outFile = path.canonicalize(path.join(p.dirPath, 'main.js'));
final result = p.runSync('compile', [
'js',
'-m',
'-o',
outFile,
'-v',
inFile,
]);
expect(File(outFile).existsSync(), true,
reason: 'File not found: $outFile');
expect(result.stderr, isEmpty);
expect(result.exitCode, 0);
});
}

View file

@ -57,6 +57,7 @@ void help() {
expect(result.stdout, contains('Available commands:'));
expect(result.stdout, contains('analyze '));
expect(result.stdout, contains('create '));
expect(result.stdout, contains('compile '));
expect(result.stdout, contains('format '));
expect(result.stdout, contains('migrate '));
});

View file

@ -6,6 +6,7 @@ import 'package:test/test.dart';
import 'analytics_test.dart' as analytics;
import 'commands/analyze_test.dart' as analyze;
import 'commands/compile_test.dart' as compile;
import 'commands/create_test.dart' as create;
import 'commands/flag_test.dart' as flag;
import 'commands/format_test.dart' as format;
@ -29,6 +30,7 @@ void main() {
migrate.main();
pub.main();
run.main();
compile.main();
test.main();
core.main();
sdk.main();