blob: ac95cce611a45bc8ea27aa073411bc3538f3b891 [file] [log] [blame]
// Copyright (c) 2019, 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.
// @dart = 2.10
/// Test the modular compilation pipeline of dart2js.
///
/// This is a shell that runs multiple tests, one per folder under `data/`.
import 'dart:io';
import 'dart:async';
import 'package:compiler/src/commandline_options.dart';
import 'package:compiler/src/kernel/dart2js_target.dart';
import 'package:front_end/src/compute_platform_binaries_location.dart'
show computePlatformBinariesLocation;
import 'package:modular_test/src/create_package_config.dart';
import 'package:modular_test/src/io_pipeline.dart';
import 'package:modular_test/src/pipeline.dart';
import 'package:modular_test/src/runner.dart';
import 'package:modular_test/src/suite.dart';
String packageConfigJsonPath = ".dart_tool/package_config.json";
Uri sdkRoot = Platform.script.resolve("../../../");
Uri packageConfigUri = sdkRoot.resolve(packageConfigJsonPath);
Options _options;
String _dart2jsScript;
String _kernelWorkerScript;
const dillSummaryId = DataId("summary.dill");
const dillId = DataId("full.dill");
const fullDillId = DataId("concatenate.dill");
const modularUpdatedDillId = DataId("modular.dill");
const modularDataId = DataId("modular.data");
const modularFullDataId = DataId("concatenate.modular.data");
const closedWorldId = DataId("world");
const globalUpdatedDillId = DataId("global.dill");
const globalDataId = DataId("global.data");
const codeId = ShardsDataId("code", 2);
const codeId0 = ShardDataId(codeId, 0);
const codeId1 = ShardDataId(codeId, 1);
const jsId = DataId("js");
const txtId = DataId("txt");
const fakeRoot = 'dev-dart-app:/';
String getRootScheme(Module module) {
// We use non file-URI schemes for representeing source locations in a
// root-agnostic way. This allows us to refer to file across modules and
// across steps without exposing the underlying temporary folders that are
// created by the framework. In build systems like bazel this is especially
// important because each step may be run on a different machine.
//
// Files in packages are defined in terms of `package:` URIs, while
// non-package URIs are defined using the `dart-dev-app` scheme.
return module.isSdk ? 'dart-dev-sdk' : 'dev-dart-app';
}
String sourceToImportUri(Module module, Uri relativeUri) {
if (module.isPackage) {
var basePath = module.packageBase.path;
var packageRelativePath = basePath == "./"
? relativeUri.path
: relativeUri.path.substring(basePath.length);
return 'package:${module.name}/$packageRelativePath';
} else {
return '${getRootScheme(module)}:/$relativeUri';
}
}
List<String> getSources(Module module) {
return module.sources.map((uri) => sourceToImportUri(module, uri)).toList();
}
abstract class CFEStep extends IOModularStep {
final String stepName;
CFEStep(this.stepName, this.onlyOnSdk);
@override
bool get needsSources => true;
@override
bool get onlyOnMain => false;
@override
final bool onlyOnSdk;
@override
Future<void> execute(Module module, Uri root, ModuleDataToRelativeUri toUri,
List<String> flags) async {
if (_options.verbose) print("\nstep: $stepName on $module");
Set<Module> transitiveDependencies = computeTransitiveDependencies(module);
await writePackageConfig(module, transitiveDependencies, root);
String rootScheme = getRootScheme(module);
List<String> sources;
List<String> extraArgs = [
'--packages-file',
'$rootScheme:/$packageConfigJsonPath'
];
if (module.isSdk) {
// When no flags are passed, we can skip compilation and reuse the
// platform.dill created by build.py.
if (flags.isEmpty) {
var platform = computePlatformBinariesLocation()
.resolve("dart2js_platform_unsound.dill");
var destination = root.resolveUri(toUri(module, outputData));
if (_options.verbose) {
print('command:\ncp $platform $destination');
}
await File.fromUri(platform).copy(destination.toFilePath());
return;
}
sources = requiredLibraries['dart2js'] + ['dart:core'];
extraArgs += [
'--libraries-file',
'$rootScheme:///sdk/lib/libraries.json'
];
assert(transitiveDependencies.isEmpty);
} else {
sources = getSources(module);
}
// TODO(joshualitt): Ensure the kernel worker has some way to specify
// --no-sound-null-safety
List<String> args = [
_kernelWorkerScript,
...stepArguments,
'--exclude-non-sources',
'--multi-root',
'$root',
'--multi-root-scheme',
rootScheme,
...extraArgs,
'--output',
'${toUri(module, outputData)}',
...(transitiveDependencies
.expand((m) => ['--input-summary', '${toUri(m, inputData)}'])),
...(sources.expand((String uri) => ['--source', uri])),
...(flags.expand((String flag) => ['--enable-experiment', flag])),
];
var result =
await _runProcess(Platform.resolvedExecutable, args, root.toFilePath());
_checkExitCode(result, this, module);
}
List<String> get stepArguments;
DataId get inputData;
DataId get outputData;
@override
void notifyCached(Module module) {
if (_options.verbose) print("\ncached step: $stepName on $module");
}
}
// Step that compiles sources in a module to a summary .dill file.
class OutlineDillCompilationStep extends CFEStep {
@override
List<DataId> get resultData => const [dillSummaryId];
@override
bool get needsSources => true;
@override
List<DataId> get dependencyDataNeeded => const [dillSummaryId];
@override
List<DataId> get moduleDataNeeded => const [];
@override
List<String> get stepArguments =>
['--target', 'dart2js_summary', '--summary-only'];
@override
DataId get inputData => dillSummaryId;
@override
DataId get outputData => dillSummaryId;
OutlineDillCompilationStep() : super('outline-dill-compilation', false);
}
// Step that compiles sources in a module to a .dill file.
class FullDillCompilationStep extends CFEStep {
@override
List<DataId> get resultData => const [dillId];
@override
bool get needsSources => true;
@override
List<DataId> get dependencyDataNeeded => const [dillSummaryId];
@override
List<DataId> get moduleDataNeeded => const [];
@override
List<String> get stepArguments =>
['--target', 'dart2js', '--no-summary', '--no-summary-only'];
@override
DataId get inputData => dillSummaryId;
@override
DataId get outputData => dillId;
FullDillCompilationStep({bool onlyOnSdk = false})
: super('full-dill-compilation', onlyOnSdk);
}
class ModularAnalysisStep extends IOModularStep {
@override
List<DataId> get resultData => [modularDataId, modularUpdatedDillId];
@override
bool get needsSources => !onlyOnSdk;
/// The SDK has no dependencies, and for all other modules we only need
/// summaries.
@override
List<DataId> get dependencyDataNeeded => [dillSummaryId];
/// All non SDK modules only need sources for module data.
@override
List<DataId> get moduleDataNeeded => onlyOnSdk ? [dillId] : const [];
@override
bool get onlyOnMain => false;
@override
final bool onlyOnSdk;
@override
bool get notOnSdk => !onlyOnSdk;
// TODO(joshualitt): We currently special case the SDK both because it is not
// trivial to build it in the same fashion as other modules, and because it is
// a special case in other build environments. Eventually, we should
// standardize this a bit more and always build the SDK modularly, if we have
// to build it.
ModularAnalysisStep({this.onlyOnSdk = false});
@override
Future<void> execute(Module module, Uri root, ModuleDataToRelativeUri toUri,
List<String> flags) async {
if (_options.verbose) print("\nstep: modular analysis on $module");
Set<Module> transitiveDependencies = computeTransitiveDependencies(module);
List<String> dillDependencies = [];
List<String> sources = [];
List<String> extraArgs = [];
if (!module.isSdk) {
await writePackageConfig(module, transitiveDependencies, root);
String rootScheme = getRootScheme(module);
sources = getSources(module);
dillDependencies = transitiveDependencies
.map((m) => '${toUri(m, dillSummaryId)}')
.toList();
extraArgs = [
'--packages=${root.resolve(packageConfigJsonPath)}',
'--multi-root=$root',
'--multi-root-scheme=$rootScheme',
];
}
List<String> args = [
'--packages=${sdkRoot.toFilePath()}/$packageConfigJsonPath',
_dart2jsScript,
'--no-sound-null-safety',
if (_options.useSdk) '--libraries-spec=$_librarySpecForSnapshot',
// If we have sources, then we aren't building the SDK, otherwise we
// assume we are building the sdk and pass in a full dill.
if (sources.isNotEmpty)
'${Flags.sources}=${sources.join(',')}'
else
'${Flags.inputDill}=${toUri(module, dillId)}',
'${Flags.cfeConstants}',
if (dillDependencies.isNotEmpty)
'--dill-dependencies=${dillDependencies.join(',')}',
'--out=${toUri(module, modularUpdatedDillId)}',
'${Flags.writeModularAnalysis}=${toUri(module, modularDataId)}',
for (String flag in flags) '--enable-experiment=$flag',
...extraArgs
];
var result =
await _runProcess(Platform.resolvedExecutable, args, root.toFilePath());
_checkExitCode(result, this, module);
}
@override
void notifyCached(Module module) {
if (_options.verbose) {
print("cached step: dart2js modular analysis on $module");
}
}
}
class ConcatenateDillsStep extends IOModularStep {
final bool useModularAnalysis;
DataId get idForDill => useModularAnalysis ? modularUpdatedDillId : dillId;
List<DataId> get dependencies => [
idForDill,
if (useModularAnalysis) modularDataId,
];
@override
List<DataId> get resultData => [
fullDillId,
if (useModularAnalysis) modularFullDataId,
];
@override
bool get needsSources => false;
@override
List<DataId> get dependencyDataNeeded => dependencies;
@override
List<DataId> get moduleDataNeeded => dependencies;
@override
bool get onlyOnMain => true;
ConcatenateDillsStep({this.useModularAnalysis});
@override
Future<void> execute(Module module, Uri root, ModuleDataToRelativeUri toUri,
List<String> flags) async {
if (_options.verbose) print("\nstep: dart2js concatenate dills on $module");
Set<Module> transitiveDependencies = computeTransitiveDependencies(module);
DataId dillId = idForDill;
Iterable<String> dillDependencies =
transitiveDependencies.map((m) => '${toUri(m, dillId)}');
List<String> dataDependencies = transitiveDependencies
.map((m) => '${toUri(m, modularDataId)}')
.toList();
dataDependencies.add('${toUri(module, modularDataId)}');
List<String> args = [
'--packages=${sdkRoot.toFilePath()}/$packageConfigJsonPath',
_dart2jsScript,
// TODO(sigmund): remove this dependency on libraries.json
if (_options.useSdk) '--libraries-spec=$_librarySpecForSnapshot',
'${Flags.entryUri}=$fakeRoot${module.mainSource}',
'${Flags.inputDill}=${toUri(module, dillId)}',
for (String flag in flags) '--enable-experiment=$flag',
'${Flags.dillDependencies}=${dillDependencies.join(',')}',
if (useModularAnalysis) ...[
'${Flags.readModularAnalysis}=${dataDependencies.join(',')}',
'${Flags.writeModularAnalysis}=${toUri(module, modularFullDataId)}',
],
'${Flags.cfeOnly}',
'--out=${toUri(module, fullDillId)}',
];
var result =
await _runProcess(Platform.resolvedExecutable, args, root.toFilePath());
_checkExitCode(result, this, module);
}
@override
void notifyCached(Module module) {
if (_options.verbose)
print("\ncached step: dart2js concatenate dills on $module");
}
}
// Step that invokes the dart2js closed world computation.
class ComputeClosedWorldStep extends IOModularStep {
final bool useModularAnalysis;
ComputeClosedWorldStep({this.useModularAnalysis});
List<DataId> get dependencies => [
fullDillId,
if (useModularAnalysis) modularFullDataId,
];
@override
List<DataId> get resultData => const [closedWorldId, globalUpdatedDillId];
@override
bool get needsSources => false;
@override
List<DataId> get dependencyDataNeeded => dependencies;
@override
List<DataId> get moduleDataNeeded => dependencies;
@override
bool get onlyOnMain => true;
@override
Future<void> execute(Module module, Uri root, ModuleDataToRelativeUri toUri,
List<String> flags) async {
if (_options.verbose)
print("\nstep: dart2js compute closed world on $module");
List<String> args = [
'--packages=${sdkRoot.toFilePath()}/$packageConfigJsonPath',
_dart2jsScript,
// TODO(sigmund): remove this dependency on libraries.json
if (_options.useSdk) '--libraries-spec=$_librarySpecForSnapshot',
'${Flags.entryUri}=$fakeRoot${module.mainSource}',
'${Flags.inputDill}=${toUri(module, fullDillId)}',
for (String flag in flags) '--enable-experiment=$flag',
if (useModularAnalysis)
'${Flags.readModularAnalysis}=${toUri(module, modularFullDataId)}',
'${Flags.writeClosedWorld}=${toUri(module, closedWorldId)}',
Flags.noClosedWorldInData,
'--out=${toUri(module, globalUpdatedDillId)}',
];
var result =
await _runProcess(Platform.resolvedExecutable, args, root.toFilePath());
_checkExitCode(result, this, module);
}
@override
void notifyCached(Module module) {
if (_options.verbose)
print("\ncached step: dart2js compute closed world on $module");
}
}
// Step that runs the dart2js modular analysis.
class GlobalAnalysisStep extends IOModularStep {
@override
List<DataId> get resultData => const [globalDataId];
@override
bool get needsSources => false;
@override
List<DataId> get dependencyDataNeeded => const [globalUpdatedDillId];
@override
List<DataId> get moduleDataNeeded =>
const [closedWorldId, globalUpdatedDillId];
@override
bool get onlyOnMain => true;
@override
Future<void> execute(Module module, Uri root, ModuleDataToRelativeUri toUri,
List<String> flags) async {
if (_options.verbose) print("\nstep: dart2js global analysis on $module");
List<String> args = [
'--packages=${sdkRoot.toFilePath()}/$packageConfigJsonPath',
_dart2jsScript,
// TODO(sigmund): remove this dependency on libraries.json
if (_options.useSdk) '--libraries-spec=$_librarySpecForSnapshot',
'${Flags.entryUri}=$fakeRoot${module.mainSource}',
'${Flags.inputDill}=${toUri(module, globalUpdatedDillId)}',
for (String flag in flags) '--enable-experiment=$flag',
'${Flags.readClosedWorld}=${toUri(module, closedWorldId)}',
'${Flags.writeData}=${toUri(module, globalDataId)}',
// TODO(joshualitt): delete this flag after google3 roll
'${Flags.noClosedWorldInData}',
];
var result =
await _runProcess(Platform.resolvedExecutable, args, root.toFilePath());
_checkExitCode(result, this, module);
}
@override
void notifyCached(Module module) {
if (_options.verbose)
print("\ncached step: dart2js global analysis on $module");
}
}
// Step that invokes the dart2js code generation on the main module given the
// results of the global analysis step and produces one shard of the codegen
// output.
class Dart2jsCodegenStep extends IOModularStep {
final ShardDataId codeId;
Dart2jsCodegenStep(this.codeId);
@override
List<DataId> get resultData => [codeId];
@override
bool get needsSources => false;
@override
List<DataId> get dependencyDataNeeded => const [];
@override
List<DataId> get moduleDataNeeded =>
const [globalUpdatedDillId, closedWorldId, globalDataId];
@override
bool get onlyOnMain => true;
@override
Future<void> execute(Module module, Uri root, ModuleDataToRelativeUri toUri,
List<String> flags) async {
if (_options.verbose) print("\nstep: dart2js backend on $module");
List<String> args = [
'--packages=${sdkRoot.toFilePath()}/$packageConfigJsonPath',
_dart2jsScript,
if (_options.useSdk) '--libraries-spec=$_librarySpecForSnapshot',
'${Flags.entryUri}=$fakeRoot${module.mainSource}',
'${Flags.inputDill}=${toUri(module, globalUpdatedDillId)}',
for (String flag in flags) '--enable-experiment=$flag',
'${Flags.readClosedWorld}=${toUri(module, closedWorldId)}',
'${Flags.readData}=${toUri(module, globalDataId)}',
'${Flags.writeCodegen}=${toUri(module, codeId.dataId)}',
'${Flags.codegenShard}=${codeId.shard}',
'${Flags.codegenShards}=${codeId.dataId.shards}',
];
var result =
await _runProcess(Platform.resolvedExecutable, args, root.toFilePath());
_checkExitCode(result, this, module);
}
@override
void notifyCached(Module module) {
if (_options.verbose) print("cached step: dart2js backend on $module");
}
}
// Step that invokes the dart2js codegen enqueuer and emitter on the main module
// given the results of the global analysis step and codegen shards.
class Dart2jsEmissionStep extends IOModularStep {
@override
List<DataId> get resultData => const [jsId];
@override
bool get needsSources => false;
@override
List<DataId> get dependencyDataNeeded => const [];
@override
List<DataId> get moduleDataNeeded => const [
globalUpdatedDillId,
closedWorldId,
globalDataId,
codeId0,
codeId1
];
@override
bool get onlyOnMain => true;
@override
Future<void> execute(Module module, Uri root, ModuleDataToRelativeUri toUri,
List<String> flags) async {
if (_options.verbose) print("step: dart2js backend on $module");
List<String> args = [
'--packages=${sdkRoot.toFilePath()}/$packageConfigJsonPath',
_dart2jsScript,
if (_options.useSdk) '--libraries-spec=$_librarySpecForSnapshot',
'${Flags.entryUri}=$fakeRoot${module.mainSource}',
'${Flags.inputDill}=${toUri(module, globalUpdatedDillId)}',
for (String flag in flags) '${Flags.enableLanguageExperiments}=$flag',
'${Flags.readClosedWorld}=${toUri(module, closedWorldId)}',
'${Flags.readData}=${toUri(module, globalDataId)}',
'${Flags.readCodegen}=${toUri(module, codeId)}',
'${Flags.codegenShards}=${codeId.shards}',
'--out=${toUri(module, jsId)}',
];
var result =
await _runProcess(Platform.resolvedExecutable, args, root.toFilePath());
_checkExitCode(result, this, module);
}
@override
void notifyCached(Module module) {
if (_options.verbose) print("\ncached step: dart2js backend on $module");
}
}
/// Step that runs the output of dart2js in d8 and saves the output.
class RunD8 extends IOModularStep {
@override
List<DataId> get resultData => const [txtId];
@override
bool get needsSources => false;
@override
List<DataId> get dependencyDataNeeded => const [];
@override
List<DataId> get moduleDataNeeded => const [jsId];
@override
bool get onlyOnMain => true;
@override
Future<void> execute(Module module, Uri root, ModuleDataToRelativeUri toUri,
List<String> flags) async {
if (_options.verbose) print("\nstep: d8 on $module");
List<String> d8Args = [
sdkRoot
.resolve('sdk/lib/_internal/js_runtime/lib/preambles/d8.js')
.toFilePath(),
root.resolveUri(toUri(module, jsId)).toFilePath(),
];
var result = await _runProcess(
sdkRoot.resolve(_d8executable).toFilePath(), d8Args, root.toFilePath());
_checkExitCode(result, this, module);
await File.fromUri(root.resolveUri(toUri(module, txtId)))
.writeAsString(result.stdout);
}
@override
void notifyCached(Module module) {
if (_options.verbose) print("\ncached step: d8 on $module");
}
}
void _checkExitCode(ProcessResult result, IOModularStep step, Module module) {
if (result.exitCode != 0 || _options.verbose) {
stdout.write(result.stdout);
stderr.write(result.stderr);
}
if (result.exitCode != 0) {
throw "${step.runtimeType} failed on $module:\n\n"
"stdout:\n${result.stdout}\n\n"
"stderr:\n${result.stderr}";
}
}
Future<ProcessResult> _runProcess(
String command, List<String> arguments, String workingDirectory) {
if (_options.verbose) {
print('command:\n$command ${arguments.join(' ')} from $workingDirectory');
}
return Process.run(command, arguments, workingDirectory: workingDirectory);
}
String get _d8executable {
if (Platform.isWindows) {
return 'third_party/d8/windows/d8.exe';
} else if (Platform.isLinux) {
return 'third_party/d8/linux/d8';
} else if (Platform.isMacOS) {
return 'third_party/d8/macos/d8';
}
throw UnsupportedError('Unsupported platform.');
}
class ShardsDataId implements DataId {
@override
final String name;
final int shards;
const ShardsDataId(this.name, this.shards);
@override
String toString() => name;
}
class ShardDataId implements DataId {
final ShardsDataId dataId;
final int _shard;
const ShardDataId(this.dataId, this._shard);
int get shard {
assert(0 <= _shard && _shard < dataId.shards);
return _shard;
}
@override
String get name => '${dataId.name}${shard}';
@override
String toString() => name;
}
Future<void> resolveScripts(Options options) async {
Future<String> resolve(
String sourceUriOrPath, String relativeSnapshotPath) async {
Uri sourceUri = sdkRoot.resolve(sourceUriOrPath);
String result =
sourceUri.isScheme('file') ? sourceUri.toFilePath() : sourceUriOrPath;
if (_options.useSdk) {
String snapshot = Uri.file(Platform.resolvedExecutable)
.resolve(relativeSnapshotPath)
.toFilePath();
if (await File(snapshot).exists()) {
return snapshot;
}
}
return result;
}
_options = options;
_dart2jsScript = await resolve(
'package:compiler/src/dart2js.dart', 'snapshots/dart2js.dart.snapshot');
_kernelWorkerScript = await resolve('utils/bazel/kernel_worker.dart',
'snapshots/kernel_worker.dart.snapshot');
}
String _librarySpecForSnapshot = Uri.file(Platform.resolvedExecutable)
.resolve('../lib/libraries.json')
.toFilePath();