diff --git a/webdev/lib/src/command/build_command.dart b/webdev/lib/src/command/build_command.dart index a250abda9..c34618430 100644 --- a/webdev/lib/src/command/build_command.dart +++ b/webdev/lib/src/command/build_command.dart @@ -3,85 +3,20 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; -import 'dart:io'; -import 'dart:isolate'; +import 'dart:io' show Directory; import 'package:args/args.dart'; import 'package:args/command_runner.dart'; -import 'package:stack_trace/stack_trace.dart'; +import 'package:build_daemon/client.dart'; +import 'package:build_daemon/data/build_status.dart'; +import 'package:build_daemon/data/build_target.dart'; +import 'package:logging/logging.dart'; +import '../daemon_client.dart'; +import '../logging.dart'; import 'configuration.dart'; import 'shared.dart'; -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 args, [SendPort sendPort]) async { - var buildScript = await generateBuildScript(); - var scriptFile = new File(scriptLocation)..createSync(recursive: true); - scriptFile.writeAsStringSync(buildScript); - sendPort.send(p.absolute(scriptLocation)); -} -'''; -const _packagesFileName = '.packages'; - -Future _buildRunnerScript() async { - var packagesFile = File(_packagesFileName); - if (!packagesFile.existsSync()) { - throw FileSystemException( - 'A `$_packagesFileName` file does not exist in the target directory.', - packagesFile.absolute.path); - } - - var dataUri = Uri.dataFromString(_bootstrapScript); - - var messagePort = ReceivePort(); - var exitPort = ReceivePort(); - var errorPort = ReceivePort(); - - try { - await Isolate.spawnUri(dataUri, [], messagePort.sendPort, - onExit: exitPort.sendPort, - onError: errorPort.sendPort, - errorsAreFatal: true, - packageConfig: Uri.file(_packagesFileName)); - - var allErrorsFuture = errorPort.forEach((error) { - var errorList = error as List; - var message = errorList[0] as String; - var stack = 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 StateError('An error occurred while bootstrapping.'); - } - - assert(messages.length == 1); - return Uri.file(messages.single as String); - } finally { - messagePort.close(); - exitPort.close(); - errorPort.close(); - } -} - /// Command to execute pub run build_runner build. class BuildCommand extends Command { @override @@ -98,71 +33,74 @@ class BuildCommand extends Command { } @override - Future run() { + Future run() async { if (argResults.rest.isNotEmpty) { throw UsageException( 'Arguments were provided that are not supported: ' '"${argResults.rest.join(' ')}".', argParser.usage); } - return runCore('build', extraArgs: ['--fail-on-severe']); - } - Future runCore(String command, {List extraArgs}) async { var configuration = Configuration.fromArgs(argResults); + setVerbosity(configuration.verbose); var pubspecLock = await readPubspecLock(configuration); - final arguments = [command] - ..addAll(extraArgs ?? const []) - ..addAll(buildRunnerArgs(pubspecLock, configuration)); - - stdout.write('Creating build script'); - var stopwatch = 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 = ReceivePort(); - var errorPort = ReceivePort(); - var messagePort = 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(Trace.parse(trace).terse); - if (exitCode == 0) exitCode = 1; - }); + final arguments = buildRunnerArgs(pubspecLock, configuration); 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 StateError( - 'Bad response from isolate, expected an exit code but got ' - '$isolateExitCode'); + logHandler(Level.INFO, 'Connecting to the build daemon...'); + var client = await connectClient( + Directory.current.path, + arguments, + (serverLog) { + var recordLevel = levelForLog(serverLog) ?? Level.INFO; + logHandler(recordLevel, trimLevel(recordLevel, serverLog.log)); + }, + ); + OutputLocation outputLocation; + if (configuration.outputPath != null) { + outputLocation = OutputLocation((b) => b + ..output = configuration.outputPath + ..useSymlinks = false + ..hoist = configuration.outputInput.isNotEmpty); + } + client.registerBuildTarget(DefaultBuildTarget((b) => b + ..target = configuration.outputInput + ..outputLocation = outputLocation?.toBuilder())); + client.startBuild(); + var exitCode = 0; + var gotBuildStart = false; + await for (final result in client.buildResults) { + var targetResult = result.results.firstWhere( + (buildResult) => buildResult.target == configuration.outputInput, + orElse: () => null); + if (targetResult == null) continue; + // We ignore any builds that happen before we get a `started` event, + // because those could be stale (from some other client). + gotBuildStart = + gotBuildStart || targetResult.status == BuildStatus.started; + if (!gotBuildStart) continue; + + // Shouldn't happen, but being a bit defensive here. + if (targetResult.status == BuildStatus.started) continue; + + if (targetResult.status == BuildStatus.failed) { + exitCode = 1; } - exitCode = isolateExitCode as int; - exitCodeListener.cancel(); - exitCodeListener = null; - }); - await exitPort.first; - await errorListener.cancel(); - await exitCodeListener?.cancel(); + if (targetResult.error?.isNotEmpty == true) { + logHandler(Level.SEVERE, targetResult.error); + } + break; + } + await client.close(); return exitCode; - } finally { - exitPort.close(); - errorPort.close(); - messagePort.close(); + } on OptionsSkew catch (_) { + logHandler( + Level.SEVERE, + 'Incompatible options with current running build daemon.\n\n' + 'Please stop other WebDev instances running in this directory ' + 'before starting a new instance with these options.\n\n'); + return 1; } } } diff --git a/webdev/lib/src/command/configuration.dart b/webdev/lib/src/command/configuration.dart index 0672b16c5..faa4eb937 100644 --- a/webdev/lib/src/command/configuration.dart +++ b/webdev/lib/src/command/configuration.dart @@ -5,8 +5,8 @@ import 'package:args/args.dart'; import 'package:logging/logging.dart'; +import '../logging.dart'; import '../serve/injected/configuration.dart'; -import '../serve/logging.dart'; const autoOption = 'auto'; const chromeDebugPortFlag = 'chrome-debug-port'; @@ -169,12 +169,12 @@ class Configuration { String outputInput; if (output != 'NONE') { var splitOutput = output.split(':'); - if (splitOutput.length == 2) { - outputInput = splitOutput.first; - outputPath = splitOutput.last; - } else { + if (splitOutput.length == 1) { outputInput = ''; outputPath = output; + } else { + outputInput = splitOutput.first; + outputPath = splitOutput.skip(1).join(':'); } } diff --git a/webdev/lib/src/command/daemon_command.dart b/webdev/lib/src/command/daemon_command.dart index 6a440cedb..f96a1b828 100644 --- a/webdev/lib/src/command/daemon_command.dart +++ b/webdev/lib/src/command/daemon_command.dart @@ -12,8 +12,8 @@ import 'package:pedantic/pedantic.dart'; import '../daemon/app_domain.dart'; import '../daemon/daemon.dart'; import '../daemon/daemon_domain.dart'; +import '../logging.dart'; import '../serve/dev_workflow.dart'; -import '../serve/logging.dart'; import '../serve/utils.dart'; import 'configuration.dart'; import 'shared.dart'; @@ -63,8 +63,7 @@ class DaemonCommand extends Command { var configuration = Configuration(launchInChrome: true, debug: true, autoRun: false); var pubspecLock = await readPubspecLock(configuration); - var buildOptions = - buildRunnerArgs(pubspecLock, configuration, includeOutput: false); + var buildOptions = buildRunnerArgs(pubspecLock, configuration); var port = await findUnusedPort(); workflow = await DevWorkflow.start( configuration, diff --git a/webdev/lib/src/command/serve_command.dart b/webdev/lib/src/command/serve_command.dart index 3d2681c7b..aaf69e84d 100644 --- a/webdev/lib/src/command/serve_command.dart +++ b/webdev/lib/src/command/serve_command.dart @@ -8,8 +8,8 @@ import 'dart:io'; import 'package:args/args.dart'; import 'package:args/command_runner.dart'; +import '../logging.dart'; import '../serve/dev_workflow.dart'; -import '../serve/logging.dart'; import 'configuration.dart'; import 'shared.dart'; @@ -102,11 +102,10 @@ refresh: Performs a full page refresh. var pubspecLock = await readPubspecLock(configuration); // Forward remaining arguments as Build Options to the Daemon. // This isn't documented. Should it be advertised? - var buildOptions = - buildRunnerArgs(pubspecLock, configuration, includeOutput: false) - ..addAll(argResults.rest - .where((arg) => !arg.contains(':') || arg.startsWith('--')) - .toList()); + var buildOptions = buildRunnerArgs(pubspecLock, configuration) + ..addAll(argResults.rest + .where((arg) => !arg.contains(':') || arg.startsWith('--')) + .toList()); var directoryArgs = argResults.rest .where((arg) => arg.contains(':') || !arg.startsWith('--')) .toList(); diff --git a/webdev/lib/src/command/shared.dart b/webdev/lib/src/command/shared.dart index 5c3e4ec3e..12f61a301 100644 --- a/webdev/lib/src/command/shared.dart +++ b/webdev/lib/src/command/shared.dart @@ -46,20 +46,12 @@ void addSharedArgs(ArgParser argParser, /// Parses the provided [Configuration] to return a list of /// `package:build_runner` appropriate arguments. List buildRunnerArgs( - PubspecLock pubspecLock, Configuration configuration, - {bool includeOutput}) { - includeOutput ??= true; + PubspecLock pubspecLock, Configuration configuration) { var arguments = []; if (configuration.release) { arguments.add('--$releaseFlag'); } - if (includeOutput && - configuration.output != null && - configuration.output != outputNone) { - arguments.addAll(['--$outputFlag', configuration.output]); - } - if (configuration.verbose) { arguments.add('--$verboseFlag'); } diff --git a/webdev/lib/src/serve/daemon_client.dart b/webdev/lib/src/daemon_client.dart similarity index 97% rename from webdev/lib/src/serve/daemon_client.dart rename to webdev/lib/src/daemon_client.dart index 37ee6d165..67b67632b 100644 --- a/webdev/lib/src/serve/daemon_client.dart +++ b/webdev/lib/src/daemon_client.dart @@ -8,7 +8,8 @@ import 'dart:io'; import 'package:build_daemon/client.dart'; import 'package:build_daemon/constants.dart'; import 'package:build_daemon/data/server_log.dart'; -import 'package:webdev/src/util.dart'; + +import 'util.dart'; /// Connects to the `build_runner` daemon. Future connectClient(String workingDirectory, diff --git a/webdev/lib/src/serve/logging.dart b/webdev/lib/src/logging.dart similarity index 69% rename from webdev/lib/src/serve/logging.dart rename to webdev/lib/src/logging.dart index 72a33b43c..644698cf9 100644 --- a/webdev/lib/src/serve/logging.dart +++ b/webdev/lib/src/logging.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:io/ansi.dart'; +import 'package:build_daemon/data/server_log.dart'; import 'package:logging/logging.dart'; var _verbose = false; @@ -46,3 +47,24 @@ void _colorLog(Level level, String message, {bool verbose}) { stdout.writeln(''); } } + +/// Trims [level] from [message] if it is prefixed by it. +String trimLevel(Level level, String message) => message.startsWith('[$level]') + ? message.replaceFirst('[$level]', '').trimLeft() + : message; + +/// Detects if the [ServerLog] contains a [Level] and returns the +/// resulting value. +/// +/// If the [ServerLog] does not contain a [Level], null will be returned. +Level levelForLog(ServerLog serverLog) { + var log = serverLog.log; + Level recordLevel; + for (var level in Level.LEVELS) { + if (log.startsWith('[$level]')) { + recordLevel = level; + break; + } + } + return recordLevel; +} diff --git a/webdev/lib/src/serve/dev_workflow.dart b/webdev/lib/src/serve/dev_workflow.dart index de763d57b..cb0038c4e 100644 --- a/webdev/lib/src/serve/dev_workflow.dart +++ b/webdev/lib/src/serve/dev_workflow.dart @@ -10,20 +10,17 @@ import 'package:build_daemon/data/build_target.dart'; import 'package:logging/logging.dart'; import '../command/configuration.dart'; -import '../serve/chrome.dart'; -import '../serve/daemon_client.dart'; -import '../serve/debugger/devtools.dart'; -import '../serve/logging.dart'; -import '../serve/server_manager.dart'; -import '../serve/utils.dart'; -import '../serve/webdev_server.dart'; +import '../daemon_client.dart'; +import '../logging.dart'; +import 'chrome.dart'; +import 'debugger/devtools.dart'; +import 'server_manager.dart'; +import 'webdev_server.dart'; Future _startBuildDaemon( - String workingDirectory, - List buildOptions, -) async { - logHandler(Level.INFO, 'Connecting to the build daemon...'); + String workingDirectory, List buildOptions) async { try { + logHandler(Level.INFO, 'Connecting to the build daemon...'); return await connectClient( workingDirectory, buildOptions, diff --git a/webdev/lib/src/serve/handlers/dev_handler.dart b/webdev/lib/src/serve/handlers/dev_handler.dart index 43e080028..91fc2225b 100644 --- a/webdev/lib/src/serve/handlers/dev_handler.dart +++ b/webdev/lib/src/serve/handlers/dev_handler.dart @@ -14,11 +14,11 @@ import 'package:shelf/shelf.dart'; import 'package:sse/server/sse_handler.dart'; import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; -import '../../serve/chrome.dart'; -import '../../serve/data/run_request.dart'; -import '../../serve/logging.dart'; +import '../../logging.dart'; +import '../chrome.dart'; import '../data/connect_request.dart'; import '../data/devtools_request.dart'; +import '../data/run_request.dart'; import '../data/serializers.dart' as webdev; import '../debugger/app_debug_services.dart'; import '../debugger/devtools.dart'; diff --git a/webdev/lib/src/serve/utils.dart b/webdev/lib/src/serve/utils.dart index 986dc61b4..480e23276 100644 --- a/webdev/lib/src/serve/utils.dart +++ b/webdev/lib/src/serve/utils.dart @@ -5,9 +5,6 @@ import 'dart:async'; import 'dart:io'; -import 'package:build_daemon/data/server_log.dart'; -import 'package:logging/logging.dart'; - /// Returns a port that is probably, but not definitely, not in use. /// /// This has a built-in race condition: another process may bind this port at @@ -25,23 +22,3 @@ Future findUnusedPort() async { await socket.close(); return port; } - -String trimLevel(Level level, String message) => message.startsWith('[$level]') - ? message.replaceFirst('[$level]', '').trimLeft() - : message; - -/// Detects if the [ServerLog] contains a [Level] and returns the -/// resulting value. -/// -/// If the [ServerLog] does not contain a [Level], null will be returned. -Level levelForLog(ServerLog serverLog) { - var log = serverLog.log; - Level recordLevel; - for (var level in Level.LEVELS) { - if (log.startsWith('[$level]')) { - recordLevel = level; - break; - } - } - return recordLevel; -} diff --git a/webdev/test/e2e_test.dart b/webdev/test/e2e_test.dart index 705bc10fc..4ff6e011a 100644 --- a/webdev/test/e2e_test.dart +++ b/webdev/test/e2e_test.dart @@ -52,14 +52,14 @@ void main() { await expectLater( process.stdout, emitsThrough( - '[SEVERE] Unable to create merged directory at ${d.sandbox}.')); + contains('Unable to create merged directory at ${d.sandbox}.'))); await expectLater( process.stdout, emitsThrough( 'Choose a different directory or delete the contents of that ' 'directory.')); - await process.shouldExit(73); + await process.shouldExit(isNot(0)); }); group('should build with valid configuration', () { @@ -72,7 +72,7 @@ void main() { var process = await runWebDev(args, workingDirectory: exampleDirectory); - var expectedItems = ['[INFO] Succeeded']; + var expectedItems = ['Succeeded']; await checkProcessStdout(process, expectedItems); await process.shouldExit(0);