blob: b9104afbf4d3d0b1b1c4096d6de1192e6ca68bfb [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.
import 'dart:async';
import 'package:build/build.dart';
import 'package:build_daemon/change_provider.dart';
import 'package:build_daemon/constants.dart';
import 'package:build_daemon/daemon_builder.dart';
import 'package:build_daemon/data/build_status.dart';
import 'package:build_daemon/data/build_target.dart' hide OutputLocation;
import 'package:build_daemon/data/server_log.dart';
import 'package:build_runner/src/entrypoint/options.dart';
import 'package:build_runner/src/package_graph/build_config_overrides.dart';
import 'package:build_runner/src/watcher/asset_change.dart';
import 'package:build_runner/src/watcher/change_filter.dart';
import 'package:build_runner/src/watcher/collect_changes.dart';
import 'package:build_runner/src/watcher/delete_writer.dart';
import 'package:build_runner/src/watcher/graph_watcher.dart';
import 'package:build_runner/src/watcher/node_watcher.dart';
import 'package:build_runner_core/build_runner_core.dart'
hide BuildResult, BuildStatus;
import 'package:build_runner_core/build_runner_core.dart' as core
show BuildStatus;
import 'package:build_runner_core/src/generate/build_definition.dart';
import 'package:build_runner_core/src/generate/build_impl.dart';
import 'package:stream_transform/stream_transform.dart';
import 'package:watcher/watcher.dart';
import 'change_providers.dart';
/// A Daemon Builder that uses build_runner_core for building.
class BuildRunnerDaemonBuilder implements DaemonBuilder {
final _buildResults = StreamController<BuildResults>();
final BuildImpl _builder;
final BuildOptions _buildOptions;
final StreamController<ServerLog> _outputStreamController;
final ChangeProvider changeProvider;
Completer<Null> _buildingCompleter;
@override
final Stream<ServerLog> logs;
BuildRunnerDaemonBuilder._(
this._builder,
this._buildOptions,
this._outputStreamController,
this.changeProvider,
) : logs = _outputStreamController.stream.asBroadcastStream();
/// Waits for a running build to complete before returning.
///
/// If there is no running build, it will return immediately.
Future<void> get building => _buildingCompleter?.future;
@override
Stream<BuildResults> get builds => _buildResults.stream;
FinalizedReader get reader => _builder.finalizedReader;
final _buildScriptUpdateCompleter = Completer();
Future<void> get buildScriptUpdated => _buildScriptUpdateCompleter.future;
@override
Future<void> build(
Set<BuildTarget> targets, Iterable<WatchEvent> fileChanges) async {
var defaultTargets = targets.cast<DefaultBuildTarget>();
var changes = fileChanges
.map<AssetChange>(
(change) => AssetChange(AssetId.parse(change.path), change.type))
.toList();
if (!_buildOptions.skipBuildScriptCheck &&
_builder.buildScriptUpdates.hasBeenUpdated(
changes.map<AssetId>((change) => change.id).toSet())) {
_buildScriptUpdateCompleter.complete();
return;
}
var targetNames = targets.map((t) => t.target).toSet();
_logMessage(Level.INFO, 'About to build ${targetNames.toList()}...');
_signalStart(targetNames);
var results = <BuildResult>[];
var buildDirs = <BuildDirectory>{};
var buildFilters = <BuildFilter>{};
for (var target in defaultTargets) {
OutputLocation outputLocation;
if (target.outputLocation != null) {
outputLocation = OutputLocation(target.outputLocation.output,
useSymlinks: target.outputLocation.useSymlinks,
hoist: target.outputLocation.hoist);
}
buildDirs
.add(BuildDirectory(target.target, outputLocation: outputLocation));
if (target.buildFilters != null && target.buildFilters.isNotEmpty) {
buildFilters.addAll([
for (var pattern in target.buildFilters)
BuildFilter.fromArg(pattern, _buildOptions.packageGraph.root.name)
]);
} else {
buildFilters
..add(BuildFilter.fromArg(
'package:*/**', _buildOptions.packageGraph.root.name))
..add(BuildFilter.fromArg(
'${target.target}/**', _buildOptions.packageGraph.root.name));
}
}
try {
var mergedChanges = collectChanges([changes]);
var result = await _builder.run(mergedChanges,
buildDirs: buildDirs, buildFilters: buildFilters);
for (var target in targets) {
if (result.status == core.BuildStatus.success) {
// TODO(grouma) - Can we notify if a target was cached?
results.add(DefaultBuildResult((b) => b
..status = BuildStatus.succeeded
..target = target.target));
} else {
results.add(DefaultBuildResult((b) => b
..status = BuildStatus.failed
// TODO(grouma) - We should forward the error messages instead.
// We can use the AssetGraph and FailureReporter to provide a better
// error message.
..error = 'FailureType: ${result.failureType.exitCode}'
..target = target.target));
}
}
} catch (e) {
for (var target in targets) {
results.add(DefaultBuildResult((b) => b
..status = BuildStatus.failed
..error = '$e'
..target = target.target));
}
_logMessage(Level.SEVERE, 'Build Failed:\n${e.toString()}');
}
_signalEnd(results);
}
@override
Future<void> stop() async {
await _builder.beforeExit();
await _buildOptions.logListener.cancel();
}
void _logMessage(Level level, String message) =>
_outputStreamController.add(ServerLog(
(b) => b
..message = message
..level = level,
));
void _signalEnd(Iterable<BuildResult> results) {
_buildingCompleter.complete();
_buildResults.add(BuildResults((b) => b..results.addAll(results)));
}
void _signalStart(Iterable<String> targets) {
_buildingCompleter = Completer();
var results = <BuildResult>[];
for (var target in targets) {
results.add(DefaultBuildResult((b) => b
..status = BuildStatus.started
..target = target));
}
_buildResults.add(BuildResults((b) => b..results.addAll(results)));
}
static Future<BuildRunnerDaemonBuilder> create(
PackageGraph packageGraph,
List<BuilderApplication> builders,
DaemonOptions daemonOptions,
) async {
var expectedDeletes = <AssetId>{};
var outputStreamController = StreamController<ServerLog>();
var environment = OverrideableEnvironment(
IOEnvironment(packageGraph,
outputSymlinksOnly: daemonOptions.outputSymlinksOnly),
onLog: (record) {
outputStreamController.add(ServerLog.fromLogRecord(record));
});
var daemonEnvironment = OverrideableEnvironment(environment,
writer: OnDeleteWriter(environment.writer, expectedDeletes.add));
var logSubscription =
LogSubscription(environment, verbose: daemonOptions.verbose);
var overrideBuildConfig =
await findBuildConfigOverrides(packageGraph, daemonOptions.configKey);
var buildOptions = await BuildOptions.create(
logSubscription,
packageGraph: packageGraph,
deleteFilesByDefault: daemonOptions.deleteFilesByDefault,
overrideBuildConfig: overrideBuildConfig,
skipBuildScriptCheck: daemonOptions.skipBuildScriptCheck,
enableLowResourcesMode: daemonOptions.enableLowResourcesMode,
trackPerformance: daemonOptions.trackPerformance,
logPerformanceDir: daemonOptions.logPerformanceDir,
);
var builder = await BuildImpl.create(buildOptions, daemonEnvironment,
builders, daemonOptions.builderConfigOverrides,
isReleaseBuild: daemonOptions.isReleaseBuild);
// Only actually used for the AutoChangeProvider.
Stream<List<WatchEvent>> graphEvents() => PackageGraphWatcher(packageGraph,
watch: (node) => PackageNodeWatcher(node,
watch: daemonOptions.directoryWatcherFactory))
.watch()
.where((change) => shouldProcess(
change,
builder.assetGraph,
buildOptions,
// Assume we will create an outputDir.
true,
expectedDeletes,
))
.map((data) => WatchEvent(data.type, '${data.id}'))
.debounceBuffer(buildOptions.debounceDelay);
var changeProvider = daemonOptions.buildMode == BuildMode.Auto
? AutoChangeProvider(graphEvents())
: ManualChangeProvider(AssetTracker(builder.assetGraph,
daemonEnvironment.reader, buildOptions.targetGraph));
return BuildRunnerDaemonBuilder._(
builder, buildOptions, outputStreamController, changeProvider);
}
}