blob: 3be7ee4d2fdf65b7b27b24c26b897e6e7ff3bcd6 [file] [log] [blame]
// Copyright (c) 2016, 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_config/build_config.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';
import 'package:build_runner_core/src/asset_graph/graph.dart';
import 'package:build_runner_core/src/generate/build_impl.dart';
import 'package:crypto/crypto.dart';
import 'package:logging/logging.dart';
import 'package:pedantic/pedantic.dart';
import 'package:stream_transform/stream_transform.dart';
import 'package:watcher/watcher.dart';
import '../logging/std_io_logging.dart';
import '../server/server.dart';
import 'terminator.dart';
final _logger = Logger('Watch');
Future<ServeHandler> watch(
List<BuilderApplication> builders, {
bool deleteFilesByDefault,
bool assumeTty,
String configKey,
PackageGraph packageGraph,
RunnerAssetReader reader,
RunnerAssetWriter writer,
Resolvers resolvers,
Level logLevel,
void Function(LogRecord) onLog,
Duration debounceDelay,
DirectoryWatcher Function(String) directoryWatcherFactory,
Stream terminateEventStream,
bool skipBuildScriptCheck,
bool enableLowResourcesMode,
Map<String, BuildConfig> overrideBuildConfig,
Set<BuildDirectory> buildDirs,
bool outputSymlinksOnly,
bool trackPerformance,
bool verbose,
Map<String, Map<String, dynamic>> builderConfigOverrides,
bool isReleaseBuild,
String logPerformanceDir,
Set<BuildFilter> buildFilters,
}) async {
builderConfigOverrides ??= const {};
packageGraph ??= PackageGraph.forThisPackage();
buildDirs ??= <BuildDirectory>{};
buildFilters ??= <BuildFilter>{};
var environment = OverrideableEnvironment(
IOEnvironment(packageGraph,
assumeTty: assumeTty, outputSymlinksOnly: outputSymlinksOnly),
reader: reader,
writer: writer,
onLog: onLog ?? stdIOLogListener(assumeTty: assumeTty, verbose: verbose));
var logSubscription =
LogSubscription(environment, verbose: verbose, logLevel: logLevel);
overrideBuildConfig ??=
await findBuildConfigOverrides(packageGraph, configKey);
var options = await BuildOptions.create(
logSubscription,
deleteFilesByDefault: deleteFilesByDefault,
packageGraph: packageGraph,
overrideBuildConfig: overrideBuildConfig,
debounceDelay: debounceDelay,
skipBuildScriptCheck: skipBuildScriptCheck,
enableLowResourcesMode: enableLowResourcesMode,
trackPerformance: trackPerformance,
logPerformanceDir: logPerformanceDir,
resolvers: resolvers,
);
var terminator = Terminator(terminateEventStream);
var watch = _runWatch(
options,
environment,
builders,
builderConfigOverrides,
terminator.shouldTerminate,
directoryWatcherFactory,
configKey,
buildDirs
.any((target) => target?.outputLocation?.path?.isNotEmpty ?? false),
buildDirs,
buildFilters,
isReleaseMode: isReleaseBuild ?? false);
unawaited(watch.buildResults.drain().then((_) async {
await terminator.cancel();
await options.logListener.cancel();
}));
return createServeHandler(watch);
}
/// Repeatedly run builds as files change on disk until [until] fires.
///
/// Sets up file watchers and collects changes then triggers new builds. When
/// [until] fires the file watchers will be stopped and up to one additional
/// build may run if there were pending changes.
///
/// The [BuildState.buildResults] stream will end after the final build has been
/// run.
WatchImpl _runWatch(
BuildOptions options,
BuildEnvironment environment,
List<BuilderApplication> builders,
Map<String, Map<String, dynamic>> builderConfigOverrides,
Future until,
DirectoryWatcher Function(String) directoryWatcherFactory,
String configKey,
bool willCreateOutputDirs,
Set<BuildDirectory> buildDirs,
Set<BuildFilter> buildFilters,
{bool isReleaseMode = false}) =>
WatchImpl(
options,
environment,
builders,
builderConfigOverrides,
until,
directoryWatcherFactory,
configKey,
willCreateOutputDirs,
buildDirs,
buildFilters,
isReleaseMode: isReleaseMode);
class WatchImpl implements BuildState {
BuildImpl _build;
AssetGraph get assetGraph => _build?.assetGraph;
final _readyCompleter = Completer<void>();
Future<void> get ready => _readyCompleter.future;
final String _configKey; // may be null
/// Delay to wait for more file watcher events.
final Duration _debounceDelay;
/// Injectable factory for creating directory watchers.
final DirectoryWatcher Function(String) _directoryWatcherFactory;
/// Whether or not we will be creating any output directories.
///
/// If not, then we don't care about source edits that don't have outputs.
final bool _willCreateOutputDirs;
/// Should complete when we need to kill the build.
final _terminateCompleter = Completer<Null>();
/// The [PackageGraph] for the current program.
final PackageGraph packageGraph;
/// The directories to build upon file changes and where to output them.
final Set<BuildDirectory> _buildDirs;
/// Filters for specific files to build.
final Set<BuildFilter> _buildFilters;
@override
Future<BuildResult> currentBuild;
/// Pending expected delete events from the build.
final Set<AssetId> _expectedDeletes = <AssetId>{};
FinalizedReader _reader;
FinalizedReader get reader => _reader;
WatchImpl(
BuildOptions options,
BuildEnvironment environment,
List<BuilderApplication> builders,
Map<String, Map<String, dynamic>> builderConfigOverrides,
Future until,
this._directoryWatcherFactory,
this._configKey,
this._willCreateOutputDirs,
this._buildDirs,
this._buildFilters,
{bool isReleaseMode = false})
: _debounceDelay = options.debounceDelay,
packageGraph = options.packageGraph {
buildResults = _run(
options, environment, builders, builderConfigOverrides, until,
isReleaseMode: isReleaseMode)
.asBroadcastStream();
}
@override
Stream<BuildResult> buildResults;
/// Runs a build any time relevant files change.
///
/// Only one build will run at a time, and changes are batched.
///
/// File watchers are scheduled synchronously.
Stream<BuildResult> _run(
BuildOptions options,
BuildEnvironment environment,
List<BuilderApplication> builders,
Map<String, Map<String, dynamic>> builderConfigOverrides,
Future until,
{bool isReleaseMode = false}) {
var watcherEnvironment = OverrideableEnvironment(environment,
writer: OnDeleteWriter(environment.writer, _expectedDeletes.add));
var firstBuildCompleter = Completer<BuildResult>();
currentBuild = firstBuildCompleter.future;
var controller = StreamController<BuildResult>();
Future<BuildResult> doBuild(List<List<AssetChange>> changes) async {
assert(_build != null);
_logger..info('${'-' * 72}\n')..info('Starting Build\n');
var mergedChanges = collectChanges(changes);
_expectedDeletes.clear();
if (!options.skipBuildScriptCheck) {
if (_build.buildScriptUpdates
.hasBeenUpdated(mergedChanges.keys.toSet())) {
_terminateCompleter.complete();
_logger.severe('Terminating builds due to build script update');
return BuildResult(BuildStatus.failure, [],
failureType: FailureType.buildScriptChanged);
}
}
return _build.run(mergedChanges,
buildDirs: _buildDirs, buildFilters: _buildFilters);
}
var terminate = Future.any([until, _terminateCompleter.future]).then((_) {
_logger.info('Terminating. No further builds will be scheduled\n');
});
Digest originalRootPackagesDigest;
final rootPackagesId = AssetId(packageGraph.root.name, '.packages');
// Start watching files immediately, before the first build is even started.
var graphWatcher = PackageGraphWatcher(packageGraph,
logger: _logger,
watch: (node) =>
PackageNodeWatcher(node, watch: _directoryWatcherFactory));
graphWatcher
.watch()
.asyncMap<AssetChange>((change) {
// Delay any events until the first build is completed.
if (firstBuildCompleter.isCompleted) return change;
return firstBuildCompleter.future.then((_) => change);
})
.asyncMap<AssetChange>((change) {
var id = change.id;
assert(originalRootPackagesDigest != null);
if (id == rootPackagesId) {
// Kill future builds if the root packages file changes.
return watcherEnvironment.reader
.readAsBytes(rootPackagesId)
.then((bytes) {
if (md5.convert(bytes) != originalRootPackagesDigest) {
_terminateCompleter.complete();
_logger
.severe('Terminating builds due to package graph update, '
'please restart the build.');
}
return change;
});
} else if (_isBuildYaml(id) ||
_isConfiguredBuildYaml(id) ||
_isPackageBuildYamlOverride(id)) {
controller.add(BuildResult(BuildStatus.failure, [],
failureType: FailureType.buildConfigChanged));
// Kill future builds if the build.yaml files change.
_terminateCompleter.complete();
_logger.severe(
'Terminating builds due to ${id.package}:${id.path} update.');
}
return change;
})
.where((change) {
assert(_readyCompleter.isCompleted);
return shouldProcess(
change,
assetGraph,
options,
_willCreateOutputDirs,
_expectedDeletes,
);
})
.debounceBuffer(_debounceDelay)
.takeUntil(terminate)
.asyncMapBuffer((changes) => currentBuild = doBuild(changes)
..whenComplete(() => currentBuild = null))
.listen((BuildResult result) {
if (controller.isClosed) return;
controller.add(result);
})
.onDone(() async {
await currentBuild;
await _build?.beforeExit();
if (!controller.isClosed) await controller.close();
_logger.info('Builds finished. Safe to exit\n');
});
// Schedule the actual first build for the future so we can return the
// stream synchronously.
() async {
await logTimedAsync(_logger, 'Waiting for all file watchers to be ready',
() => graphWatcher.ready);
originalRootPackagesDigest = md5
.convert(await watcherEnvironment.reader.readAsBytes(rootPackagesId));
BuildResult firstBuild;
try {
_build = await BuildImpl.create(
options, watcherEnvironment, builders, builderConfigOverrides,
isReleaseBuild: isReleaseMode);
firstBuild = await _build
.run({}, buildDirs: _buildDirs, buildFilters: _buildFilters);
} on CannotBuildException {
_terminateCompleter.complete();
firstBuild = BuildResult(BuildStatus.failure, []);
} on BuildScriptChangedException {
_terminateCompleter.complete();
firstBuild = BuildResult(BuildStatus.failure, [],
failureType: FailureType.buildScriptChanged);
}
_reader = _build?.finalizedReader;
_readyCompleter.complete();
// It is possible this is already closed if the user kills the process
// early, which results in an exception without this check.
if (!controller.isClosed) controller.add(firstBuild);
firstBuildCompleter.complete(firstBuild);
}();
return controller.stream;
}
bool _isBuildYaml(AssetId id) => id.path == 'build.yaml';
bool _isConfiguredBuildYaml(AssetId id) =>
id.package == packageGraph.root.name &&
id.path == 'build.$_configKey.yaml';
bool _isPackageBuildYamlOverride(AssetId id) =>
id.package == packageGraph.root.name &&
id.path.contains(_packageBuildYamlRegexp);
final _packageBuildYamlRegexp = RegExp(r'^[a-z0-9_]+\.build\.yaml$');
}