blob: 24054a638b8e084fa093da833a36b1de2eb717a1 [file]
// 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.
import 'dart:async';
import 'dart:io' hide exitCode, exit;
import 'dart:isolate';
import 'package:args/command_runner.dart';
import 'package:meta/meta.dart';
import 'package:stack_trace/stack_trace.dart';
import '../pubspec.dart';
const _packagesFileName = '.packages';
const _release = 'release';
const _output = 'output';
const _verbose = 'verbose';
const outputNone = 'NONE';
const _requireBuildWebCompilers = 'build-web-compilers';
/// Extend to get a command with the arguments common to all build_runner
/// commands.
abstract class CommandBase extends Command<int> {
final bool releaseDefault;
CommandBase({@required this.releaseDefault, @required String outputDefault}) {
// TODO(nshahan) Expose more common args passed to build_runner commands.
// build_runner might expose args for use in wrapping scripts like this one.
argParser
..addFlag(_release,
abbr: 'r',
defaultsTo: releaseDefault,
negatable: true,
help: 'Build with release mode defaults for builders.')
..addOption(
_output,
abbr: 'o',
defaultsTo: outputDefault,
help: 'A directory to write the result of a build to. Or a mapping '
'from a top-level directory in the package to the directory to '
'write a filtered build output to. For example "web:deploy".\n'
'A value of "NONE" indicates that no "--output" value should be '
'passed to `build_runner`.',
)
..addFlag(_verbose,
abbr: 'v',
defaultsTo: false,
negatable: false,
help: 'Enables verbose logging.')
..addFlag(_requireBuildWebCompilers,
defaultsTo: true,
negatable: true,
help: 'If a dependency on `build_web_compilers` is required to run.');
}
List<String> getArgs(PubspecLock pubspecLock) {
var arguments = <String>[];
if ((argResults[_release] as bool) ?? releaseDefault) {
arguments.add('--$_release');
}
var output = argResults[_output] as String;
if (output != null && output != outputNone) {
arguments.addAll(['--$_output', output]);
}
if (argResults[_verbose] as bool) {
arguments.add('--$_verbose');
}
return arguments;
}
Future<int> runCore(String command, {List<String> extraArgs}) async {
var pubspecLock = await PubspecLock.read();
await checkPubspecLock(pubspecLock,
requireBuildWebCompilers:
argResults[_requireBuildWebCompilers] as bool);
final arguments = [command]
..addAll(extraArgs ?? const [])
..addAll(getArgs(pubspecLock));
stdout.write('Creating build script');
var stopwatch = new Stopwatch()..start();
var buildRunnerScript = await _buildRunnerScript();
stdout.writeln(', took ${stopwatch.elapsedMilliseconds}ms');
var exitCode = 0;
// Heavily inspired by dart-lang/build @ 0c77443dd7
// /build_runner/bin/build_runner.dart#L58-L85
var exitPort = new ReceivePort();
var errorPort = new ReceivePort();
var messagePort = new ReceivePort();
var errorListener = errorPort.listen((e) {
stderr.writeln('\n\nYou have hit a bug in build_runner');
stderr.writeln('Please file an issue with reproduction steps at '
'https://github.com/dart-lang/build/issues\n\n');
final error = e[0];
final trace = e[1] as String;
stderr.writeln(error);
stderr.writeln(new Trace.parse(trace).terse);
if (exitCode == 0) exitCode = 1;
});
try {
await Isolate.spawnUri(buildRunnerScript, arguments, messagePort.sendPort,
onExit: exitPort.sendPort,
onError: errorPort.sendPort,
automaticPackageResolution: true);
StreamSubscription exitCodeListener;
exitCodeListener = messagePort.listen((isolateExitCode) {
if (isolateExitCode is! int) {
throw new StateError(
'Bad response from isolate, expected an exit code but got '
'$isolateExitCode');
}
exitCode = isolateExitCode as int;
exitCodeListener.cancel();
exitCodeListener = null;
});
await exitPort.first;
await errorListener.cancel();
await exitCodeListener?.cancel();
return exitCode;
} finally {
exitPort.close();
errorPort.close();
messagePort.close();
}
}
}
Future<Uri> _buildRunnerScript() async {
var packagesFile = new File(_packagesFileName);
if (!packagesFile.existsSync()) {
throw new FileSystemException(
'A `$_packagesFileName` file does not exist in the target directory.',
packagesFile.absolute.path);
}
var dataUri = new Uri.dataFromString(_bootstrapScript);
var messagePort = new ReceivePort();
var exitPort = new ReceivePort();
var errorPort = new ReceivePort();
try {
await Isolate.spawnUri(dataUri, [], messagePort.sendPort,
onExit: exitPort.sendPort,
onError: errorPort.sendPort,
errorsAreFatal: true,
packageConfig: new Uri.file(_packagesFileName));
var allErrorsFuture = errorPort.forEach((error) {
var errorList = error as List;
var message = errorList[0] as String;
var stack = new StackTrace.fromString(errorList[1] as String);
stderr.writeln(message);
stderr.writeln(stack);
});
var items = await Future.wait([
messagePort.toList(),
allErrorsFuture,
exitPort.first.whenComplete(() {
messagePort.close();
errorPort.close();
})
]);
var messages = items[0] as List;
if (messages.isEmpty) {
throw new StateError('An error occurred while bootstrapping.');
}
assert(messages.length == 1);
return new Uri.file(messages.single as String);
} finally {
messagePort.close();
exitPort.close();
errorPort.close();
}
}
const _bootstrapScript = r'''
import 'dart:io';
import 'dart:isolate';
import 'package:build_runner/build_script_generate.dart';
import 'package:path/path.dart' as p;
void main(List<String> args, [SendPort sendPort]) async {
var buildScript = await generateBuildScript();
var scriptFile = new File(scriptLocation)..createSync(recursive: true);
scriptFile.writeAsStringSync(buildScript);
sendPort.send(p.absolute(scriptLocation));
}
''';