diff --git a/build_runner/lib/src/build/build.dart b/build_runner/lib/src/build/build.dart index 43e6b4b04b..466da55f73 100644 --- a/build_runner/lib/src/build/build.dart +++ b/build_runner/lib/src/build/build.dart @@ -15,7 +15,6 @@ import 'package:path/path.dart' as p; import 'package:watcher/watcher.dart'; import '../bootstrap/build_process_state.dart'; -import '../build_plan/build_directory.dart'; import '../build_plan/build_options.dart'; import '../build_plan/build_phases.dart'; import '../build_plan/build_plan.dart'; @@ -24,6 +23,7 @@ import '../build_plan/phase.dart'; import '../build_plan/target_graph.dart'; import '../build_plan/testing_overrides.dart'; import '../constants.dart'; +import '../io/build_output_reader.dart'; import '../io/create_merged_dir.dart'; import '../io/reader_writer.dart'; import '../logging/build_log.dart'; @@ -33,12 +33,10 @@ import 'asset_graph/node.dart'; import 'asset_graph/post_process_build_step_id.dart'; import 'build_dirs.dart'; import 'build_result.dart'; -import 'finalized_assets_view.dart'; import 'input_tracker.dart'; import 'library_cycle_graph/asset_deps_loader.dart'; import 'library_cycle_graph/library_cycle_graph.dart'; import 'library_cycle_graph/library_cycle_graph_loader.dart'; -import 'optional_output_tracker.dart'; import 'performance_tracker.dart'; import 'performance_tracking_resolvers.dart'; import 'resolver/analysis_driver_model.dart'; @@ -110,6 +108,9 @@ class Build { /// transitive source. final Map changedGraphs = Map.identity(); + /// The build output. + final BuildOutputReader buildOutputReader; + Build({ required this.buildPlan, required this.readerWriter, @@ -122,7 +123,12 @@ class Build { previousDepsLoader = assetGraph.previousPhasedAssetDeps == null ? null - : AssetDepsLoader.fromDeps(assetGraph.previousPhasedAssetDeps!); + : AssetDepsLoader.fromDeps(assetGraph.previousPhasedAssetDeps!), + buildOutputReader = BuildOutputReader( + buildPlan: buildPlan, + readerWriter: readerWriter, + assetGraph: assetGraph, + ); BuildOptions get buildOptions => buildPlan.buildOptions; TestingOverrides get testingOverrides => buildPlan.testingOverrides; @@ -138,13 +144,6 @@ class Build { (b) => b..rootPackageName = packageGraph.root.name, ); var result = await _safeBuild(updates); - final optionalOutputTracker = OptionalOutputTracker( - assetGraph, - targetGraph, - BuildDirectory.buildPaths(buildPlan.buildOptions.buildDirs), - buildPlan.buildOptions.buildFilters, - buildPhases, - ); if (result.status == BuildStatus.success) { final failures = []; for (final output in processedOutputs) { @@ -169,21 +168,31 @@ class Build { logger.severe(error); } } - result = BuildResult( - BuildStatus.failure, - result.outputs, - performance: result.performance, - ); + result = result.copyWith(status: BuildStatus.failure); } } readerWriter.cache.flush(); await resourceManager.disposeAll(); - result = await _finalizeBuild( - result, - FinalizedAssetsView(assetGraph, packageGraph, optionalOutputTracker), - readerWriter, - buildPlan.buildOptions.buildDirs, - ); + + // If requested, create output directories. If that fails, fail the build. + if (buildPlan.buildOptions.buildDirs.any( + (target) => target.outputLocation?.path.isNotEmpty ?? false, + ) && + result.status == BuildStatus.success) { + if (!await createMergedOutputDirectories( + packageGraph: packageGraph, + outputSymlinksOnly: buildOptions.outputSymlinksOnly, + buildDirs: buildOptions.buildDirs, + buildOutputReader: buildOutputReader, + readerWriter: readerWriter, + )) { + result = result.copyWith( + status: BuildStatus.failure, + failureType: FailureType.cantCreate, + ); + } + } + _resolvers.reset(); buildLog.finishBuild( result: result.status == BuildStatus.success, @@ -282,7 +291,13 @@ class Build { buildLog.error( buildLog.renderThrowable('Unhandled build failure!', e, st), ); - done.complete(BuildResult(BuildStatus.failure, [])); + done.complete( + BuildResult( + status: BuildStatus.failure, + outputs: BuiltList(), + buildOutputReader: buildOutputReader, + ), + ); } }, ); @@ -372,9 +387,10 @@ class Build { ); // Assume success, `_assetGraph.failedOutputs` will be checked later. return BuildResult( - BuildStatus.success, - outputs, + status: BuildStatus.success, + outputs: outputs.build(), performance: performanceTracker, + buildOutputReader: buildOutputReader, ); }); } @@ -394,7 +410,7 @@ class Build { .toList(growable: false)) { if (!shouldBuildForDirs( node.id, - buildDirs: BuildDirectory.buildPaths(buildPlan.buildOptions.buildDirs), + buildDirs: buildPlan.buildOptions.buildDirs, buildFilters: buildPlan.buildOptions.buildFilters, phase: phase, targetGraph: targetGraph, @@ -1211,60 +1227,6 @@ class Build { } Future _delete(AssetId id) => readerWriter.delete(id); - - /// Invoked after each build, can modify the [BuildResult] in any way, even - /// converting it to a failure. - /// - /// The [finalizedAssetsView] can only be used until the returned [Future] - /// completes, it will expire afterwords since it can no longer guarantee a - /// consistent state. - /// - /// By default this returns the original result. - /// - /// Any operation may be performed, as determined by environment. - Future _finalizeBuild( - BuildResult buildResult, - FinalizedAssetsView finalizedAssetsView, - ReaderWriter readerWriter, - BuiltSet buildDirs, - ) async { - if (testingOverrides.finalizeBuild != null) { - return testingOverrides.finalizeBuild!( - buildResult, - finalizedAssetsView, - readerWriter, - buildDirs, - ); - } - if (buildDirs.any( - (target) => target.outputLocation?.path.isNotEmpty ?? false, - ) && - buildResult.status == BuildStatus.success) { - if (!await createMergedOutputDirectories( - buildDirs, - packageGraph, - readerWriter, - finalizedAssetsView, - buildOptions.outputSymlinksOnly, - )) { - return _convertToFailure( - buildResult, - failureType: FailureType.cantCreate, - ); - } - } - return buildResult; - } } String _twoDigits(int n) => '$n'.padLeft(2, '0'); - -BuildResult _convertToFailure( - BuildResult previous, { - FailureType? failureType, -}) => BuildResult( - BuildStatus.failure, - previous.outputs, - performance: previous.performance, - failureType: failureType, -); diff --git a/build_runner/lib/src/build/build_dirs.dart b/build_runner/lib/src/build/build_dirs.dart index 96a6a6c42e..cd86de52e2 100644 --- a/build_runner/lib/src/build/build_dirs.dart +++ b/build_runner/lib/src/build/build_dirs.dart @@ -5,6 +5,7 @@ import 'package:build/build.dart'; import 'package:built_collection/built_collection.dart'; +import '../build_plan/build_directory.dart'; import '../build_plan/build_filter.dart'; import '../build_plan/phase.dart'; import '../build_plan/target_graph.dart'; @@ -23,18 +24,20 @@ import '../build_plan/target_graph.dart'; /// `id.path` must start with one of the specified directory names. bool shouldBuildForDirs( AssetId id, { - required BuiltSet buildDirs, + required BuiltSet buildDirs, required BuildPhase phase, required TargetGraph targetGraph, BuiltSet? buildFilters, }) { + // Empty paths means "build everything". + final paths = BuildDirectory.buildPaths(buildDirs); buildFilters ??= BuiltSet(); if (buildFilters.isEmpty) { // Build asset if: It's built to source, it's public or if it's matched by // a build directory. return !phase.hideOutput || - buildDirs.isEmpty || - buildDirs.any(id.path.startsWith) || + paths.isEmpty || + paths.any(id.path.startsWith) || targetGraph.isPublicAsset(id); } else { // Don't build assets not matched by build filters @@ -44,8 +47,8 @@ bool shouldBuildForDirs( // In filtered assets, build the public ones or those inside a build // directory. - return buildDirs.isEmpty || - buildDirs.any(id.path.startsWith) || + return paths.isEmpty || + paths.any(id.path.startsWith) || targetGraph.isPublicAsset(id); } } diff --git a/build_runner/lib/src/build/build_result.dart b/build_runner/lib/src/build/build_result.dart index 77c7d04337..b6af35d10c 100644 --- a/build_runner/lib/src/build/build_result.dart +++ b/build_runner/lib/src/build/build_result.dart @@ -1,11 +1,12 @@ // 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:built_collection/built_collection.dart'; import 'package:meta/meta.dart'; +import '../io/build_output_reader.dart'; import 'performance_tracker.dart'; /// The result of an individual build, this may be an incremental build or @@ -18,22 +19,36 @@ class BuildResult { final FailureType? failureType; /// All outputs created/updated during this build. - final List outputs; + final BuiltList outputs; - /// The [BuildPerformance] broken out by build action, may be `null`. + // The build output. + final BuildOutputReader buildOutputReader; + + /// The [BuildPerformance] broken out by build action. @experimental final BuildPerformance? performance; - BuildResult( - this.status, - List outputs, { + BuildResult({ + required this.status, + BuiltList? outputs, + required this.buildOutputReader, this.performance, FailureType? failureType, - }) : outputs = List.unmodifiable(outputs), - failureType = + }) : failureType = failureType == null && status == BuildStatus.failure ? FailureType.general - : failureType; + : failureType, + outputs = outputs ?? BuiltList(); + + BuildResult copyWith({BuildStatus? status, FailureType? failureType}) => + BuildResult( + status: status ?? this.status, + outputs: outputs, + buildOutputReader: buildOutputReader, + performance: performance, + failureType: failureType ?? this.failureType, + ); + @override String toString() { if (status == BuildStatus.success) { @@ -50,9 +65,9 @@ Build Failed :( } factory BuildResult.buildScriptChanged() => BuildResult( - BuildStatus.failure, - const [], + status: BuildStatus.failure, failureType: FailureType.buildScriptChanged, + buildOutputReader: BuildOutputReader.empty(), ); } @@ -68,8 +83,3 @@ class FailureType { final int exitCode; FailureType._(this.exitCode); } - -abstract class BuildState { - Future? get currentBuild; - Stream get buildResults; -} diff --git a/build_runner/lib/src/build/build_series.dart b/build_runner/lib/src/build/build_series.dart index 5d704207fa..7dd5756ea6 100644 --- a/build_runner/lib/src/build/build_series.dart +++ b/build_runner/lib/src/build/build_series.dart @@ -8,14 +8,17 @@ import 'package:build/build.dart'; import 'package:built_collection/built_collection.dart'; import 'package:watcher/watcher.dart'; -import '../bootstrap/build_script_updates.dart'; import '../build_plan/build_directory.dart'; import '../build_plan/build_filter.dart'; import '../build_plan/build_plan.dart'; +import '../commands/watch/asset_change.dart'; +import '../constants.dart'; +import '../io/asset_tracker.dart'; +import '../io/build_output_reader.dart'; import '../io/filesystem_cache.dart'; -import '../io/finalized_reader.dart'; import '../io/reader_writer.dart'; import 'asset_graph/graph.dart'; +import 'asset_graph/node.dart'; import 'build.dart'; import 'build_result.dart'; @@ -32,40 +35,134 @@ import 'build_result.dart'; /// this serialized state is not actually used: the `AssetGraph` instance /// already in memory is used directly. class BuildSeries { - final BuildPlan buildPlan; + final BuildPlan _buildPlan; - final AssetGraph assetGraph; - final BuildScriptUpdates? buildScriptUpdates; + final AssetGraph _assetGraph; - final FinalizedReader finalizedReader; - final ReaderWriter readerWriter; - final ResourceManager resourceManager = ResourceManager(); + final ReaderWriter _readerWriter; + final ResourceManager _resourceManager = ResourceManager(); /// For the first build only, updates from the previous serialized build /// state. /// /// Null after the first build, or if there was no serialized build state, or /// if the serialized build state was discarded. - BuiltMap? updatesFromLoad; + BuiltMap? _updatesFromLoad; + + final StreamController _buildResultsController = + StreamController.broadcast(); /// Whether the next build is the first build. bool firstBuild = true; - Future beforeExit() => resourceManager.beforeExit(); - - BuildSeries._( - this.buildPlan, - this.assetGraph, - this.buildScriptUpdates, - this.finalizedReader, - this.updatesFromLoad, - ) : readerWriter = buildPlan.readerWriter.copyWith( - generatedAssetHider: assetGraph, - cache: - buildPlan.buildOptions.enableLowResourcesMode - ? const PassthroughFilesystemCache() - : InMemoryFilesystemCache(), - ); + BuildSeries._({ + required BuildPlan buildPlan, + required AssetGraph assetGraph, + required ReaderWriter readerWriter, + required BuiltMap? updatesFromLoad, + }) : _buildPlan = buildPlan, + _assetGraph = assetGraph, + _readerWriter = readerWriter, + _updatesFromLoad = updatesFromLoad; + + factory BuildSeries(BuildPlan buildPlan) { + final assetGraph = buildPlan.takeAssetGraph(); + final readerWriter = buildPlan.readerWriter.copyWith( + generatedAssetHider: assetGraph, + cache: + buildPlan.buildOptions.enableLowResourcesMode + ? const PassthroughFilesystemCache() + : InMemoryFilesystemCache(), + ); + return BuildSeries._( + buildPlan: buildPlan, + assetGraph: assetGraph, + readerWriter: readerWriter, + updatesFromLoad: buildPlan.updates, + ); + } + + /// Broadcast stream of build results. + Stream get buildResults => _buildResultsController.stream; + Future? _currentBuildResult; + + bool _hasBuildScriptChanged(Set changes) { + if (_buildPlan.buildOptions.skipBuildScriptCheck) return false; + if (_buildPlan.buildScriptUpdates == null) return true; + return _buildPlan.buildScriptUpdates!.hasBeenUpdated(changes); + } + + /// Returns whether [change] might trigger a build. + /// + /// Pass expected deletes in [expectedDeletes]. Expected deletes do not + /// trigger a build. A delete that matches is removed from the set. + Future shouldProcess( + AssetChange change, + Set expectedDeletes, + ) async { + // Ignore any expected delete once. + if (change.type == ChangeType.REMOVE && expectedDeletes.remove(change.id)) { + return false; + } + + final node = + _assetGraph.contains(change.id) ? _assetGraph.get(change.id) : null; + + // Changes to files that are not currently part of the build. + if (node == null) { + // Ignore under `.dart_tool/build`. + if (change.id.path.startsWith(cacheDir)) return false; + + // Ignore modifications and deletes. + if (change.type != ChangeType.ADD) return false; + + // It's an add: return whether it's a new input. + return _buildPlan.targetGraph.anyMatchesAsset(change.id); + } + + // Changes to files that are part of the build. + + // If not copying to a merged output directory, ignore changes to files with + // no outputs. + if (!_buildPlan.buildOptions.anyMergedOutputDirectory && + !node.changesRequireRebuild) { + return false; + } + + // Ignore creation or modification of outputs. + if (node.type == NodeType.generated && change.type != ChangeType.REMOVE) { + return false; + } + + // For modifications, confirm that the content actually changed. + if (change.type == ChangeType.MODIFY) { + _readerWriter.cache.invalidate([change.id]); + final newDigest = await _readerWriter.digest(change.id); + return node.digest != newDigest; + } + + // It's an add of "missing source" node or a deletion of an input. + return true; + } + + Future> checkForChanges() async { + final updates = await AssetTracker( + _buildPlan.readerWriter, + _buildPlan.targetGraph, + ).collectChanges(_assetGraph); + return List.of( + updates.entries.map((entry) => WatchEvent(entry.value, '${entry.key}')), + ); + } + + /// If a build is running, the build result when it's done. + /// + /// If no build has ever run, returns the first build result when it's + /// available. + /// + /// If a build has run, the most recent build result. + Future get currentBuildResult => + _currentBuildResult ?? buildResults.first; /// Runs a single build. /// @@ -80,50 +177,47 @@ class BuildSeries { BuiltSet? buildDirs, BuiltSet? buildFilters, }) async { - buildDirs ??= buildPlan.buildOptions.buildDirs; - buildFilters ??= buildPlan.buildOptions.buildFilters; + if (_hasBuildScriptChanged(updates.keys.toSet())) { + return BuildResult( + status: BuildStatus.failure, + failureType: FailureType.buildScriptChanged, + buildOutputReader: BuildOutputReader( + buildPlan: _buildPlan, + readerWriter: _readerWriter, + assetGraph: _assetGraph, + ), + ); + } + + buildDirs ??= _buildPlan.buildOptions.buildDirs; + buildFilters ??= _buildPlan.buildOptions.buildFilters; if (firstBuild) { - if (updatesFromLoad != null) { - updates = updatesFromLoad!.toMap()..addAll(updates); - updatesFromLoad = null; + if (_updatesFromLoad != null) { + updates = _updatesFromLoad!.toMap()..addAll(updates); + _updatesFromLoad = null; } } else { - if (updatesFromLoad != null) { + if (_updatesFromLoad != null) { throw StateError('Only first build can have updates from load.'); } } - finalizedReader.reset(BuildDirectory.buildPaths(buildDirs), buildFilters); final build = Build( - buildPlan: buildPlan.copyWith( + buildPlan: _buildPlan.copyWith( buildDirs: buildDirs, buildFilters: buildFilters, ), - assetGraph: assetGraph, - readerWriter: readerWriter, - resourceManager: resourceManager, + assetGraph: _assetGraph, + readerWriter: _readerWriter, + resourceManager: _resourceManager, ); if (firstBuild) firstBuild = false; - final result = await build.run(updates); + + _currentBuildResult = build.run(updates); + final result = await _currentBuildResult!; + _buildResultsController.add(result); return result; } - static Future create({required BuildPlan buildPlan}) async { - final assetGraph = buildPlan.takeAssetGraph(); - final finalizedReader = FinalizedReader( - buildPlan.readerWriter.copyWith(generatedAssetHider: assetGraph), - assetGraph, - buildPlan.targetGraph, - buildPlan.buildPhases, - buildPlan.packageGraph.root.name, - ); - final build = BuildSeries._( - buildPlan, - assetGraph, - buildPlan.buildScriptUpdates, - finalizedReader, - buildPlan.updates, - ); - return build; - } + Future beforeExit() => _resourceManager.beforeExit(); } diff --git a/build_runner/lib/src/build/finalized_assets_view.dart b/build_runner/lib/src/build/finalized_assets_view.dart deleted file mode 100644 index 6a69ae68fa..0000000000 --- a/build_runner/lib/src/build/finalized_assets_view.dart +++ /dev/null @@ -1,85 +0,0 @@ -// 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 'package:build/build.dart'; -import 'package:path/path.dart' as p; - -import '../build_plan/package_graph.dart'; -import 'asset_graph/graph.dart'; -import 'asset_graph/node.dart'; -import 'optional_output_tracker.dart'; - -/// A lazily computed view of all the assets available after a build. -/// -/// Note that this class has a limited lifetime during which it is available, -/// and should not be used outside of the scope in which it is given. It will -/// throw a [StateError] if you attempt to use it once it has expired. -class FinalizedAssetsView { - final AssetGraph _assetGraph; - final PackageGraph _packageGraph; - final OptionalOutputTracker _optionalOutputTracker; - - bool _expired = false; - - FinalizedAssetsView( - this._assetGraph, - this._packageGraph, - this._optionalOutputTracker, - ); - - List allAssets({String? rootDir}) { - if (_expired) { - throw StateError( - 'Cannot use a FinalizedAssetsView after it has expired!', - ); - } - return _assetGraph.allNodes - .map((node) { - if (_shouldSkipNode( - node, - rootDir, - _packageGraph, - _optionalOutputTracker, - )) { - return null; - } - return node.id; - }) - .whereType() - .toList(); - } - - void markExpired() { - assert(!_expired); - _expired = true; - } -} - -bool _shouldSkipNode( - AssetNode node, - String? rootDir, - PackageGraph packageGraph, - OptionalOutputTracker optionalOutputTracker, -) { - if (!node.isFile) return true; - if (node.isDeleted) return true; - - // Exclude non-lib assets if they're outside of the root directory or not from - // root package. - if (!node.id.path.startsWith('lib/')) { - if (rootDir != null && !p.isWithin(rootDir, node.id.path)) return true; - if (node.id.package != packageGraph.root.name) return true; - } - - if (node.type == NodeType.internal || node.type == NodeType.glob) return true; - if (node.type == NodeType.generated) { - if (!node.wasOutput || node.generatedNodeState!.result == false) { - return true; - } - return !optionalOutputTracker.isRequired(node.id); - } - if (node.id.path == '.packages') return true; - if (node.id.path == '.dart_tool/package_config.json') return true; - return false; -} diff --git a/build_runner/lib/src/build/optional_output_tracker.dart b/build_runner/lib/src/build/optional_output_tracker.dart deleted file mode 100644 index db221e8ea0..0000000000 --- a/build_runner/lib/src/build/optional_output_tracker.dart +++ /dev/null @@ -1,100 +0,0 @@ -// 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 'package:build/build.dart'; -import 'package:built_collection/built_collection.dart'; - -import '../build_plan/build_filter.dart'; -import '../build_plan/build_phases.dart'; -import '../build_plan/target_graph.dart'; -import 'asset_graph/graph.dart'; -import 'asset_graph/node.dart'; -import 'build_dirs.dart'; - -/// A cache of the results of checking whether outputs from optional build steps -/// were required by in the current build. -/// -/// An optional output becomes required if: -/// - Any of it's transitive outputs is required (based on the criteria below). -/// - It was output by the same build step as any required output. -/// -/// Any outputs from non-optional phases are considered required, unless the -/// following are all true. -/// - [_buildDirs] is non-empty. -/// - The output lives in a non-lib directory. -/// - The outputs path is not prefixed by one of [_buildDirs]. -/// - If [_buildFilters] is non-empty and the output doesn't match one of the -/// filters. -/// -/// Non-required optional output might still exist in the generated directory -/// and the asset graph but we should avoid serving them, outputting them in -/// the merged directories, or considering a failed output as an overall. -// TODO(davidmorgan): can this be removed? -class OptionalOutputTracker { - final _checkedOutputs = {}; - final AssetGraph _assetGraph; - final TargetGraph _targetGraph; - final BuiltSet _buildDirs; - final BuiltSet _buildFilters; - final BuildPhases _buildPhases; - - OptionalOutputTracker( - this._assetGraph, - this._targetGraph, - this._buildDirs, - this._buildFilters, - this._buildPhases, - ); - - /// Returns whether [output] is required. - /// - /// If necessary crawls transitive outputs that read [output] or any other - /// assets generated by the same phase until it finds one which is required. - /// - /// [currentlyChecking] is used to aovid repeatedly checking the same outputs. - bool isRequired(AssetId output, [Set? currentlyChecking]) { - currentlyChecking ??= {}; - if (currentlyChecking.contains(output)) return false; - currentlyChecking.add(output); - - final node = _assetGraph.get(output)!; - if (node.type != NodeType.generated) return true; - final nodeConfiguration = node.generatedNodeConfiguration!; - final phase = _buildPhases[nodeConfiguration.phaseNumber]; - if (!phase.isOptional && - shouldBuildForDirs( - output, - buildDirs: _buildDirs, - buildFilters: _buildFilters, - phase: phase, - targetGraph: _targetGraph, - )) { - return true; - } - return _checkedOutputs.putIfAbsent( - output, - () => - (_assetGraph.computeOutputs()[node.id] ?? {}).any( - (o) => isRequired(o, currentlyChecking), - ) || - _assetGraph - .outputsForPhase(output.package, nodeConfiguration.phaseNumber) - .where( - (n) => - n.generatedNodeConfiguration!.primaryInput == - node.generatedNodeConfiguration!.primaryInput, - ) - .map((n) => n.id) - .any((o) => isRequired(o, currentlyChecking)), - ); - } - - /// Clears the cache of which assets were required. - /// - /// If the tracker is used across multiple builds it must be reset in between - /// each one. - void reset() { - _checkedOutputs.clear(); - } -} diff --git a/build_runner/lib/src/build_plan/build_options.dart b/build_runner/lib/src/build_plan/build_options.dart index a527953571..fd50554869 100644 --- a/build_runner/lib/src/build_plan/build_options.dart +++ b/build_runner/lib/src/build_plan/build_options.dart @@ -30,6 +30,10 @@ class BuildOptions { final bool trackPerformance; final bool verbose; + late final bool anyMergedOutputDirectory = buildDirs.any( + (target) => target.outputLocation?.path.isNotEmpty ?? false, + ); + BuildOptions({ required this.buildDirs, required this.builderConfigOverrides, diff --git a/build_runner/lib/src/build_plan/build_plan.dart b/build_runner/lib/src/build_plan/build_plan.dart index 6496cdad58..b6b00b5529 100644 --- a/build_runner/lib/src/build_plan/build_plan.dart +++ b/build_runner/lib/src/build_plan/build_plan.dart @@ -129,12 +129,14 @@ class BuildPlan { builderApplications = BuiltList(); } - final buildPhases = await createBuildPhases( - targetGraph, - builderApplications, - buildOptions.builderConfigOverrides, - buildOptions.isReleaseBuild, - ); + final buildPhases = + testingOverrides.buildPhases ?? + await createBuildPhases( + targetGraph, + builderApplications, + buildOptions.builderConfigOverrides, + buildOptions.isReleaseBuild, + ); buildPhases.checkOutputLocations(packageGraph.root.name); if (buildPhases.inBuildPhases.isEmpty && buildPhases.postBuildPhase.builderActions.isEmpty) { diff --git a/build_runner/lib/src/build_plan/target_graph.dart b/build_runner/lib/src/build_plan/target_graph.dart index 7db049f1c9..4c5fa377f2 100644 --- a/build_runner/lib/src/build_plan/target_graph.dart +++ b/build_runner/lib/src/build_plan/target_graph.dart @@ -147,7 +147,8 @@ class TargetGraph { if (package.isRoot) { defaultInclude = [ - ...defaultRootPackageSources, + ...(testingOverrides?.defaultRootPackageSources ?? + defaultRootPackageSources), ...config.additionalPublicAssets, ].build(); rootPackageConfig = config; diff --git a/build_runner/lib/src/build_plan/testing_overrides.dart b/build_runner/lib/src/build_plan/testing_overrides.dart index baa1738fb0..857f9c88d3 100644 --- a/build_runner/lib/src/build_plan/testing_overrides.dart +++ b/build_runner/lib/src/build_plan/testing_overrides.dart @@ -10,10 +10,8 @@ import 'package:built_collection/built_collection.dart'; import 'package:logging/logging.dart'; import 'package:watcher/watcher.dart'; -import '../build/build_result.dart'; -import '../build/finalized_assets_view.dart'; import '../io/reader_writer.dart'; -import 'build_directory.dart'; +import 'build_phases.dart'; import 'builder_application.dart'; import 'package_graph.dart'; @@ -21,16 +19,10 @@ import 'package_graph.dart'; class TestingOverrides { final BuiltList? builderApplications; final BuiltMap? buildConfig; + final BuildPhases? buildPhases; final Duration? debounceDelay; final BuiltList? defaultRootPackageSources; final DirectoryWatcher Function(String)? directoryWatcherFactory; - final Future Function( - BuildResult, - FinalizedAssetsView, - ReaderWriter readerWriter, - BuiltSet, - )? - finalizeBuild; final void Function(LogRecord)? onLog; final PackageGraph? packageGraph; final ReaderWriter? readerWriter; @@ -41,10 +33,10 @@ class TestingOverrides { const TestingOverrides({ this.builderApplications, this.buildConfig, + this.buildPhases, this.debounceDelay, this.defaultRootPackageSources, this.directoryWatcherFactory, - this.finalizeBuild, this.onLog, this.packageGraph, this.readerWriter, @@ -60,10 +52,10 @@ class TestingOverrides { }) => TestingOverrides( builderApplications: builderApplications ?? this.builderApplications, buildConfig: buildConfig ?? this.buildConfig, + buildPhases: buildPhases, debounceDelay: debounceDelay, defaultRootPackageSources: defaultRootPackageSources, directoryWatcherFactory: directoryWatcherFactory, - finalizeBuild: finalizeBuild, onLog: onLog, packageGraph: packageGraph ?? this.packageGraph, readerWriter: readerWriter, diff --git a/build_runner/lib/src/commands/build_command.dart b/build_runner/lib/src/commands/build_command.dart index cb335e23b8..09ae3bf3b1 100644 --- a/build_runner/lib/src/commands/build_command.dart +++ b/build_runner/lib/src/commands/build_command.dart @@ -56,9 +56,9 @@ class BuildCommand implements BuildRunnerCommand { return BuildResult.buildScriptChanged(); } - final build = await BuildSeries.create(buildPlan: buildPlan); - final result = await build.run({}); - await build.beforeExit(); + final buildSeries = BuildSeries(buildPlan); + final result = await buildSeries.run({}); + await buildSeries.beforeExit(); return result; } } diff --git a/build_runner/lib/src/commands/daemon/asset_server.dart b/build_runner/lib/src/commands/daemon/asset_server.dart index 785c647474..17f21c6337 100644 --- a/build_runner/lib/src/commands/daemon/asset_server.dart +++ b/build_runner/lib/src/commands/daemon/asset_server.dart @@ -34,7 +34,14 @@ class AssetServer { await builder.building; return Response.notFound(''); }) - .add(AssetHandler(builder.reader, rootPackage).handle); + .add( + AssetHandler( + () async => + (await builder.buildSeries.currentBuildResult) + .buildOutputReader, + rootPackage, + ).handle, + ); var pipeline = const Pipeline(); if (options.logRequests) { diff --git a/build_runner/lib/src/commands/daemon/change_providers.dart b/build_runner/lib/src/commands/daemon/change_providers.dart index 179e9bc2c8..99e4877865 100644 --- a/build_runner/lib/src/commands/daemon/change_providers.dart +++ b/build_runner/lib/src/commands/daemon/change_providers.dart @@ -7,9 +7,6 @@ import 'dart:async'; import 'package:build_daemon/change_provider.dart'; import 'package:watcher/watcher.dart' show WatchEvent; -import '../../build/asset_graph/graph.dart'; -import '../../io/asset_tracker.dart'; - /// Continually updates the [changes] stream as watch events are seen on the /// input stream. class AutoChangeProviderImpl implements AutoChangeProvider { @@ -22,16 +19,12 @@ class AutoChangeProviderImpl implements AutoChangeProvider { /// Computes changes with a file scan when requested by a call to /// [collectChanges]. class ManualChangeProviderImpl implements ManualChangeProvider { - final AssetGraph _assetGraph; - final AssetTracker _assetTracker; + final Future> Function() _function; - ManualChangeProviderImpl(this._assetTracker, this._assetGraph); + ManualChangeProviderImpl(this._function); @override Future> collectChanges() async { - final updates = await _assetTracker.collectChanges(_assetGraph); - return List.of( - updates.entries.map((entry) => WatchEvent(entry.value, '${entry.key}')), - ); + return _function(); } } diff --git a/build_runner/lib/src/commands/daemon/daemon_builder.dart b/build_runner/lib/src/commands/daemon/daemon_builder.dart index 8fc0ce423c..ff64bf5220 100644 --- a/build_runner/lib/src/commands/daemon/daemon_builder.dart +++ b/build_runner/lib/src/commands/daemon/daemon_builder.dart @@ -20,12 +20,9 @@ import '../../build/build_series.dart'; import '../../build_plan/build_directory.dart'; import '../../build_plan/build_filter.dart'; import '../../build_plan/build_plan.dart'; -import '../../io/asset_tracker.dart' show AssetTracker; -import '../../io/finalized_reader.dart'; import '../../logging/build_log.dart'; import '../daemon_options.dart'; import '../watch/asset_change.dart'; -import '../watch/change_filter.dart'; import '../watch/collect_changes.dart'; import '../watch/graph_watcher.dart'; import '../watch/node_watcher.dart'; @@ -36,7 +33,7 @@ class BuildRunnerDaemonBuilder implements DaemonBuilder { final _buildResults = StreamController(); final BuildPlan _buildPlan; - final BuildSeries _buildSeries; + final BuildSeries buildSeries; final StreamController _outputStreamController; final ChangeProvider changeProvider; @@ -47,7 +44,7 @@ class BuildRunnerDaemonBuilder implements DaemonBuilder { BuildRunnerDaemonBuilder._( this._buildPlan, - this._buildSeries, + this.buildSeries, this._outputStreamController, this.changeProvider, ) : logs = _outputStreamController.stream.asBroadcastStream(); @@ -59,8 +56,6 @@ class BuildRunnerDaemonBuilder implements DaemonBuilder { @override Stream get builds => _buildResults.stream; - FinalizedReader get reader => _buildSeries.finalizedReader; - final _buildScriptUpdateCompleter = Completer(); Future get buildScriptUpdated => _buildScriptUpdateCompleter.future; @@ -79,15 +74,6 @@ class BuildRunnerDaemonBuilder implements DaemonBuilder { ) .toList(); - if (!_buildPlan.buildOptions.skipBuildScriptCheck && - _buildSeries.buildScriptUpdates!.hasBeenUpdated( - changes.map((change) => change.id).toSet(), - )) { - if (!_buildScriptUpdateCompleter.isCompleted) { - _buildScriptUpdateCompleter.complete(); - } - return; - } final targetNames = targets.map((t) => t.target).toSet(); _logMessage(Level.INFO, 'About to build ${targetNames.toList()}...'); _signalStart(targetNames); @@ -122,11 +108,17 @@ class BuildRunnerDaemonBuilder implements DaemonBuilder { try { final mergedChanges = collectChanges([changes]); - final result = await _buildSeries.run( + final result = await buildSeries.run( mergedChanges, buildDirs: buildDirs.build(), buildFilters: buildFilters.build(), ); + if (result.failureType == core.FailureType.buildScriptChanged) { + if (!_buildScriptUpdateCompleter.isCompleted) { + _buildScriptUpdateCompleter.complete(); + } + return; + } final interestedInOutputs = targets.any( (e) => e is DefaultBuildTarget && e.reportChangedAssets, ); @@ -175,7 +167,7 @@ class BuildRunnerDaemonBuilder implements DaemonBuilder { @override Future stop() async { - await _buildSeries.beforeExit(); + await buildSeries.beforeExit(); } void _logMessage(Level level, String message) => _outputStreamController.add( @@ -233,7 +225,7 @@ class BuildRunnerDaemonBuilder implements DaemonBuilder { ), ); - final buildSeries = await BuildSeries.create(buildPlan: buildPlan); + final buildSeries = BuildSeries(buildPlan); // Only actually used for the AutoChangeProvider. Stream> graphEvents() => PackageGraphWatcher( @@ -242,15 +234,7 @@ class BuildRunnerDaemonBuilder implements DaemonBuilder { ) .watch() .asyncWhere( - (change) => shouldProcess( - change, - buildSeries.assetGraph, - buildPlan.targetGraph, - // Assume we will create an outputDir. - true, - expectedDeletes, - buildPlan.readerWriter, - ), + (change) => buildSeries.shouldProcess(change, expectedDeletes), ) .map((data) => WatchEvent(data.type, '${data.id}')) .debounceBuffer( @@ -261,10 +245,7 @@ class BuildRunnerDaemonBuilder implements DaemonBuilder { final changeProvider = daemonOptions.buildMode == BuildMode.Auto ? AutoChangeProviderImpl(graphEvents()) - : ManualChangeProviderImpl( - AssetTracker(buildPlan.readerWriter, buildPlan.targetGraph), - buildSeries.assetGraph, - ); + : ManualChangeProviderImpl(buildSeries.checkForChanges); return BuildRunnerDaemonBuilder._( buildPlan, diff --git a/build_runner/lib/src/commands/serve/server.dart b/build_runner/lib/src/commands/serve/server.dart index f7c761f79e..3b90409429 100644 --- a/build_runner/lib/src/commands/serve/server.dart +++ b/build_runner/lib/src/commands/serve/server.dart @@ -15,7 +15,7 @@ import 'package:shelf_web_socket/shelf_web_socket.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import '../../build/build_result.dart'; -import '../../io/finalized_reader.dart'; +import '../../io/build_output_reader.dart'; import '../../logging/build_log.dart'; import '../watch/watcher.dart'; import 'path_to_asset_id.dart'; @@ -35,37 +35,21 @@ enum PerfSortOrder { innerDurationDesc, } -ServeHandler createServeHandler(Watcher watch) { - final rootPackage = watch.packageGraph.root.name; - final assetHandlerCompleter = Completer(); - watch.finalizedReader - .then((reader) async { - assetHandlerCompleter.complete(AssetHandler(reader, rootPackage)); - }) - .catchError((_) {}); // These errors are separately handled. - return ServeHandler._(watch, assetHandlerCompleter.future, rootPackage); -} - -class ServeHandler implements BuildState { - final Watcher _state; - final String _rootPackage; - - final Future _assetHandler; +class ServeHandler { + final Watcher _watcher; final BuildUpdatesWebSocketHandler _webSocketHandler; - ServeHandler._(this._state, this._assetHandler, this._rootPackage) - : _webSocketHandler = BuildUpdatesWebSocketHandler(_state) { - _state.buildResults + ServeHandler(this._watcher) + : _webSocketHandler = BuildUpdatesWebSocketHandler() { + _watcher.buildResults .listen(_webSocketHandler.emitUpdateMessage) .onDone(_webSocketHandler.close); } - @override - Future? get currentBuild => _state.currentBuild; + Future? get currentBuildResult => _watcher.currentBuildResult; - @override - Stream get buildResults => _state.buildResults; + Stream get buildResults => _watcher.buildResults; shelf.Handler handlerFor( String rootDir, { @@ -79,12 +63,6 @@ class ServeHandler implements BuildState { 'Only top level directories such as `web` or `test` can be served, got', ); } - _state.currentBuild?.then((_) { - // If the first build fails with a handled exception, we might not have - // an asset graph and can't do this check. - if (_state.assetGraph == null) return; - _warnForEmptyDirectory(rootDir); - }); var cascade = shelf.Cascade(); if (liveReload) { cascade = cascade.add(_webSocketHandler.createHandlerByRootDir(rootDir)); @@ -95,7 +73,10 @@ class ServeHandler implements BuildState { if (request.url.path == _assetsDigestPath) { return _assetsDigestHandler(request, rootDir); } - final assetHandler = await _assetHandler; + final assetHandler = AssetHandler( + () async => (await _watcher.currentBuildResult).buildOutputReader, + _watcher.packageGraph.root.name, + ); return assetHandler.handle(request, rootDir: rootDir); }); var pipeline = const shelf.Pipeline(); @@ -109,7 +90,7 @@ class ServeHandler implements BuildState { } Future _blockOnCurrentBuild(void _) async { - await currentBuild; + await currentBuildResult; return shelf.Response.notFound(''); } @@ -117,10 +98,11 @@ class ServeHandler implements BuildState { shelf.Request request, String rootDir, ) async { - final reader = await _state.finalizedReader; + final buildResult = await _watcher.currentBuildResult; + final reader = buildResult.buildOutputReader; final assertPathList = (jsonDecode(await request.readAsString()) as List).cast(); - final rootPackage = _state.packageGraph.root.name; + final rootPackage = _watcher.packageGraph.root.name; final results = {}; for (final path in assertPathList) { final assetIds = pathToAssetIds(rootPackage, rootDir, p.url.split(path)); @@ -148,18 +130,6 @@ class ServeHandler implements BuildState { headers: {HttpHeaders.contentTypeHeader: 'application/json'}, ); } - - void _warnForEmptyDirectory(String rootDir) { - if (!_state.assetGraph! - .packageNodes(_rootPackage) - .any((n) => n.id.path.startsWith('$rootDir/'))) { - buildLog.warning( - 'Requested a server for `$rootDir` but this directory ' - 'has no assets in the build. You may need to add some sources or ' - 'include this directory in some target in your `build.yaml`.', - ); - } - } } /// Class that manages web socket connection handler to inform clients about @@ -172,12 +142,8 @@ class BuildUpdatesWebSocketHandler { }) _handlerFactory; final _internalHandlers = {}; - final Watcher _state; - BuildUpdatesWebSocketHandler( - this._state, [ - this._handlerFactory = webSocketHandler, - ]); + BuildUpdatesWebSocketHandler([this._handlerFactory = webSocketHandler]); shelf.Handler createHandlerByRootDir(String rootDir) { if (!_internalHandlers.containsKey(rootDir)) { @@ -193,7 +159,7 @@ class BuildUpdatesWebSocketHandler { Future emitUpdateMessage(BuildResult buildResult) async { if (buildResult.status != BuildStatus.success) return; - final reader = await _state.finalizedReader; + final reader = buildResult.buildOutputReader; final digests = {}; for (final assetId in buildResult.outputs) { final digest = await reader.digest(assetId); @@ -279,7 +245,7 @@ window.\$dartLoader.forceLoadModule('packages/build_runner/src/commands/serve/$s '''; class AssetHandler { - final FinalizedReader _reader; + final Future Function() _reader; final String _rootPackage; final _typeResolver = MimeTypeResolver(); @@ -306,10 +272,12 @@ class AssetHandler { List assetIds, { bool fallbackToDirectoryList = false, }) async { + final reader = await _reader(); + // Use the first of [assetIds] that exists. AssetId? assetId; for (final id in assetIds) { - if (await _reader.canRead(id)) { + if (await reader.canRead(id)) { assetId = id; break; } @@ -319,8 +287,8 @@ class AssetHandler { try { try { - if (!await _reader.canRead(assetId)) { - final reason = await _reader.unreadableReason(assetId); + if (!await reader.canRead(assetId)) { + final reason = await reader.unreadableReason(assetId); switch (reason) { case UnreadableReason.failed: return shelf.Response.internalServerError( @@ -344,7 +312,7 @@ class AssetHandler { return shelf.Response.notFound('Not Found'); } - final etag = base64.encode((await _reader.digest(assetId)).bytes); + final etag = base64.encode((await reader.digest(assetId)).bytes); var contentType = _typeResolver.lookup(assetId.path); if (contentType == 'text/x-dart') { contentType = '$contentType; charset=utf-8'; @@ -366,7 +334,7 @@ class AssetHandler { } List? body; if (request.method != 'HEAD') { - body = await _reader.readAsBytes(assetId); + body = await reader.readAsBytes(assetId); headers[HttpHeaders.contentLengthHeader] = '${body.length}'; } return shelf.Response.ok(body, headers: headers); @@ -384,8 +352,10 @@ class AssetHandler { Future _findDirectoryList(AssetId from) async { final directoryPath = p.url.dirname(from.path); final glob = p.url.join(directoryPath, '*'); + final reader = await _reader(); + final result = - await _reader.assetFinder.find(Glob(glob)).map((a) => a.path).toList(); + await reader.assetFinder.find(Glob(glob)).map((a) => a.path).toList(); final message = StringBuffer('Could not find ${from.path}'); if (result.isEmpty) { message.write(' or any files in $directoryPath. '); diff --git a/build_runner/lib/src/commands/serve_command.dart b/build_runner/lib/src/commands/serve_command.dart index c2bebdbdb7..19a4c825bb 100644 --- a/build_runner/lib/src/commands/serve_command.dart +++ b/build_runner/lib/src/commands/serve_command.dart @@ -102,7 +102,7 @@ class ServeCommand implements BuildRunnerCommand { } } - await handler.currentBuild; + await handler.currentBuildResult; return await completer.future; } finally { await Future.wait( diff --git a/build_runner/lib/src/commands/watch/change_filter.dart b/build_runner/lib/src/commands/watch/change_filter.dart deleted file mode 100644 index ca5b915eca..0000000000 --- a/build_runner/lib/src/commands/watch/change_filter.dart +++ /dev/null @@ -1,51 +0,0 @@ -// 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:watcher/watcher.dart'; - -import '../../build/asset_graph/graph.dart'; -import '../../build/asset_graph/node.dart'; -import '../../build_plan/target_graph.dart'; -import '../../constants.dart'; -import '../../io/reader_writer.dart'; -import 'asset_change.dart'; - -/// Returns if a given asset change should be considered for building. -FutureOr shouldProcess( - AssetChange change, - AssetGraph assetGraph, - TargetGraph targetGraph, - bool willCreateOutputDir, - Set expectedDeletes, - ReaderWriter readerWriter, -) { - if (_isCacheFile(change) && !assetGraph.contains(change.id)) return false; - final node = assetGraph.get(change.id); - if (node != null) { - if (!willCreateOutputDir && !node.changesRequireRebuild) return false; - if (_isAddOrEditOnGeneratedFile(node, change.type)) return false; - if (change.type == ChangeType.MODIFY) { - // Was it really modified or just touched? - readerWriter.cache.invalidate([change.id]); - return readerWriter - .digest(change.id) - .then((newDigest) => node.digest != newDigest); - } - } else { - if (change.type != ChangeType.ADD) return false; - if (!targetGraph.anyMatchesAsset(change.id)) return false; - } - if (_isExpectedDelete(change, expectedDeletes)) return false; - return true; -} - -bool _isAddOrEditOnGeneratedFile(AssetNode node, ChangeType changeType) => - node.type == NodeType.generated && changeType != ChangeType.REMOVE; - -bool _isCacheFile(AssetChange change) => change.id.path.startsWith(cacheDir); - -bool _isExpectedDelete(AssetChange change, Set expectedDeletes) => - expectedDeletes.remove(change.id); diff --git a/build_runner/lib/src/commands/watch/watcher.dart b/build_runner/lib/src/commands/watch/watcher.dart index 2159e6d729..39dc46661c 100644 --- a/build_runner/lib/src/commands/watch/watcher.dart +++ b/build_runner/lib/src/commands/watch/watcher.dart @@ -8,54 +8,34 @@ import 'package:build/build.dart'; import 'package:crypto/crypto.dart'; import 'package:stream_transform/stream_transform.dart'; -import '../../build/asset_graph/graph.dart'; import '../../build/build_result.dart'; import '../../build/build_series.dart'; import '../../build_plan/build_options.dart'; import '../../build_plan/build_plan.dart'; import '../../build_plan/package_graph.dart'; import '../../build_plan/testing_overrides.dart'; -import '../../exceptions.dart'; -import '../../io/finalized_reader.dart'; +import '../../io/build_output_reader.dart'; import '../../io/reader_writer.dart'; import '../../logging/build_log.dart'; import 'asset_change.dart'; -import 'change_filter.dart'; import 'collect_changes.dart'; import 'graph_watcher.dart'; import 'node_watcher.dart'; -class Watcher implements BuildState { +class Watcher { late final BuildPlan buildPlan; BuildSeries? _buildSeries; - AssetGraph? get assetGraph => _buildSeries?.assetGraph; - - /// 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(); - @override - Future? currentBuild; + late Future currentBuildResult; /// Pending expected delete events from the build. final Set _expectedDeletes = {}; - final _readerCompleter = Completer(); - - /// Completes with an error if we fail to initialize. - Future get finalizedReader => _readerCompleter.future; - - Watcher({ - required BuildPlan buildPlan, - required Future until, - required this.willCreateOutputDirs, - }) { + Watcher({required BuildPlan buildPlan, required Future until}) { this.buildPlan = buildPlan.copyWith( readerWriter: buildPlan.readerWriter.copyWith( onDelete: _expectedDeletes.add, @@ -69,7 +49,6 @@ class Watcher implements BuildState { PackageGraph get packageGraph => buildPlan.packageGraph; ReaderWriter get readerWriter => buildPlan.readerWriter; - @override late final Stream buildResults; /// Runs a build any time relevant files change. @@ -79,29 +58,16 @@ class Watcher implements BuildState { /// File watchers are scheduled synchronously. Stream _run(Future until) { final firstBuildCompleter = Completer(); - currentBuild = firstBuildCompleter.future; + currentBuildResult = firstBuildCompleter.future; final controller = StreamController(); Future doBuild(List> changes) async { buildLog.nextBuild(); - final build = _buildSeries!; + final buildSeries = _buildSeries!; final mergedChanges = collectChanges(changes); _expectedDeletes.clear(); - if (!buildOptions.skipBuildScriptCheck) { - if (build.buildScriptUpdates!.hasBeenUpdated( - mergedChanges.keys.toSet(), - )) { - _terminateCompleter.complete(); - buildLog.error('Terminating builds due to build script update.'); - return BuildResult( - BuildStatus.failure, - [], - failureType: FailureType.buildScriptChanged, - ); - } - } - return build.run(mergedChanges); + return buildSeries.run(mergedChanges); } final terminate = Future.any([until, _terminateCompleter.future]).then((_) { @@ -152,9 +118,9 @@ class Watcher implements BuildState { _isPackageBuildYamlOverride(id)) { controller.add( BuildResult( - BuildStatus.failure, - [], + status: BuildStatus.failure, failureType: FailureType.buildConfigChanged, + buildOutputReader: BuildOutputReader.empty(), ), ); @@ -167,31 +133,23 @@ class Watcher implements BuildState { return change; }) .asyncWhere((change) { - assert(_readerCompleter.isCompleted); - return shouldProcess( - change, - assetGraph!, - buildPlan.targetGraph, - willCreateOutputDirs, - _expectedDeletes, - readerWriter, - ); + return _buildSeries!.shouldProcess(change, _expectedDeletes); }) .debounceBuffer( testingOverrides.debounceDelay ?? const Duration(milliseconds: 250), ) .takeUntil(terminate) - .asyncMapBuffer( - (changes) => - currentBuild = doBuild(changes) - ..whenComplete(() => currentBuild = null), - ) + .asyncMapBuffer((changes) => currentBuildResult = doBuild(changes)) .listen((BuildResult result) { + if (result.failureType == FailureType.buildScriptChanged) { + _terminateCompleter.complete(); + buildLog.error('Terminating builds due to build script update.'); + } if (controller.isClosed) return; controller.add(result); }) .onDone(() async { - await currentBuild; + await currentBuildResult; await _buildSeries?.beforeExit(); if (!controller.isClosed) await controller.close(); buildLog.info('Builds finished. Safe to exit\n'); @@ -215,21 +173,8 @@ class Watcher implements BuildState { BuildResult firstBuild; BuildSeries? build; - try { - build = _buildSeries = await BuildSeries.create(buildPlan: buildPlan); - - firstBuild = await build.run({}); - } on CannotBuildException catch (e, s) { - _terminateCompleter.complete(); - - firstBuild = BuildResult(BuildStatus.failure, []); - _readerCompleter.completeError(e, s); - } - - if (build != null) { - assert(!_readerCompleter.isCompleted); - _readerCompleter.complete(build.finalizedReader); - } + build = _buildSeries = BuildSeries(buildPlan); + firstBuild = await build.run({}); // 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); diff --git a/build_runner/lib/src/commands/watch_command.dart b/build_runner/lib/src/commands/watch_command.dart index 11cebe861c..f08bcc331b 100644 --- a/build_runner/lib/src/commands/watch_command.dart +++ b/build_runner/lib/src/commands/watch_command.dart @@ -63,9 +63,6 @@ class WatchCommand implements BuildRunnerCommand { final watcher = Watcher( buildPlan: buildPlan, until: terminator.shouldTerminate, - willCreateOutputDirs: buildOptions.buildDirs.any( - (target) => target.outputLocation?.path.isNotEmpty ?? false, - ), ); unawaited( @@ -74,7 +71,7 @@ class WatchCommand implements BuildRunnerCommand { }), ); - return createServeHandler(watcher); + return ServeHandler(watcher); } } diff --git a/build_runner/lib/src/io/build_output_reader.dart b/build_runner/lib/src/io/build_output_reader.dart new file mode 100644 index 0000000000..10f731fd51 --- /dev/null +++ b/build_runner/lib/src/io/build_output_reader.dart @@ -0,0 +1,230 @@ +// 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 'package:build/build.dart'; +import 'package:crypto/crypto.dart'; +import 'package:glob/glob.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; + +import '../build/asset_graph/graph.dart'; +import '../build/asset_graph/node.dart'; +import '../build/build_dirs.dart'; +import '../build_plan/build_plan.dart'; +import 'asset_finder.dart'; +import 'reader_writer.dart'; + +/// A view of the build output. +/// +/// If [canRead] returns false, [unreadableReason] explains why the file is +/// missing; for example, it might say that generation failed. +/// +/// Files are only visible if they were a required part of the build, even if +/// they exist on disk from a previous build. +class BuildOutputReader { + late final AssetFinder assetFinder = FunctionAssetFinder(_findAssets); + + final BuildPlan? _buildPlan; + final AssetGraph? _assetGraph; + final ReaderWriter? _readerWriter; + + /// Results of checking if an output is required. + final Map _checkedOutputs = {}; + + /// For an unexpected failure condition, a fully empty output. + BuildOutputReader.empty() + : _assetGraph = null, + _buildPlan = null, + _readerWriter = null; + + /// For testing: a build output that does not check build phases to determine + /// whether outputs were required. + @visibleForTesting + BuildOutputReader.graphOnly({ + required ReaderWriter readerWriter, + required AssetGraph assetGraph, + }) : _buildPlan = null, + _assetGraph = assetGraph, + _readerWriter = readerWriter; + + BuildOutputReader({ + required BuildPlan buildPlan, + required ReaderWriter readerWriter, + required AssetGraph assetGraph, + }) : _readerWriter = readerWriter, + _assetGraph = assetGraph, + _buildPlan = buildPlan; + + /// Returns a reason why [id] is not readable, or null if it is readable. + Future unreadableReason(AssetId id) async { + if (_assetGraph == null || _readerWriter == null) { + return UnreadableReason.notFound; + } + if (!_assetGraph.contains(id)) { + return UnreadableReason.notFound; + } + final node = _assetGraph.get(id)!; + if (!isRequired(node.id)) { + return UnreadableReason.notOutput; + } + if (node.isDeleted) return UnreadableReason.deleted; + if (!node.isFile) return UnreadableReason.assetType; + + if (node.type == NodeType.generated) { + final nodeState = node.generatedNodeState!; + if (nodeState.result == false) return UnreadableReason.failed; + if (!node.wasOutput) return UnreadableReason.notOutput; + // No need to explicitly check readability for generated files, their + // readability is recorded in the node state. + return null; + } + + if (node.isTrackedInput && await _readerWriter.canRead(id)) return null; + return UnreadableReason.unknown; + } + + Future canRead(AssetId id) async => + (await unreadableReason(id)) == null; + + Future digest(AssetId id) async { + final unreadableReason = await this.unreadableReason(id); + // Do provide digests for generated files that are known but not output + // or known to be deleted. `build serve` uses these digests, which + // reflect that the file is missing. + if (unreadableReason != null && + unreadableReason != UnreadableReason.notOutput && + unreadableReason != UnreadableReason.deleted) { + throw AssetNotFoundException(id); + } + return _ensureDigest(id); + } + + Future> readAsBytes(AssetId id) => _readerWriter!.readAsBytes(id); + + Stream _findAssets(Glob glob, String? _) async* { + if (_assetGraph == null || _readerWriter == null) return; + final potentialNodes = + _assetGraph + .packageNodes(_readerWriter.rootPackage) + .where((n) => glob.matches(n.id.path)) + .toList(); + final potentialIds = potentialNodes.map((n) => n.id).toList(); + + for (final id in potentialIds) { + if (await _readerWriter.canRead(id)) { + yield id; + } + } + } + + /// Returns the `lastKnownDigest` of [id], computing and caching it if + /// necessary. + /// + /// Note that [id] must exist in the asset graph. + FutureOr _ensureDigest(AssetId id) { + final node = _assetGraph!.get(id)!; + if (node.digest != null) return node.digest!; + return _readerWriter!.digest(id).then((digest) { + _assetGraph.updateNode(id, (nodeBuilder) { + nodeBuilder.digest = digest; + }); + return digest; + }); + } + + /// A lazily computed view of all the assets available after a build. + List allAssets({String? rootDir}) { + if (_assetGraph == null) return []; + return _assetGraph.allNodes + .map((node) { + if (_shouldSkipNode(node, rootDir)) { + return null; + } + return node.id; + }) + .whereType() + .toList(); + } + + bool _shouldSkipNode(AssetNode node, String? rootDir) { + if (_buildPlan == null) return false; + if (!node.isFile) return true; + if (node.isDeleted) return true; + + // Exclude non-lib assets if they're outside of the root directory or not + // from the root package. + if (!node.id.path.startsWith('lib/')) { + if (rootDir != null && !p.isWithin(rootDir, node.id.path)) return true; + if (node.id.package != _buildPlan.packageGraph.root.name) return true; + } + + if (node.type == NodeType.internal || node.type == NodeType.glob) { + return true; + } + if (node.type == NodeType.generated) { + if (!node.wasOutput || node.generatedNodeState!.result == false) { + return true; + } + return !isRequired(node.id); + } + if (node.id.path == '.packages') return true; + if (node.id.path == '.dart_tool/package_config.json') return true; + return false; + } + + /// Returns whether [output] was required. + /// + /// Non-required outputs might be present from a previous build, but they + /// should not be served or copied to a merged output directory. + bool isRequired(AssetId output, [Set? currentlyChecking]) { + if (_buildPlan == null) return true; + if (_assetGraph == null) return true; + + currentlyChecking ??= {}; + if (currentlyChecking.contains(output)) return false; + currentlyChecking.add(output); + + final node = _assetGraph.get(output)!; + if (node.type != NodeType.generated) return true; + final nodeConfiguration = node.generatedNodeConfiguration!; + final phase = _buildPlan.buildPhases[nodeConfiguration.phaseNumber]; + if (!phase.isOptional && + shouldBuildForDirs( + output, + buildDirs: _buildPlan.buildOptions.buildDirs, + buildFilters: _buildPlan.buildOptions.buildFilters, + phase: phase, + targetGraph: _buildPlan.targetGraph, + )) { + return true; + } + return _checkedOutputs.putIfAbsent( + output, + () => + (_assetGraph.computeOutputs()[node.id] ?? {}).any( + (o) => isRequired(o, currentlyChecking), + ) || + _assetGraph + .outputsForPhase(output.package, nodeConfiguration.phaseNumber) + .where( + (n) => + n.generatedNodeConfiguration!.primaryInput == + node.generatedNodeConfiguration!.primaryInput, + ) + .map((n) => n.id) + .any((o) => isRequired(o, currentlyChecking)), + ); + } +} + +enum UnreadableReason { + notFound, + notOutput, + assetType, + deleted, + failed, + unknown, +} diff --git a/build_runner/lib/src/io/create_merged_dir.dart b/build_runner/lib/src/io/create_merged_dir.dart index 8abe48d570..12334bd301 100644 --- a/build_runner/lib/src/io/create_merged_dir.dart +++ b/build_runner/lib/src/io/create_merged_dir.dart @@ -11,11 +11,11 @@ import 'package:built_collection/built_collection.dart'; import 'package:path/path.dart' as p; import 'package:pool/pool.dart'; -import '../build/finalized_assets_view.dart'; import '../build_plan/build_directory.dart'; import '../build_plan/package_graph.dart'; import '../logging/build_log.dart'; import '../logging/timed_activities.dart'; +import 'build_output_reader.dart'; import 'filesystem.dart'; import 'reader_writer.dart'; @@ -28,16 +28,17 @@ const _manifestSeparator = '\n'; /// Creates merged output directories for each [OutputLocation]. /// /// Returns whether it succeeded or not. -Future createMergedOutputDirectories( - BuiltSet buildDirs, - PackageGraph packageGraph, - ReaderWriter reader, - FinalizedAssetsView finalizedAssetsView, - bool outputSymlinksOnly, -) async { +Future createMergedOutputDirectories({ + required BuiltSet buildDirs, + required PackageGraph packageGraph, + required bool outputSymlinksOnly, + required BuildOutputReader buildOutputReader, + required ReaderWriter readerWriter, +}) async { buildLog.doing('Writing the output directory.'); + return await TimedActivity.write.runAsync(() async { - if (outputSymlinksOnly && reader.filesystem is! IoFilesystem) { + if (outputSymlinksOnly && readerWriter.filesystem is! IoFilesystem) { buildLog.error( 'The current environment does not support symlinks, but symlinks were ' 'requested.', @@ -57,14 +58,14 @@ Future createMergedOutputDirectories( final outputLocation = target.outputLocation; if (outputLocation != null) { if (!await _createMergedOutputDir( - outputLocation.path, - target.directory, - packageGraph, - reader, - finalizedAssetsView, + buildOutputReader: buildOutputReader, + packageGraph: packageGraph, + outputPath: outputLocation.path, + root: target.directory, // TODO(grouma) - retrieve symlink information from target only. - outputSymlinksOnly || outputLocation.useSymlinks, - outputLocation.hoist, + symlinkOnly: outputSymlinksOnly || outputLocation.useSymlinks, + hoist: outputLocation.hoist, + readerWriter: readerWriter, )) { return false; } @@ -84,17 +85,16 @@ Set _conflicts(BuiltSet buildDirs) { return conflicts; } -Future _createMergedOutputDir( - String outputPath, - String? root, - PackageGraph packageGraph, - ReaderWriter readerWriter, - FinalizedAssetsView finalizedOutputsView, - bool symlinkOnly, - bool hoist, -) async { +Future _createMergedOutputDir({ + required String outputPath, + required String root, + required PackageGraph packageGraph, + required bool symlinkOnly, + required bool hoist, + required BuildOutputReader buildOutputReader, + required ReaderWriter readerWriter, +}) async { try { - if (root == null) return false; final absoluteRoot = p.join(packageGraph.root.path, root); if (absoluteRoot != packageGraph.root.path && !p.isWithin(packageGraph.root.path, absoluteRoot)) { @@ -108,7 +108,7 @@ Future _createMergedOutputDir( if (outputDirExists) { if (!await _cleanUpOutputDir(outputDir)) return false; } - final builtAssets = finalizedOutputsView.allAssets(rootDir: root).toList(); + final builtAssets = buildOutputReader.allAssets(rootDir: root).toList(); if (root != '' && !builtAssets .where((id) => id.package == packageGraph.root.name) diff --git a/build_runner/lib/src/io/finalized_reader.dart b/build_runner/lib/src/io/finalized_reader.dart deleted file mode 100644 index 12f78c83ab..0000000000 --- a/build_runner/lib/src/io/finalized_reader.dart +++ /dev/null @@ -1,141 +0,0 @@ -// 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:convert'; - -import 'package:build/build.dart'; -import 'package:built_collection/built_collection.dart'; -import 'package:crypto/crypto.dart'; -import 'package:glob/glob.dart'; - -import '../build/asset_graph/graph.dart'; -import '../build/asset_graph/node.dart'; -import '../build/optional_output_tracker.dart'; -import '../build_plan/build_filter.dart'; -import '../build_plan/build_phases.dart'; -import '../build_plan/target_graph.dart'; -import 'asset_finder.dart'; -import 'reader_writer.dart'; - -/// A view of the build output. -/// -/// If [canRead] returns false, [unreadableReason] explains why the file is -/// missing; for example, it might say that generation failed. -class FinalizedReader { - late final AssetFinder assetFinder = FunctionAssetFinder(_findAssets); - - final ReaderWriter _delegate; - final AssetGraph _assetGraph; - final TargetGraph _targetGraph; - OptionalOutputTracker? _optionalOutputTracker; - final String _rootPackage; - final BuildPhases _buildPhases; - - void reset(BuiltSet buildDirs, BuiltSet buildFilters) { - _optionalOutputTracker = OptionalOutputTracker( - _assetGraph, - _targetGraph, - buildDirs, - buildFilters, - _buildPhases, - ); - } - - FinalizedReader( - this._delegate, - this._assetGraph, - this._targetGraph, - this._buildPhases, - this._rootPackage, - ); - - /// Returns a reason why [id] is not readable, or null if it is readable. - Future unreadableReason(AssetId id) async { - if (!_assetGraph.contains(id)) return UnreadableReason.notFound; - final node = _assetGraph.get(id)!; - if (_optionalOutputTracker != null && - !_optionalOutputTracker!.isRequired(node.id)) { - return UnreadableReason.notOutput; - } - if (node.isDeleted) return UnreadableReason.deleted; - if (!node.isFile) return UnreadableReason.assetType; - - if (node.type == NodeType.generated) { - final nodeState = node.generatedNodeState!; - if (nodeState.result == false) return UnreadableReason.failed; - if (!node.wasOutput) return UnreadableReason.notOutput; - // No need to explicitly check readability for generated files, their - // readability is recorded in the node state. - return null; - } - - if (node.isTrackedInput && await _delegate.canRead(id)) return null; - return UnreadableReason.unknown; - } - - Future canRead(AssetId id) async => - (await unreadableReason(id)) == null; - - Future digest(AssetId id) async { - final unreadableReason = await this.unreadableReason(id); - // Do provide digests for generated files that are known but not output - // or known to be deleted. `build serve` uses these digests, which - // reflect that the file is missing. - if (unreadableReason != null && - unreadableReason != UnreadableReason.notOutput && - unreadableReason != UnreadableReason.deleted) { - throw AssetNotFoundException(id); - } - return _ensureDigest(id); - } - - Future> readAsBytes(AssetId id) => _delegate.readAsBytes(id); - - Future readAsString(AssetId id, {Encoding encoding = utf8}) async { - if (_assetGraph.get(id)?.isDeleted ?? true) { - throw AssetNotFoundException(id); - } - return _delegate.readAsString(id, encoding: encoding); - } - - Stream _findAssets(Glob glob, String? _) async* { - final potentialNodes = - _assetGraph - .packageNodes(_rootPackage) - .where((n) => glob.matches(n.id.path)) - .toList(); - final potentialIds = potentialNodes.map((n) => n.id).toList(); - - for (final id in potentialIds) { - if (await _delegate.canRead(id)) { - yield id; - } - } - } - - /// Returns the `lastKnownDigest` of [id], computing and caching it if - /// necessary. - /// - /// Note that [id] must exist in the asset graph. - FutureOr _ensureDigest(AssetId id) { - final node = _assetGraph.get(id)!; - if (node.digest != null) return node.digest!; - return _delegate.digest(id).then((digest) { - _assetGraph.updateNode(id, (nodeBuilder) { - nodeBuilder.digest = digest; - }); - return digest; - }); - } -} - -enum UnreadableReason { - notFound, - notOutput, - assetType, - deleted, - failed, - unknown, -} diff --git a/build_runner/pubspec.yaml b/build_runner/pubspec.yaml index c29f0853fa..e3bbb25dd3 100644 --- a/build_runner/pubspec.yaml +++ b/build_runner/pubspec.yaml @@ -53,7 +53,6 @@ dev_dependencies: stream_channel: ^2.0.0 test: ^1.25.5 test_descriptor: ^2.0.0 - test_process: ^2.0.0 topics: - build-runner diff --git a/build_runner/test/commands/serve/asset_handler_test.dart b/build_runner/test/commands/serve/asset_handler_test.dart index 6ffee62bc3..8d7102467a 100644 --- a/build_runner/test/commands/serve/asset_handler_test.dart +++ b/build_runner/test/commands/serve/asset_handler_test.dart @@ -9,9 +9,8 @@ import 'package:build_runner/src/build/asset_graph/graph.dart'; import 'package:build_runner/src/build/asset_graph/node.dart'; import 'package:build_runner/src/build/asset_graph/post_process_build_step_id.dart'; import 'package:build_runner/src/build_plan/build_phases.dart'; -import 'package:build_runner/src/build_plan/target_graph.dart'; import 'package:build_runner/src/commands/serve/server.dart'; -import 'package:build_runner/src/io/finalized_reader.dart'; +import 'package:build_runner/src/io/build_output_reader.dart'; import 'package:crypto/crypto.dart'; import 'package:shelf/shelf.dart'; import 'package:test/test.dart'; @@ -20,28 +19,24 @@ import '../../common/common.dart'; void main() { late AssetHandler handler; - late FinalizedReader reader; - late InternalTestReaderWriter delegate; - late AssetGraph graph; + late BuildOutputReader reader; + late InternalTestReaderWriter readerWriter; + late AssetGraph assetGraph; setUp(() async { - graph = await AssetGraph.build( + assetGraph = await AssetGraph.build( BuildPhases([]), {}, {}, buildPackageGraph({rootPackage('a'): []}), InternalTestReaderWriter(), ); - delegate = InternalTestReaderWriter(); - final packageGraph = buildPackageGraph({rootPackage('a'): []}); - reader = FinalizedReader( - delegate, - graph, - await TargetGraph.forPackageGraph(packageGraph: packageGraph), - BuildPhases([]), - 'a', + readerWriter = InternalTestReaderWriter(); + reader = BuildOutputReader.graphOnly( + readerWriter: readerWriter, + assetGraph: assetGraph, ); - handler = AssetHandler(reader, 'a'); + handler = AssetHandler(() async => reader, 'a'); }); void addAsset(String id, String content, {bool deleted = false}) { @@ -54,8 +49,8 @@ void main() { ); }); } - graph.add(node); - delegate.testing.writeString(node.id, content); + assetGraph.add(node); + readerWriter.testing.writeString(node.id, content); } test('can not read deleted nodes', () async { @@ -132,7 +127,7 @@ void main() { }); test('Fails request for failed outputs', () async { - graph.add( + assetGraph.add( AssetNode.generated( AssetId('a', 'web/main.ddc.js'), phaseNumber: 0, diff --git a/build_runner/test/commands/serve/serve_handler_test.dart b/build_runner/test/commands/serve/serve_handler_test.dart index 6764ce23dd..0fcf7ff809 100644 --- a/build_runner/test/commands/serve/serve_handler_test.dart +++ b/build_runner/test/commands/serve/serve_handler_test.dart @@ -14,10 +14,10 @@ import 'package:build_runner/src/build/asset_graph/post_process_build_step_id.da import 'package:build_runner/src/build/build_result.dart'; import 'package:build_runner/src/build_plan/build_phases.dart'; import 'package:build_runner/src/build_plan/package_graph.dart'; -import 'package:build_runner/src/build_plan/target_graph.dart'; import 'package:build_runner/src/commands/serve/server.dart'; import 'package:build_runner/src/commands/watch/watcher.dart'; -import 'package:build_runner/src/io/finalized_reader.dart'; +import 'package:build_runner/src/io/build_output_reader.dart'; +import 'package:built_collection/built_collection.dart'; import 'package:crypto/crypto.dart'; import 'package:logging/logging.dart'; import 'package:shelf/shelf.dart'; @@ -104,10 +104,12 @@ void main() { late ServeHandler serveHandler; late InternalTestReaderWriter readerWriter; late MockWatchImpl watchImpl; + late PackageGraph packageGraph; late AssetGraph assetGraph; + late BuildOutputReader finalizedReader; setUp(() async { - final packageGraph = buildPackageGraph({rootPackage('a'): []}); + packageGraph = buildPackageGraph({rootPackage('a'): []}); readerWriter = InternalTestReaderWriter( rootPackage: packageGraph.root.name, ); @@ -118,22 +120,19 @@ void main() { packageGraph, readerWriter, ); - watchImpl = MockWatchImpl( + watchImpl = MockWatchImpl(packageGraph); + serveHandler = ServeHandler(watchImpl); + finalizedReader = BuildOutputReader.graphOnly( + readerWriter: readerWriter, + assetGraph: assetGraph, + ); + watchImpl.addFutureResult( Future.value( - FinalizedReader( - readerWriter, - assetGraph, - await TargetGraph.forPackageGraph(packageGraph: packageGraph), - BuildPhases([]), - 'a', + BuildResult( + status: BuildStatus.success, + buildOutputReader: finalizedReader, ), ), - packageGraph, - assetGraph, - ); - serveHandler = createServeHandler(watchImpl); - watchImpl.addFutureResult( - Future.value(BuildResult(BuildStatus.success, [])), ); }); @@ -266,7 +265,12 @@ void main() { ), ); watchImpl.addFutureResult( - Future.value(BuildResult(BuildStatus.failure, [])), + Future.value( + BuildResult( + status: BuildStatus.failure, + buildOutputReader: finalizedReader, + ), + ), ); }); @@ -463,7 +467,7 @@ void main() { onConnect(serverChannel, ''); }; - handler = BuildUpdatesWebSocketHandler(watchImpl, mockHandlerFactory); + handler = BuildUpdatesWebSocketHandler(mockHandlerFactory); (serverChannel1, clientChannel1) = createFakes(); (serverChannel2, clientChannel2) = createFakes(); @@ -481,7 +485,12 @@ void main() { expect(clientChannel2.stream, emitsInOrder(['{}', emitsDone])); await createMockConnection(serverChannel1, 'web'); await createMockConnection(serverChannel2, 'web'); - await handler.emitUpdateMessage(BuildResult(BuildStatus.success, [])); + await handler.emitUpdateMessage( + BuildResult( + status: BuildStatus.success, + buildOutputReader: finalizedReader, + ), + ); await clientChannel1.sink.close(); await clientChannel2.sink.close(); }); @@ -491,16 +500,31 @@ void main() { expect(clientChannel2.stream, emitsInOrder(['{}', emitsDone])); await createMockConnection(serverChannel1, 'web'); await createMockConnection(serverChannel2, 'web'); - await handler.emitUpdateMessage(BuildResult(BuildStatus.success, [])); + await handler.emitUpdateMessage( + BuildResult( + status: BuildStatus.success, + buildOutputReader: finalizedReader, + ), + ); await clientChannel2.sink.close(); - await handler.emitUpdateMessage(BuildResult(BuildStatus.success, [])); + await handler.emitUpdateMessage( + BuildResult( + status: BuildStatus.success, + buildOutputReader: finalizedReader, + ), + ); await clientChannel1.sink.close(); }); test('emits only on successful builds', () async { expect(clientChannel1.stream, emitsDone); await createMockConnection(serverChannel1, 'web'); - await handler.emitUpdateMessage(BuildResult(BuildStatus.failure, [])); + await handler.emitUpdateMessage( + BuildResult( + status: BuildStatus.failure, + buildOutputReader: finalizedReader, + ), + ); await clientChannel1.sink.close(); }); @@ -536,13 +560,22 @@ void main() { ); await createMockConnection(serverChannel1, 'web'); await handler.emitUpdateMessage( - BuildResult(BuildStatus.success, [AssetId('a', 'web/index.html')]), + BuildResult( + status: BuildStatus.success, + outputs: [AssetId('a', 'web/index.html')].build(), + buildOutputReader: finalizedReader, + ), ); await handler.emitUpdateMessage( - BuildResult(BuildStatus.success, [ - AssetId('a', 'web/index.html'), - AssetId('a', 'lib/some.dart.js'), - ]), + BuildResult( + status: BuildStatus.success, + outputs: + [ + AssetId('a', 'web/index.html'), + AssetId('a', 'lib/some.dart.js'), + ].build(), + buildOutputReader: finalizedReader, + ), ); await clientChannel1.sink.close(); }); @@ -587,11 +620,16 @@ void main() { await createMockConnection(serverChannel1, 'web1'); await createMockConnection(serverChannel2, 'web2'); await handler.emitUpdateMessage( - BuildResult(BuildStatus.success, [ - AssetId('a', 'web1/index.html'), - AssetId('a', 'web2/index.html'), - AssetId('a', 'lib/some.dart.js'), - ]), + BuildResult( + status: BuildStatus.success, + outputs: + [ + AssetId('a', 'web1/index.html'), + AssetId('a', 'web2/index.html'), + AssetId('a', 'lib/some.dart.js'), + ].build(), + buildOutputReader: finalizedReader, + ), ); await clientChannel1.sink.close(); await clientChannel2.sink.close(); @@ -601,15 +639,13 @@ void main() { } class MockWatchImpl implements Watcher { - @override - final AssetGraph assetGraph; - Future? _currentBuild; @override - Future? get currentBuild => _currentBuild; + Future get currentBuildResult => _currentBuild!; + @override - set currentBuild(Future? _) => + set currentBuildResult(Future? _) => throw UnsupportedError('unsupported!'); final _futureBuildResultsController = StreamController>(); @@ -624,14 +660,11 @@ class MockWatchImpl implements Watcher { @override final PackageGraph packageGraph; - @override - final Future finalizedReader; - void addFutureResult(Future result) { _futureBuildResultsController.add(result); } - MockWatchImpl(this.finalizedReader, this.packageGraph, this.assetGraph) { + MockWatchImpl(this.packageGraph) { final firstBuild = Completer(); _currentBuild = firstBuild.future; _futureBuildResultsController.stream.listen((futureBuildResult) { diff --git a/build_runner/test/commands/watch/watcher_test.dart b/build_runner/test/commands/watch/watcher_test.dart deleted file mode 100644 index cdd1d45495..0000000000 --- a/build_runner/test/commands/watch/watcher_test.dart +++ /dev/null @@ -1,1206 +0,0 @@ -// 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 'dart:convert'; -import 'dart:io'; - -import 'package:async/async.dart'; -import 'package:build/build.dart'; -import 'package:build_config/build_config.dart'; -import 'package:build_runner/src/build/asset_graph/graph.dart'; -import 'package:build_runner/src/build/asset_graph/node.dart'; -import 'package:build_runner/src/build/build_result.dart'; -import 'package:build_runner/src/build_plan/apply_builders.dart'; -import 'package:build_runner/src/build_plan/build_options.dart'; -import 'package:build_runner/src/build_plan/build_phases.dart'; -import 'package:build_runner/src/build_plan/builder_application.dart'; -import 'package:build_runner/src/build_plan/builder_factories.dart'; -import 'package:build_runner/src/build_plan/package_graph.dart'; -import 'package:build_runner/src/build_plan/testing_overrides.dart'; -import 'package:build_runner/src/commands/watch_command.dart'; -import 'package:build_runner/src/constants.dart'; -import 'package:built_collection/built_collection.dart'; -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; -import 'package:test/test.dart'; -import 'package:watcher/watcher.dart'; - -import '../../common/common.dart'; - -void main() { - /// Basic phases/phase groups which get used in many tests - final copyABuildApplication = applyToRoot( - TestBuilder(buildExtensions: appendExtension('.copy', from: '.txt')), - ); - final packageConfigId = makeAssetId('a|.dart_tool/package_config.json'); - final packageGraph = buildPackageGraph({ - rootPackage('a', path: path.absolute('a')): [], - }); - late InternalTestReaderWriter readerWriter; - - setUp(() async { - readerWriter = InternalTestReaderWriter( - rootPackage: packageGraph.root.name, - ); - await readerWriter.writeAsString( - packageConfigId, - jsonEncode(_packageConfig), - ); - }); - - group('watch', () { - setUp(() { - _terminateWatchController = StreamController(); - }); - - tearDown(() { - FakeWatcher.watchers.clear(); - return terminateWatch(); - }); - - group('simple', () { - test('rebuilds once on file updates', () async { - final buildState = await startWatch( - [copyABuildApplication], - {'a|web/a.txt': 'a'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a'}, - readerWriter: readerWriter, - ); - - await readerWriter.writeAsString(makeAssetId('a|web/a.txt'), 'b'); - - result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'b'}, - readerWriter: readerWriter, - ); - - // Wait for the `_debounceDelay` before terminating. - await Future.delayed(_debounceDelay); - - await terminateWatch(); - expect(await results.hasNext, isFalse); - }); - - test('emits a warning when no builders are specified', () async { - final logs = []; - final buildState = await startWatch( - [], - {'a|web/a.txt.copy': 'a'}, - readerWriter, - packageGraph: packageGraph, - onLog: (record) { - if (record.level == Level.WARNING) logs.add(record); - }, - ); - final result = await buildState.buildResults.first; - expect(result.status, BuildStatus.success); - expect( - logs, - contains( - predicate( - (LogRecord record) => - record.message.contains('Nothing to build.'), - ), - ), - ); - }); - - test('rebuilds on file updates outside hardcoded sources', () async { - final buildState = await startWatch( - [copyABuildApplication], - { - 'a|test_files/a.txt': 'a', - 'a|build.yaml': ''' -targets: - a: - sources: - - test_files/** -''', - }, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: {'a|test_files/a.txt.copy': 'a'}, - readerWriter: readerWriter, - ); - - await readerWriter.writeAsString( - makeAssetId('a|test_files/a.txt'), - 'b', - ); - - result = await results.next; - checkBuild( - result, - outputs: {'a|test_files/a.txt.copy': 'b'}, - readerWriter: readerWriter, - ); - }); - - test('rebuilds on new files', () async { - final buildState = await startWatch( - [copyABuildApplication], - {'a|web/a.txt': 'a'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a'}, - readerWriter: readerWriter, - ); - - await readerWriter.writeAsString(makeAssetId('a|web/b.txt'), 'b'); - - result = await results.next; - checkBuild( - result, - outputs: {'a|web/b.txt.copy': 'b'}, - readerWriter: readerWriter, - ); - // Previous outputs should still exist. - expect( - readerWriter.testing.readString(makeAssetId('a|web/a.txt.copy')), - 'a', - ); - }); - - test('rebuilds on new files outside hardcoded sources', () async { - final buildState = await startWatch( - [copyABuildApplication], - { - 'a|test_files/a.txt': 'a', - 'a|build.yaml': ''' -targets: - a: - sources: - - test_files/** -''', - }, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: {'a|test_files/a.txt.copy': 'a'}, - readerWriter: readerWriter, - ); - - await readerWriter.writeAsString( - makeAssetId('a|test_files/b.txt'), - 'b', - ); - - result = await results.next; - checkBuild( - result, - outputs: {'a|test_files/b.txt.copy': 'b'}, - readerWriter: readerWriter, - ); - // Previous outputs should still exist. - expect( - readerWriter.testing.readString( - makeAssetId('a|test_files/a.txt.copy'), - ), - 'a', - ); - }); - - test('rebuilds on deleted files', () async { - final buildState = await startWatch( - [copyABuildApplication], - {'a|web/a.txt': 'a', 'a|web/b.txt': 'b'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a', 'a|web/b.txt.copy': 'b'}, - readerWriter: readerWriter, - ); - - // Don't call writer.delete, that has side effects. - readerWriter.testing.delete(makeAssetId('a|web/a.txt')); - FakeWatcher.notifyWatchers( - WatchEvent(ChangeType.REMOVE, path.absolute('a', 'web', 'a.txt')), - ); - - result = await results.next; - - // Shouldn't rebuild anything, no outputs. - checkBuild(result, outputs: {}, readerWriter: readerWriter); - - // The old output file should no longer exist either. - expect( - readerWriter.testing.exists(makeAssetId('a|web/a.txt.copy')), - isFalse, - ); - // Previous outputs should still exist. - expect( - readerWriter.testing.readString(makeAssetId('a|web/b.txt.copy')), - 'b', - ); - }); - - test('rebuilds on created missing source files', () async { - final application = applyToRoot( - TestBuilder( - buildExtensions: appendExtension('.copy', from: '.txt'), - extraWork: (buildStep, _) async { - await buildStep.canRead(makeAssetId('a|web/b.other')); - }, - ), - ); - - final buildState = await startWatch( - [application], - {'a|web/a.txt': 'a'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a'}, - readerWriter: readerWriter, - ); - - readerWriter.testing.writeString(makeAssetId('a|web/b.other'), 'b'); - FakeWatcher.notifyWatchers( - WatchEvent(ChangeType.ADD, path.absolute('a', 'web', 'b.other')), - ); - - // Should rebuild due to the previously-missing input appearing. - result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a'}, - readerWriter: readerWriter, - ); - }); - - test('rebuilds on deleted files outside hardcoded sources', () async { - final buildState = await startWatch( - [copyABuildApplication], - { - 'a|test_files/a.txt': 'a', - 'a|test_files/b.txt': 'b', - 'a|build.yaml': ''' -targets: - a: - sources: - - test_files/** -''', - }, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: { - 'a|test_files/a.txt.copy': 'a', - 'a|test_files/b.txt.copy': 'b', - }, - readerWriter: readerWriter, - ); - - // Don't call writer.delete, that has side effects. - readerWriter.testing.delete(makeAssetId('a|test_files/a.txt')); - FakeWatcher.notifyWatchers( - WatchEvent( - ChangeType.REMOVE, - path.absolute('a', 'test_files', 'a.txt'), - ), - ); - - result = await results.next; - - // Shouldn't rebuild anything, no outputs. - checkBuild(result, outputs: {}, readerWriter: readerWriter); - - // The old output file should no longer exist either. - expect( - readerWriter.testing.exists(makeAssetId('a|test_files/a.txt.copy')), - isFalse, - ); - // Previous outputs should still exist. - expect( - readerWriter.testing.readString( - makeAssetId('a|test_files/b.txt.copy'), - ), - 'b', - ); - }); - - test('rebuilds properly update asset_graph.json', () async { - final buildState = await startWatch( - [copyABuildApplication], - {'a|web/a.txt': 'a', 'a|web/b.txt': 'b'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a', 'a|web/b.txt.copy': 'b'}, - readerWriter: readerWriter, - ); - - await readerWriter.writeAsString(makeAssetId('a|web/c.txt'), 'c'); - - await readerWriter.writeAsString(makeAssetId('a|web/b.txt'), 'b2'); - - // Don't call writer.delete, that has side effects. - readerWriter.testing.delete(makeAssetId('a|web/a.txt')); - FakeWatcher.notifyWatchers( - WatchEvent(ChangeType.REMOVE, path.absolute('a', 'web', 'a.txt')), - ); - - result = await results.next; - checkBuild( - result, - outputs: {'a|web/b.txt.copy': 'b2', 'a|web/c.txt.copy': 'c'}, - readerWriter: readerWriter, - ); - - final cachedGraph = - AssetGraph.deserialize( - readerWriter.testing.readBytes(makeAssetId('a|$assetGraphPath')), - )!; - - final expectedGraph = await AssetGraph.build( - BuildPhases([]), - {}, - {packageConfigId}, - buildPackageGraph({rootPackage('a'): []}), - readerWriter, - ); - - final aTxtId = makeAssetId('a|web/a.txt'); - final aTxtNode = AssetNode.missingSource(aTxtId); - final aTxtCopyId = makeAssetId('a|web/a.txt.copy'); - final aTxtCopyNode = AssetNode.missingSource(aTxtCopyId); - final bCopyId = makeAssetId('a|web/b.txt.copy'); - final bTxtId = makeAssetId('a|web/b.txt'); - final bCopyNode = AssetNode.generated( - bCopyId, - phaseNumber: 0, - primaryInput: makeAssetId('a|web/b.txt'), - result: true, - digest: computeDigest(bCopyId, 'b2'), - inputs: [makeAssetId('a|web/b.txt')], - isHidden: false, - ); - - expectedGraph - ..add(aTxtNode) - ..add(aTxtCopyNode) - ..add(bCopyNode) - ..add( - AssetNode.source( - AssetId.parse('a|web/b.txt'), - outputs: [bCopyNode.id], - primaryOutputs: [bCopyNode.id], - digest: computeDigest(bTxtId, 'b2'), - ), - ); - - final cCopyId = makeAssetId('a|web/c.txt.copy'); - final cTxtId = makeAssetId('a|web/c.txt'); - final cCopyNode = AssetNode.generated( - cCopyId, - phaseNumber: 0, - primaryInput: cTxtId, - result: true, - digest: computeDigest(cCopyId, 'c'), - inputs: [makeAssetId('a|web/c.txt')], - isHidden: false, - ); - expectedGraph - ..add(cCopyNode) - ..add( - AssetNode.source( - AssetId.parse('a|web/c.txt'), - outputs: [cCopyNode.id], - primaryOutputs: [cCopyNode.id], - digest: computeDigest(cTxtId, 'c'), - ), - ); - - expect(cachedGraph, equalsAssetGraph(expectedGraph)); - expect( - cachedGraph.allPostProcessBuildStepOutputs, - expectedGraph.allPostProcessBuildStepOutputs, - ); - }); - - test('ignores events from nested packages', () async { - final packageGraph = buildPackageGraph({ - rootPackage('a', path: path.absolute('a')): ['b'], - package('b', path: path.absolute('a', 'b')): [], - }); - - final buildState = await startWatch( - [copyABuildApplication], - {'a|web/a.txt': 'a', 'b|web/b.txt': 'b'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - // Should ignore the files under the `b` package, even though they - // match the input set. - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a'}, - readerWriter: readerWriter, - ); - - await readerWriter.writeAsString(makeAssetId('a|web/a.txt'), 'b'); - await readerWriter.writeAsString(makeAssetId('b|web/b.txt'), 'c'); - // Have to manually notify here since the path isn't standard. - FakeWatcher.notifyWatchers( - WatchEvent( - ChangeType.MODIFY, - path.absolute('a', 'b', 'web', 'a.txt'), - ), - ); - - result = await results.next; - // Ignores the modification under the `b` package, even though it - // matches the input set. - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'b'}, - readerWriter: readerWriter, - ); - }); - - test('rebuilds on file updates during first build', () async { - final blocker = Completer(); - final buildAction = applyToRoot( - TestBuilder(extraWork: (_, _) => blocker.future), - ); - final buildState = await startWatch( - [buildAction], - {'a|web/a.txt': 'a'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - FakeWatcher.notifyWatchers( - WatchEvent(ChangeType.MODIFY, path.absolute('a', 'web', 'a.txt')), - ); - blocker.complete(); - - var result = await results.next; - // TODO: Move this up above the call to notifyWatchers once - // https://github.com/dart-lang/build/issues/526 is fixed. - await readerWriter.writeAsString(makeAssetId('a|web/a.txt'), 'b'); - - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a'}, - readerWriter: readerWriter, - ); - - result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'b'}, - readerWriter: readerWriter, - ); - }); - - test('edits to .dart_tool/package_config.json prevent future builds ' - 'and ask you to restart', () async { - final logs = []; - final buildState = await startWatch( - [copyABuildApplication], - {'a|web/a.txt': 'a'}, - readerWriter, - packageGraph: packageGraph, - onLog: (record) { - if (record.level == Level.SEVERE) logs.add(record); - }, - ); - final results = StreamQueue(buildState.buildResults); - - final result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a'}, - readerWriter: readerWriter, - ); - - final newConfig = Map.of(_packageConfig); - newConfig['extra'] = 'stuff'; - await readerWriter.writeAsString( - packageConfigId, - jsonEncode(newConfig), - ); - - expect(await results.hasNext, isFalse); - expect(logs, hasLength(1)); - expect( - logs.first.message, - contains('Terminating builds due to package graph update.'), - ); - }); - - test( - 'Gives the package config a chance to be re-written before failing', - () async { - final logs = []; - final buildState = await startWatch( - [copyABuildApplication], - {'a|web/a.txt': 'a'}, - readerWriter, - packageGraph: packageGraph, - onLog: (record) { - if (record.level == Level.SEVERE) logs.add(record); - }, - ); - buildState.buildResults.handleError( - (Object e, StackTrace s) => print('$e\n$s'), - ); - buildState.buildResults.listen(print); - final results = StreamQueue(buildState.buildResults); - - final result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a'}, - readerWriter: readerWriter, - ); - - await readerWriter.delete(packageConfigId); - - // Wait for it to try reading the file twice to ensure it will retry. - await (_readerForState[buildState] as InternalTestReaderWriter) - .onCanRead - .where((id) => id == packageConfigId) - .take(2) - .drain(); - - final newConfig = Map.of(_packageConfig); - newConfig['extra'] = 'stuff'; - await readerWriter.writeAsString( - packageConfigId, - jsonEncode(newConfig), - ); - - expect(await results.hasNext, isFalse); - expect(logs, hasLength(1)); - expect( - logs.first.message, - contains('Terminating builds due to package graph update.'), - ); - }, - ); - - group('build.yaml', () { - final packageGraph = buildPackageGraph({ - rootPackage('a', path: path.absolute('a')): ['b'], - package('b', path: path.absolute('b'), type: DependencyType.path): [], - }); - late List logs; - late StreamQueue results; - - group('is added', () { - setUp(() async { - logs = []; - final buildState = await startWatch( - [copyABuildApplication], - {}, - readerWriter, - onLog: (record) { - if (record.level == Level.SEVERE) logs.add(record); - }, - packageGraph: packageGraph, - ); - results = StreamQueue(buildState.buildResults); - await results.next; - }); - - test('to the root package', () async { - await readerWriter.writeAsString( - AssetId('a', 'build.yaml'), - '# New build.yaml file', - ); - expect(await results.hasNext, isTrue); - final next = await results.next; - expect(next.status, BuildStatus.failure); - expect(next.failureType, FailureType.buildConfigChanged); - expect(logs, hasLength(1)); - expect( - logs.first.message, - contains('Terminating builds due to a:build.yaml update'), - ); - }); - - test('to a dependency', () async { - await readerWriter.writeAsString( - AssetId('b', 'build.yaml'), - '# New build.yaml file', - ); - - expect(await results.hasNext, isTrue); - final next = await results.next; - expect(next.status, BuildStatus.failure); - expect(next.failureType, FailureType.buildConfigChanged); - expect(logs, hasLength(1)); - expect( - logs.first.message, - contains('Terminating builds due to b:build.yaml update'), - ); - }); - - test('.build.yaml', () async { - await readerWriter.writeAsString( - AssetId('a', 'b.build.yaml'), - '# New b.build.yaml file', - ); - expect(await results.hasNext, isTrue); - final next = await results.next; - expect(next.status, BuildStatus.failure); - expect(next.failureType, FailureType.buildConfigChanged); - expect(logs, hasLength(1)); - expect( - logs.first.message, - contains('Terminating builds due to a:b.build.yaml update'), - ); - }); - }); - - group('is edited', () { - setUp(() async { - logs = []; - final buildState = await startWatch( - [copyABuildApplication], - {'a|build.yaml': '', 'b|build.yaml': ''}, - readerWriter, - onLog: (record) { - if (record.level == Level.SEVERE) logs.add(record); - }, - packageGraph: packageGraph, - ); - results = StreamQueue(buildState.buildResults); - await results.next; - }); - - test('in the root package', () async { - await readerWriter.writeAsString( - AssetId('a', 'build.yaml'), - '# Edited build.yaml file', - ); - - expect(await results.hasNext, isTrue); - final next = await results.next; - expect(next.status, BuildStatus.failure); - expect(next.failureType, FailureType.buildConfigChanged); - expect(logs, hasLength(1)); - expect( - logs.first.message, - contains('Terminating builds due to a:build.yaml update'), - ); - }); - - test('in a dependency', () async { - await readerWriter.writeAsString( - AssetId('b', 'build.yaml'), - '# Edited build.yaml file', - ); - - expect(await results.hasNext, isTrue); - final next = await results.next; - expect(next.status, BuildStatus.failure); - expect(next.failureType, FailureType.buildConfigChanged); - expect(logs, hasLength(1)); - expect( - logs.first.message, - contains('Terminating builds due to b:build.yaml update'), - ); - }); - }); - - group('with --config', () { - setUp(() async { - logs = []; - final buildState = await startWatch( - [copyABuildApplication], - {'a|build.yaml': '', 'a|build.cool.yaml': ''}, - readerWriter, - configKey: 'cool', - onLog: (record) { - if (record.level == Level.SEVERE) logs.add(record); - }, - overrideBuildConfig: { - 'a': BuildConfig.useDefault('a', ['b']), - }, - packageGraph: packageGraph, - ); - results = StreamQueue(buildState.buildResults); - await results.next; - }); - - test('original is edited', () async { - await readerWriter.writeAsString( - AssetId('a', 'build.yaml'), - '# Edited build.yaml file', - ); - - expect(await results.hasNext, isTrue); - final next = await results.next; - expect(next.status, BuildStatus.failure); - expect(next.failureType, FailureType.buildConfigChanged); - expect(logs, hasLength(1)); - expect( - logs.first.message, - contains('Terminating builds due to a:build.yaml update'), - ); - }); - - test('build..yaml in dependencies are ignored', () async { - await readerWriter.writeAsString( - AssetId('b', 'build.cool.yaml'), - '# New build.yaml file', - ); - - await Future.delayed(_debounceDelay); - expect(logs, isEmpty); - - await terminateWatch(); - }); - - test('build..yaml is edited', () async { - await readerWriter.writeAsString( - AssetId('a', 'build.cool.yaml'), - '# Edited build.cool.yaml file', - ); - - expect(await results.hasNext, isTrue); - final next = await results.next; - expect(next.status, BuildStatus.failure); - expect(next.failureType, FailureType.buildConfigChanged); - expect(logs, hasLength(1)); - expect( - logs.first.message, - contains('Terminating builds due to a:build.cool.yaml update'), - ); - }); - }); - }); - }); - - group('file updates to same contents', () { - test('does not rebuild', () async { - var runCount = 0; - final buildState = await startWatch( - [ - applyToRoot( - TestBuilder( - buildExtensions: appendExtension('.copy', from: '.txt'), - build: (buildStep, _) { - runCount++; - buildStep.writeAsString( - buildStep.inputId.addExtension('.copy'), - buildStep.readAsString(buildStep.inputId), - ); - throw StateError('Fail'); - }, - ), - ), - ], - {'a|web/a.txt': 'a'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - final result = await results.next; - expect(runCount, 1); - checkBuild( - result, - status: BuildStatus.failure, - readerWriter: readerWriter, - ); - - await readerWriter.writeAsString(makeAssetId('a|web/a.txt'), 'a'); - - // Wait for the `_debounceDelay * 4` before terminating to - // give it a chance to pick up the change. - await Future.delayed(_debounceDelay * 4); - - await terminateWatch(); - expect(await results.hasNext, isFalse); - }); - }); - - group('multiple phases', () { - test('edits propagate through all phases', () async { - final buildActions = [ - copyABuildApplication, - applyToRoot( - TestBuilder( - buildExtensions: appendExtension('.copy', from: '.copy'), - ), - ), - ]; - - final buildState = await startWatch( - buildActions, - {'a|web/a.txt': 'a'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a', 'a|web/a.txt.copy.copy': 'a'}, - readerWriter: readerWriter, - ); - - await readerWriter.writeAsString(makeAssetId('a|web/a.txt'), 'b'); - - result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'b', 'a|web/a.txt.copy.copy': 'b'}, - readerWriter: readerWriter, - ); - }); - - test('adds propagate through all phases', () async { - final buildActions = [ - copyABuildApplication, - applyToRoot( - TestBuilder( - buildExtensions: appendExtension('.copy', from: '.copy'), - ), - ), - ]; - - final buildState = await startWatch( - buildActions, - {'a|web/a.txt': 'a'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a', 'a|web/a.txt.copy.copy': 'a'}, - readerWriter: readerWriter, - ); - - await readerWriter.writeAsString(makeAssetId('a|web/b.txt'), 'b'); - - result = await results.next; - checkBuild( - result, - outputs: {'a|web/b.txt.copy': 'b', 'a|web/b.txt.copy.copy': 'b'}, - readerWriter: readerWriter, - ); - // Previous outputs should still exist. - expect( - readerWriter.testing.readString(makeAssetId('a|web/a.txt.copy')), - 'a', - ); - expect( - readerWriter.testing.readString(makeAssetId('a|web/a.txt.copy.copy')), - 'a', - ); - }); - - test('deletes propagate through all phases', () async { - final buildActions = [ - copyABuildApplication, - applyToRoot( - TestBuilder( - buildExtensions: appendExtension('.copy', from: '.copy'), - ), - ), - ]; - - final buildState = await startWatch( - buildActions, - {'a|web/a.txt': 'a', 'a|web/b.txt': 'b'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: { - 'a|web/a.txt.copy': 'a', - 'a|web/a.txt.copy.copy': 'a', - 'a|web/b.txt.copy': 'b', - 'a|web/b.txt.copy.copy': 'b', - }, - readerWriter: readerWriter, - ); - - // Don't call writer.delete, that has side effects. - readerWriter.testing.delete(makeAssetId('a|web/a.txt')); - - FakeWatcher.notifyWatchers( - WatchEvent(ChangeType.REMOVE, path.absolute('a', 'web', 'a.txt')), - ); - - result = await results.next; - // Shouldn't rebuild anything, no outputs. - checkBuild(result, outputs: {}, readerWriter: readerWriter); - - // Derived outputs should no longer exist. - expect( - readerWriter.testing.exists(makeAssetId('a|web/a.txt.copy')), - isFalse, - ); - expect( - readerWriter.testing.exists(makeAssetId('a|web/a.txt.copy.copy')), - isFalse, - ); - // Other outputs should still exist. - expect( - readerWriter.testing.readString(makeAssetId('a|web/b.txt.copy')), - 'b', - ); - expect( - readerWriter.testing.readString(makeAssetId('a|web/b.txt.copy.copy')), - 'b', - ); - }); - - test('deleted generated outputs are regenerated', () async { - final buildActions = [ - copyABuildApplication, - applyToRoot( - TestBuilder( - buildExtensions: appendExtension('.copy', from: '.copy'), - ), - ), - ]; - - final buildState = await startWatch( - buildActions, - {'a|web/a.txt': 'a'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a', 'a|web/a.txt.copy.copy': 'a'}, - readerWriter: readerWriter, - ); - - // Don't call writer.delete, that has side effects. - readerWriter.testing.delete(makeAssetId('a|web/a.txt.copy')); - FakeWatcher.notifyWatchers( - WatchEvent( - ChangeType.REMOVE, - path.absolute('a', 'web', 'a.txt.copy'), - ), - ); - - result = await results.next; - // Should rebuild the generated asset, but not its outputs because its - // content didn't change. - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a'}, - readerWriter: readerWriter, - ); - }); - }); - - /// Tests for updates - group('secondary dependency', () { - test('of an output file is edited', () async { - final buildActions = [ - applyToRoot( - TestBuilder( - buildExtensions: appendExtension('.copy', from: '.a'), - build: copyFrom(makeAssetId('a|web/file.b')), - ), - ), - ]; - - final buildState = await startWatch( - buildActions, - {'a|web/file.a': 'a', 'a|web/file.b': 'b'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: {'a|web/file.a.copy': 'b'}, - readerWriter: readerWriter, - ); - - await readerWriter.writeAsString(makeAssetId('a|web/file.b'), 'c'); - - result = await results.next; - checkBuild( - result, - outputs: {'a|web/file.a.copy': 'c'}, - readerWriter: readerWriter, - ); - }); - - test( - 'of an output which is derived from another generated file is edited', - () async { - final buildActions = [ - applyToRoot( - TestBuilder( - buildExtensions: appendExtension('.copy', from: '.a'), - ), - ), - applyToRoot( - TestBuilder( - buildExtensions: appendExtension('.copy', from: '.a.copy'), - build: copyFrom(makeAssetId('a|web/file.b')), - ), - ), - ]; - - final buildState = await startWatch( - buildActions, - {'a|web/file.a': 'a', 'a|web/file.b': 'b'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: {'a|web/file.a.copy': 'a', 'a|web/file.a.copy.copy': 'b'}, - readerWriter: readerWriter, - ); - - await readerWriter.writeAsString(makeAssetId('a|web/file.b'), 'c'); - - result = await results.next; - checkBuild( - result, - outputs: {'a|web/file.a.copy.copy': 'c'}, - readerWriter: readerWriter, - ); - }, - ); - }); - }); -} - -final _debounceDelay = const Duration(milliseconds: 10); -StreamController? _terminateWatchController; - -/// Start watching files and running builds. -Future startWatch( - List builders, - Map inputs, - InternalTestReaderWriter readerWriter, { - required PackageGraph packageGraph, - Map overrideBuildConfig = const {}, - void Function(LogRecord)? onLog, - String? configKey, -}) async { - onLog ??= (_) {}; - inputs.forEach((serializedId, contents) { - readerWriter.writeAsString(makeAssetId(serializedId), contents); - }); - FakeWatcher watcherFactory(String path) => FakeWatcher(path); - - final state = - (await WatchCommand( - builderFactories: BuilderFactories(), - buildOptions: BuildOptions.forTests( - configKey: configKey, - skipBuildScriptCheck: true, - ), - testingOverrides: TestingOverrides( - builderApplications: builders.toBuiltList(), - buildConfig: overrideBuildConfig.build(), - directoryWatcherFactory: watcherFactory, - debounceDelay: _debounceDelay, - onLog: onLog, - packageGraph: packageGraph, - readerWriter: readerWriter, - terminateEventStream: _terminateWatchController!.stream, - ), - ).watch())!; - - // Some tests need access to `reader` so we expose it through an expando. - _readerForState[state] = readerWriter; - return state; -} - -/// Tells the program to stop watching files and terminate. -Future terminateWatch() async { - final terminateWatchController = _terminateWatchController; - if (terminateWatchController == null) return; - - /// Can add any type of event. - terminateWatchController.add(ProcessSignal.sigabrt); - await terminateWatchController.close(); - _terminateWatchController = null; -} - -const _packageConfig = { - 'configVersion': 2, - 'packages': [ - {'name': 'a', 'rootUri': 'file://fake/pkg/path', 'packageUri': 'lib/'}, - ], -}; - -/// Store the private in memory asset reader for a given [BuildState] object -/// here so we can get access to it. -final _readerForState = Expando(); diff --git a/build_runner/test/common/build_runner_tester.dart b/build_runner/test/common/build_runner_tester.dart index 697ea798c1..7ce3bfc5d5 100644 --- a/build_runner/test/common/build_runner_tester.dart +++ b/build_runner/test/common/build_runner_tester.dart @@ -2,6 +2,7 @@ // 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:convert'; import 'dart:io'; import 'dart:isolate'; @@ -13,7 +14,6 @@ import 'package:package_config/package_config.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart' as test; import 'package:test/test.dart'; -import 'package:test_process/test_process.dart'; import 'fixture_packages.dart'; @@ -171,7 +171,7 @@ ${result.stdout}${result.stderr}=== Future start(String directory, String commandLine) async { final args = commandLine.split(' '); final command = args.removeAt(0); - final process = await TestProcess.start( + final process = await Process.start( command, args, workingDirectory: p.join(tempDirectory.path, directory), @@ -183,16 +183,36 @@ ${result.stdout}${result.stderr}=== /// A running `build_runner` process. class BuildRunnerProcess { - final TestProcess process; + final Process process; final StreamQueue _outputs; late final HttpClient _client = HttpClient(); int? _port; BuildRunnerProcess(this.process) : _outputs = StreamQueue( - StreamGroup.merge([process.stdoutStream(), process.stderrStream()]), + StreamGroup.merge([ + process.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()), + process.stderr + .transform(utf8.decoder) + .transform(const LineSplitter()), + ]), ); + /// Expects nothing new on stdout or stderr for [duration]. + Future expectNoOutput(Duration duration) async { + printOnFailure('--- $_testLine expects no output'); + try { + final line = await _outputs.next.timeout(duration); + fail('While expecting no output, got `$line`.'); + } on TimeoutException catch (_) { + // Expected. + } catch (_) { + fail('While expecting no output, process exited.'); + } + } + /// Expects [pattern] to appear in the process's stdout or stderr. /// /// If [failOn] is encountered instead, the test fails immediately. It @@ -202,12 +222,21 @@ class BuildRunnerProcess { /// If the process exits instead, the test fails immediately. /// /// Otherwise, waits until [pattern] appears, returns the matching line. + /// + /// Throws if the process appears to be stuck or done: if it outputs nothing + /// for 30s. Future expect(Pattern pattern, {Pattern? failOn}) async { + printOnFailure( + '--- $_testLine expects `$pattern`' + '${failOn == null ? '' : ', failOn: `$failOn`'}', + ); failOn ??= BuildLog.failurePattern; while (true) { String? line; try { - line = await _outputs.next; + line = await _outputs.next.timeout(const Duration(seconds: 30)); + } on TimeoutException catch (_) { + throw fail('While expecting `$pattern`, timed out after 30s.'); } catch (_) { throw fail('While expecting `$pattern`, process exited.'); } @@ -219,8 +248,23 @@ class BuildRunnerProcess { } } + String get _testLine { + var result = + StackTrace.current + .toString() + .split('\n') + .where((l) => l.contains('_test.dart')) + .first; + result = result.substring(result.lastIndexOf('/') + 1); + result = result.substring(0, result.lastIndexOf(':')); + return result; + } + /// Kills the process. - Future kill() => process.kill(); + Future kill() async { + process.kill(); + await process.exitCode; + } // Expects the server to log that it is serving, records the port. Future expectServing() async { diff --git a/build_runner/test/common/test_phases.dart b/build_runner/test/common/test_phases.dart index 0877d28819..3f5efb0302 100644 --- a/build_runner/test/common/test_phases.dart +++ b/build_runner/test/common/test_phases.dart @@ -154,7 +154,7 @@ Future testPhases( await buildPlan.deleteFilesAndFolders(); BuildResult result; - final build = await BuildSeries.create(buildPlan: buildPlan); + final build = BuildSeries(buildPlan); result = await build.run({}); await build.beforeExit(); diff --git a/build_runner/test/integration_tests/watch_command_invalidation_test.dart b/build_runner/test/integration_tests/watch_command_invalidation_test.dart deleted file mode 100644 index 73181e16c1..0000000000 --- a/build_runner/test/integration_tests/watch_command_invalidation_test.dart +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) 2025, 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. - -@Tags(['integration3']) -library; - -import 'package:build_runner/src/logging/build_log.dart'; -import 'package:test/test.dart'; - -import '../common/common.dart'; - -void main() async { - test('watch command invalidation', () async { - final pubspecs = await Pubspecs.load(); - final tester = BuildRunnerTester(pubspecs); - - tester.writeFixturePackage(FixturePackages.copyBuilder()); - tester.writePackage( - name: 'root_pkg', - dependencies: ['build_runner'], - pathDependencies: ['builder_pkg'], - files: {'web/a.txt': 'a'}, - ); - - // Watch and initial build. - var watch = await tester.start('root_pkg', 'dart run build_runner watch'); - await watch.expect(BuildLog.successPattern); - expect(tester.read('root_pkg/web/a.txt.copy'), 'a'); - - // Builder change. - tester.update('builder_pkg/lib/builder.dart', (script) => '$script\n'); - await watch.expect('Terminating builds due to build script update'); - await watch.expect('Compiling the build script'); - await watch.expect('Creating the asset graph'); - await watch.expect(BuildLog.successPattern); - expect(tester.read('root_pkg/web/a.txt.copy'), 'a'); - - // Builder config change. - tester.write('root_pkg/build.yaml', '# new file, nothing here'); - await watch.expect('Terminating builds due to root_pkg:build.yaml update'); - await watch.expect(BuildLog.successPattern); - expect(tester.read('root_pkg/web/a.txt.copy'), 'a'); - - // Now with --output. - await watch.kill(); - watch = await tester.start( - 'root_pkg', - 'dart run build_runner watch --output web:build', - ); - await watch.expect(BuildLog.successPattern); - expect(tester.read('root_pkg/build/a.txt'), 'a'); - expect(tester.read('root_pkg/build/a.txt.copy'), 'a'); - - // Changed inputs and outputs are written to output directory. - tester.write('root_pkg/lib/a.txt', 'updated'); - await watch.expect(BuildLog.successPattern); - expect(tester.read('root_pkg/build/a.txt'), 'a'); - expect(tester.read('root_pkg/build/a.txt.copy'), 'a'); - }); -} diff --git a/build_runner/test/integration_tests/watch_command_test.dart b/build_runner/test/integration_tests/watch_command_test.dart new file mode 100644 index 0000000000..6a0e47d285 --- /dev/null +++ b/build_runner/test/integration_tests/watch_command_test.dart @@ -0,0 +1,143 @@ +// Copyright (c) 2025, 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. + +@Tags(['integration3']) +library; + +import 'package:build_runner/src/logging/build_log.dart'; +import 'package:test/test.dart'; + +import '../common/common.dart'; + +void main() async { + test('watch command invalidation', () async { + final pubspecs = await Pubspecs.load(); + final tester = BuildRunnerTester(pubspecs); + + tester.writeFixturePackage(FixturePackages.copyBuilder()); + tester.writePackage( + name: 'root_pkg', + dependencies: ['build_runner'], + pathDependencies: ['builder_pkg', 'other_pkg'], + files: {'web/a.txt': 'a'}, + ); + tester.writePackage( + name: 'other_pkg', + dependencies: ['build_runner'], + pathDependencies: ['builder_pkg'], + files: {}, + ); + + // Watch and initial build. + var watch = await tester.start('root_pkg', 'dart run build_runner watch'); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/web/a.txt.copy'), 'a'); + + // File change. + tester.write('root_pkg/web/a.txt', 'updated'); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/web/a.txt.copy'), 'updated'); + + // File rewrite without change. + tester.write('root_pkg/web/a.txt', 'updated'); + await watch.expectNoOutput(const Duration(seconds: 1)); + + // State on disk is updated so `build` knows to do nothing. + var output = await tester.run('root_pkg', 'dart run build_runner build'); + expect(output, contains('wrote 0 outputs')); + + // New file. + tester.write('root_pkg/web/b.txt', 'b'); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/web/b.txt.copy'), 'b'); + + // State on disk is updated so `build` knows to do nothing. + output = await tester.run('root_pkg', 'dart run build_runner build'); + expect(output, contains('wrote 0 outputs')); + + // Deleted file. + tester.delete('root_pkg/web/b.txt'); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/web/b.txt.copy'), null); + + // Deleted output. + tester.delete('root_pkg/web/a.txt.copy'); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/web/a.txt.copy'), 'updated'); + + // Builder change. + tester.update('builder_pkg/lib/builder.dart', (script) => '$script\n'); + await watch.expect('Terminating builds due to build script update'); + await watch.expect('Compiling the build script'); + await watch.expect('Creating the asset graph'); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/web/a.txt.copy'), 'updated'); + + // State on disk is updated so `build` knows to do nothing. + output = await tester.run('root_pkg', 'dart run build_runner build'); + expect(output, contains('wrote 0 outputs')); + + // Builder config change, add a file. + tester.write('root_pkg/build.yaml', '# new file, nothing here'); + await watch.expect('Terminating builds due to root_pkg:build.yaml update'); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/web/a.txt.copy'), 'updated'); + + // Builder config change, update a file. + tester.update('root_pkg/build.yaml', (yaml) => '$yaml\n'); + await watch.expect('Terminating builds due to root_pkg:build.yaml update'); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/web/a.txt.copy'), 'updated'); + + // Builder config change in dependency. + tester.write('other_pkg/build.yaml', '# new file, nothing here'); + await watch.expect('Terminating builds due to other_pkg:build.yaml update'); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/web/a.txt.copy'), 'updated'); + + // Builder config change in root overriding dependency. + tester.write('root_pkg/other_pkg.build.yaml', '# new file, nothing here'); + await watch.expect( + 'Terminating builds due to root_pkg:other_pkg.build.yaml update', + ); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/web/a.txt.copy'), 'updated'); + + // State on disk is updated so `build` knows to do nothing. + output = await tester.run('root_pkg', 'dart run build_runner build'); + expect(output, contains('wrote 0 outputs')); + + // File change during build. + tester.write('root_pkg/web/a.txt', 'a'); + await watch.expect('Building'); + tester.write('root_pkg/web/a.txt', 'updated'); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/web/a.txt.copy'), 'a'); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/web/a.txt.copy'), 'updated'); + + // Change to `package_config.json` causes the watcher to exit. + tester.update( + 'root_pkg/.dart_tool/package_config.json', + (script) => '$script\n', + ); + await watch.expect('Terminating builds due to package graph update.'); + await watch.kill(); + + // Now with --output. + watch = await tester.start( + 'root_pkg', + 'dart run build_runner watch --output web:build', + ); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/build/a.txt'), 'updated'); + expect(tester.read('root_pkg/build/a.txt.copy'), 'updated'); + + // Changed inputs and outputs are written to output directory. + tester.write('root_pkg/web/a.txt', 'a'); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/build/a.txt'), 'a'); + expect(tester.read('root_pkg/build/a.txt.copy'), 'a'); + }); +} diff --git a/build_runner/test/io/finalized_reader_test.dart b/build_runner/test/io/build_output_reader_test.dart similarity index 51% rename from build_runner/test/io/finalized_reader_test.dart rename to build_runner/test/io/build_output_reader_test.dart index 108d9db09b..4777ac2a97 100644 --- a/build_runner/test/io/finalized_reader_test.dart +++ b/build_runner/test/io/build_output_reader_test.dart @@ -9,12 +9,17 @@ import 'package:build/build.dart'; import 'package:build_runner/src/build/asset_graph/graph.dart'; import 'package:build_runner/src/build/asset_graph/node.dart'; import 'package:build_runner/src/build/asset_graph/post_process_build_step_id.dart'; +import 'package:build_runner/src/build_plan/build_directory.dart'; import 'package:build_runner/src/build_plan/build_filter.dart'; +import 'package:build_runner/src/build_plan/build_options.dart'; import 'package:build_runner/src/build_plan/build_phases.dart'; +import 'package:build_runner/src/build_plan/build_plan.dart'; +import 'package:build_runner/src/build_plan/builder_factories.dart'; +import 'package:build_runner/src/build_plan/package_graph.dart'; import 'package:build_runner/src/build_plan/phase.dart'; import 'package:build_runner/src/build_plan/target_graph.dart'; import 'package:build_runner/src/build_plan/testing_overrides.dart'; -import 'package:build_runner/src/io/finalized_reader.dart'; +import 'package:build_runner/src/io/build_output_reader.dart'; import 'package:built_collection/built_collection.dart'; import 'package:crypto/crypto.dart'; import 'package:glob/glob.dart'; @@ -24,26 +29,23 @@ import '../common/common.dart'; void main() { group('FinalizedReader', () { - FinalizedReader reader; - late AssetGraph graph; - late TargetGraph targetGraph; + BuildOutputReader reader; + late InternalTestReaderWriter readerWriter; + late AssetGraph assetGraph; + late PackageGraph packageGraph; + late BuildPhases buildPhases; setUp(() async { - final packageGraph = buildPackageGraph({rootPackage('a'): []}); - targetGraph = await TargetGraph.forPackageGraph( - packageGraph: packageGraph, - testingOverrides: TestingOverrides( - defaultRootPackageSources: defaultNonRootVisibleAssets, - ), - ); - - graph = await AssetGraph.build( + readerWriter = InternalTestReaderWriter(rootPackage: 'a'); + packageGraph = buildPackageGraph({rootPackage('a'): []}); + assetGraph = await AssetGraph.build( BuildPhases([]), {}, {}, packageGraph, - InternalTestReaderWriter(), + readerWriter, ); + buildPhases = BuildPhases([]); }); test('can not read deleted files', () async { @@ -64,20 +66,26 @@ void main() { ), ); - graph + assetGraph ..add(notDeleted) ..add(deleted); - final delegate = InternalTestReaderWriter(); - delegate.testing.writeString(notDeleted.id, ''); - delegate.testing.writeString(deleted.id, ''); + readerWriter.testing.writeString(notDeleted.id, ''); + readerWriter.testing.writeString(deleted.id, ''); - reader = FinalizedReader( - delegate, - graph, - targetGraph, - BuildPhases([]), - 'a', + final buildPlan = await BuildPlan.load( + builderFactories: BuilderFactories(), + buildOptions: BuildOptions.forTests(), + testingOverrides: TestingOverrides( + buildPhases: buildPhases, + readerWriter: readerWriter, + packageGraph: packageGraph, + ), + ); + reader = BuildOutputReader( + buildPlan: buildPlan, + readerWriter: readerWriter, + assetGraph: assetGraph, ); expect(await reader.canRead(notDeleted.id), true); expect(await reader.canRead(deleted.id), false); @@ -93,26 +101,55 @@ void main() { primaryInput: AssetId('a', 'web/a.dart'), isHidden: true, ); - graph.add(node); - final delegate = InternalTestReaderWriter(); - delegate.testing.writeString(id, ''); - reader = FinalizedReader( - delegate, - graph, - targetGraph, - BuildPhases([InBuildPhase(TestBuilder(), 'a', isOptional: false)]), - 'a', - )..reset({'web'}.build(), BuiltSet()); + assetGraph.add(node); + readerWriter.testing.writeString(id, ''); + + buildPhases = BuildPhases([ + InBuildPhase(TestBuilder(), 'a', isOptional: false), + ]); + + var buildPlan = await BuildPlan.load( + builderFactories: BuilderFactories(), + buildOptions: BuildOptions.forTests( + buildDirs: {BuildDirectory('web')}.build(), + ), + testingOverrides: TestingOverrides( + buildPhases: buildPhases, + defaultRootPackageSources: defaultNonRootVisibleAssets, + readerWriter: readerWriter, + packageGraph: packageGraph, + ), + ); + reader = BuildOutputReader( + buildPlan: buildPlan, + readerWriter: readerWriter, + assetGraph: assetGraph, + ); expect( await reader.unreadableReason(id), UnreadableReason.failed, reason: 'Should report a failure if no build filters apply', ); - reader.reset( - {'web'}.build(), - {BuildFilter(Glob('b'), Glob('foo'))}.build(), + buildPlan = await BuildPlan.load( + builderFactories: BuilderFactories(), + buildOptions: BuildOptions.forTests( + buildDirs: {BuildDirectory('web')}.build(), + buildFilters: {BuildFilter(Glob('b'), Glob('foo'))}.build(), + ), + testingOverrides: TestingOverrides( + buildPhases: buildPhases, + defaultRootPackageSources: defaultNonRootVisibleAssets, + readerWriter: readerWriter, + packageGraph: packageGraph, + ), ); + reader = BuildOutputReader( + buildPlan: buildPlan, + readerWriter: readerWriter, + assetGraph: assetGraph, + ); + expect( await reader.unreadableReason(id), UnreadableReason.notOutput, diff --git a/build_runner/test/io/create_merged_dir_test.dart b/build_runner/test/io/create_merged_dir_test.dart index 50e65c1ed9..2b936b27dd 100644 --- a/build_runner/test/io/create_merged_dir_test.dart +++ b/build_runner/test/io/create_merged_dir_test.dart @@ -8,13 +8,15 @@ import 'dart:io'; import 'package:build/build.dart'; import 'package:build_runner/src/build/asset_graph/graph.dart'; import 'package:build_runner/src/build/asset_graph/post_process_build_step_id.dart'; -import 'package:build_runner/src/build/finalized_assets_view.dart'; -import 'package:build_runner/src/build/optional_output_tracker.dart'; import 'package:build_runner/src/build_plan/build_directory.dart'; +import 'package:build_runner/src/build_plan/build_options.dart'; import 'package:build_runner/src/build_plan/build_phases.dart'; +import 'package:build_runner/src/build_plan/build_plan.dart'; +import 'package:build_runner/src/build_plan/builder_factories.dart'; import 'package:build_runner/src/build_plan/phase.dart'; import 'package:build_runner/src/build_plan/target_graph.dart'; import 'package:build_runner/src/build_plan/testing_overrides.dart'; +import 'package:build_runner/src/io/build_output_reader.dart'; import 'package:build_runner/src/io/create_merged_dir.dart'; import 'package:built_collection/built_collection.dart'; import 'package:crypto/crypto.dart'; @@ -25,6 +27,7 @@ import '../common/common.dart'; void main() { group('createMergedDir', () { + late BuildPlan buildPlan; late AssetGraph graph; final phases = BuildPhases([ InBuildPhase( @@ -34,6 +37,7 @@ void main() { InBuildPhase( TestBuilder(buildExtensions: appendExtension('.copy', from: '.txt')), 'b', + hideOutput: true, ), ]); final sources = { @@ -65,43 +69,34 @@ void main() { rootPackage('a'): ['b'], package('b'): [], }); - late TargetGraph targetGraph; late Directory tmpDir; late Directory anotherTmpDir; late InternalTestReaderWriter readerWriter; - late OptionalOutputTracker optionalOutputTracker; - late FinalizedAssetsView finalizedAssetsView; + late BuildOutputReader buildOutputReader; setUp(() async { - readerWriter = InternalTestReaderWriter(); + readerWriter = InternalTestReaderWriter(rootPackage: 'a'); for (final source in sources.entries) { readerWriter.testing.writeString(source.key, source.value); } - graph = await AssetGraph.build( - phases, - sources.keys.toSet(), - {}, - packageGraph, - readerWriter, - ); - targetGraph = await TargetGraph.forPackageGraph( - packageGraph: packageGraph, + buildPlan = await BuildPlan.load( + builderFactories: BuilderFactories(), + buildOptions: BuildOptions.forTests(), testingOverrides: TestingOverrides( - defaultRootPackageSources: defaultNonRootVisibleAssets, + buildPhases: phases, + defaultRootPackageSources: + [...defaultRootPackageSources, 'foo/**'].build(), + readerWriter: readerWriter, + packageGraph: packageGraph, ), ); - optionalOutputTracker = OptionalOutputTracker( - graph, - targetGraph, - BuiltSet(), - BuiltSet(), - phases, - ); - finalizedAssetsView = FinalizedAssetsView( - graph, - packageGraph, - optionalOutputTracker, + graph = buildPlan.takeAssetGraph(); + buildOutputReader = BuildOutputReader( + buildPlan: buildPlan, + readerWriter: readerWriter, + assetGraph: graph, ); + for (final id in graph.outputs) { graph.updateNode(id, (nodeBuilder) { nodeBuilder.digest = Digest([]); @@ -122,13 +117,14 @@ void main() { test('creates a valid merged output directory', () async { final success = await createMergedOutputDirectories( - { - BuildDirectory('', outputLocation: OutputLocation(tmpDir.path)), - }.build(), - packageGraph, - readerWriter, - finalizedAssetsView, - false, + buildDirs: + { + BuildDirectory('', outputLocation: OutputLocation(tmpDir.path)), + }.build(), + packageGraph: packageGraph, + readerWriter: readerWriter, + buildOutputReader: buildOutputReader, + outputSymlinksOnly: false, ); expect(success, isTrue); @@ -144,13 +140,14 @@ void main() { }); final success = await createMergedOutputDirectories( - { - BuildDirectory('', outputLocation: OutputLocation(tmpDir.path)), - }.build(), - packageGraph, - readerWriter, - finalizedAssetsView, - false, + buildDirs: + { + BuildDirectory('', outputLocation: OutputLocation(tmpDir.path)), + }.build(), + packageGraph: packageGraph, + readerWriter: readerWriter, + buildOutputReader: buildOutputReader, + outputSymlinksOnly: false, ); expect(success, isTrue); @@ -160,24 +157,25 @@ void main() { test('does not include non-lib files from non-root packages', () { expect( - finalizedAssetsView.allAssets(), + buildOutputReader.allAssets(), isNot(contains(makeAssetId('b|test/outside.txt'))), ); }); test('can create multiple merged directories', () async { final success = await createMergedOutputDirectories( - { - BuildDirectory('', outputLocation: OutputLocation(tmpDir.path)), - BuildDirectory( - '', - outputLocation: OutputLocation(anotherTmpDir.path), - ), - }.build(), - packageGraph, - readerWriter, - finalizedAssetsView, - false, + buildDirs: + { + BuildDirectory('', outputLocation: OutputLocation(tmpDir.path)), + BuildDirectory( + '', + outputLocation: OutputLocation(anotherTmpDir.path), + ), + }.build(), + packageGraph: packageGraph, + readerWriter: readerWriter, + buildOutputReader: buildOutputReader, + outputSymlinksOnly: false, ); expect(success, isTrue); @@ -187,14 +185,21 @@ void main() { test('errors if there are conflicting directories', () async { final success = await createMergedOutputDirectories( - { - BuildDirectory('web', outputLocation: OutputLocation(tmpDir.path)), - BuildDirectory('foo', outputLocation: OutputLocation(tmpDir.path)), - }.build(), - packageGraph, - readerWriter, - finalizedAssetsView, - false, + buildDirs: + { + BuildDirectory( + 'web', + outputLocation: OutputLocation(tmpDir.path), + ), + BuildDirectory( + 'foo', + outputLocation: OutputLocation(tmpDir.path), + ), + }.build(), + packageGraph: packageGraph, + readerWriter: readerWriter, + buildOutputReader: buildOutputReader, + outputSymlinksOnly: false, ); expect(success, isFalse); expect(Directory(tmpDir.path).listSync(), isEmpty); @@ -202,24 +207,24 @@ void main() { test('succeeds if no output directory requested ', () async { final success = await createMergedOutputDirectories( - {BuildDirectory('web'), BuildDirectory('foo')}.build(), - packageGraph, - readerWriter, - finalizedAssetsView, - false, + buildDirs: {BuildDirectory('web'), BuildDirectory('foo')}.build(), + packageGraph: packageGraph, + readerWriter: readerWriter, + buildOutputReader: buildOutputReader, + outputSymlinksOnly: false, ); expect(success, isTrue); }); test('removes the provided root from the output path', () async { final success = await createMergedOutputDirectories( - { - BuildDirectory('web', outputLocation: OutputLocation(tmpDir.path)), - }.build(), - packageGraph, - readerWriter, - finalizedAssetsView, - false, + buildDirs: + {BuildDirectory('web', outputLocation: OutputLocation(tmpDir.path))} + .build(), + packageGraph: packageGraph, + readerWriter: readerWriter, + buildOutputReader: buildOutputReader, + outputSymlinksOnly: false, ); expect(success, isTrue); @@ -230,16 +235,17 @@ void main() { test('skips output directories with no assets', () async { final success = await createMergedOutputDirectories( - { - BuildDirectory( - 'no_assets_here', - outputLocation: OutputLocation(tmpDir.path), - ), - }.build(), - packageGraph, - readerWriter, - finalizedAssetsView, - false, + buildDirs: + { + BuildDirectory( + 'no_assets_here', + outputLocation: OutputLocation(tmpDir.path), + ), + }.build(), + packageGraph: packageGraph, + readerWriter: readerWriter, + buildOutputReader: buildOutputReader, + outputSymlinksOnly: false, ); expect(success, isFalse); expect(Directory(tmpDir.path).listSync(), isEmpty); @@ -247,13 +253,13 @@ void main() { test('does not output the input directory', () async { final success = await createMergedOutputDirectories( - { - BuildDirectory('web', outputLocation: OutputLocation(tmpDir.path)), - }.build(), - packageGraph, - readerWriter, - finalizedAssetsView, - false, + buildDirs: + {BuildDirectory('web', outputLocation: OutputLocation(tmpDir.path))} + .build(), + packageGraph: packageGraph, + readerWriter: readerWriter, + buildOutputReader: buildOutputReader, + outputSymlinksOnly: false, ); expect(success, isTrue); @@ -262,17 +268,21 @@ void main() { test('outputs the packages when input root is provided', () async { final success = await createMergedOutputDirectories( - { - BuildDirectory('web', outputLocation: OutputLocation(tmpDir.path)), - BuildDirectory( - 'foo', - outputLocation: OutputLocation(anotherTmpDir.path), - ), - }.build(), - packageGraph, - readerWriter, - finalizedAssetsView, - false, + buildDirs: + { + BuildDirectory( + 'web', + outputLocation: OutputLocation(tmpDir.path), + ), + BuildDirectory( + 'foo', + outputLocation: OutputLocation(anotherTmpDir.path), + ), + }.build(), + packageGraph: packageGraph, + readerWriter: readerWriter, + buildOutputReader: buildOutputReader, + outputSymlinksOnly: false, ); expect(success, isTrue); @@ -293,13 +303,14 @@ void main() { test('does not nest packages symlinks with no root', () async { final success = await createMergedOutputDirectories( - { - BuildDirectory('', outputLocation: OutputLocation(tmpDir.path)), - }.build(), - packageGraph, - readerWriter, - finalizedAssetsView, - false, + buildDirs: + { + BuildDirectory('', outputLocation: OutputLocation(tmpDir.path)), + }.build(), + packageGraph: packageGraph, + readerWriter: readerWriter, + buildOutputReader: buildOutputReader, + outputSymlinksOnly: false, ); expect(success, isTrue); _expectNoFiles({'packages/packages/a/a.txt'}, tmpDir); @@ -307,17 +318,21 @@ void main() { test('only outputs files contained in the provided root', () async { final success = await createMergedOutputDirectories( - { - BuildDirectory('web', outputLocation: OutputLocation(tmpDir.path)), - BuildDirectory( - 'foo', - outputLocation: OutputLocation(anotherTmpDir.path), - ), - }.build(), - packageGraph, - readerWriter, - finalizedAssetsView, - false, + buildDirs: + { + BuildDirectory( + 'web', + outputLocation: OutputLocation(tmpDir.path), + ), + BuildDirectory( + 'foo', + outputLocation: OutputLocation(anotherTmpDir.path), + ), + }.build(), + packageGraph: packageGraph, + readerWriter: readerWriter, + buildOutputReader: buildOutputReader, + outputSymlinksOnly: false, ); expect(success, isTrue); @@ -343,13 +358,14 @@ void main() { }); final success = await createMergedOutputDirectories( - { - BuildDirectory('', outputLocation: OutputLocation(tmpDir.path)), - }.build(), - packageGraph, - readerWriter, - finalizedAssetsView, - false, + buildDirs: + { + BuildDirectory('', outputLocation: OutputLocation(tmpDir.path)), + }.build(), + packageGraph: packageGraph, + readerWriter: readerWriter, + buildOutputReader: buildOutputReader, + outputSymlinksOnly: false, ); expect(success, isTrue); @@ -358,26 +374,22 @@ void main() { }); test('doesnt always write files not matching outputDirs', () async { - optionalOutputTracker = OptionalOutputTracker( - graph, - targetGraph, - {'foo'}.build(), - BuiltSet(), - phases, - ); - finalizedAssetsView = FinalizedAssetsView( - graph, - packageGraph, - optionalOutputTracker, + buildOutputReader = BuildOutputReader( + buildPlan: buildPlan.copyWith( + buildDirs: {BuildDirectory('foo')}.build(), + ), + readerWriter: readerWriter, + assetGraph: graph, ); final success = await createMergedOutputDirectories( - { - BuildDirectory('', outputLocation: OutputLocation(tmpDir.path)), - }.build(), - packageGraph, - readerWriter, - finalizedAssetsView, - false, + buildDirs: + { + BuildDirectory('', outputLocation: OutputLocation(tmpDir.path)), + }.build(), + packageGraph: packageGraph, + readerWriter: readerWriter, + buildOutputReader: buildOutputReader, + outputSymlinksOnly: false, ); expect(success, isTrue); @@ -405,13 +417,14 @@ void main() { test('fails the build', () async { final success = await createMergedOutputDirectories( - { - BuildDirectory('', outputLocation: OutputLocation(tmpDir.path)), - }.build(), - packageGraph, - readerWriter, - finalizedAssetsView, - false, + buildDirs: + { + BuildDirectory('', outputLocation: OutputLocation(tmpDir.path)), + }.build(), + packageGraph: packageGraph, + readerWriter: readerWriter, + buildOutputReader: buildOutputReader, + outputSymlinksOnly: false, ); expect(success, isFalse); expect( @@ -431,13 +444,14 @@ void main() { group('Empty directory cleanup', () { test('removes directories that become empty', () async { var success = await createMergedOutputDirectories( - { - BuildDirectory('', outputLocation: OutputLocation(tmpDir.path)), - }.build(), - packageGraph, - readerWriter, - finalizedAssetsView, - false, + buildDirs: + { + BuildDirectory('', outputLocation: OutputLocation(tmpDir.path)), + }.build(), + packageGraph: packageGraph, + readerWriter: readerWriter, + buildOutputReader: buildOutputReader, + outputSymlinksOnly: false, ); expect(success, isTrue); final removes = ['a|lib/a.txt', 'a|lib/a.txt.copy']; @@ -452,13 +466,14 @@ void main() { }); } success = await createMergedOutputDirectories( - { - BuildDirectory('', outputLocation: OutputLocation(tmpDir.path)), - }.build(), - packageGraph, - readerWriter, - finalizedAssetsView, - false, + buildDirs: + { + BuildDirectory('', outputLocation: OutputLocation(tmpDir.path)), + }.build(), + packageGraph: packageGraph, + readerWriter: readerWriter, + buildOutputReader: buildOutputReader, + outputSymlinksOnly: false, ); expect(success, isTrue); final packageADir = p.join(tmpDir.path, 'packages', 'a'); diff --git a/build_test/lib/src/test_builder.dart b/build_test/lib/src/test_builder.dart index ede1dfcbcc..001cd3efa3 100644 --- a/build_test/lib/src/test_builder.dart +++ b/build_test/lib/src/test_builder.dart @@ -467,7 +467,7 @@ Future testBuilderFactories( ); await buildPlan.deleteFilesAndFolders(); - final buildSeries = await BuildSeries.create(buildPlan: buildPlan); + final buildSeries = BuildSeries(buildPlan); // Run the build. final buildResult = await buildSeries.run({});