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..696e311149 100644 --- a/build_runner/lib/src/build/build_series.dart +++ b/build_runner/lib/src/build/build_series.dart @@ -13,7 +13,6 @@ import '../build_plan/build_directory.dart'; import '../build_plan/build_filter.dart'; import '../build_plan/build_plan.dart'; import '../io/filesystem_cache.dart'; -import '../io/finalized_reader.dart'; import '../io/reader_writer.dart'; import 'asset_graph/graph.dart'; import 'build.dart'; @@ -37,7 +36,6 @@ class BuildSeries { final AssetGraph assetGraph; final BuildScriptUpdates? buildScriptUpdates; - final FinalizedReader finalizedReader; final ReaderWriter readerWriter; final ResourceManager resourceManager = ResourceManager(); @@ -48,6 +46,9 @@ class BuildSeries { /// if the serialized build state was discarded. BuiltMap? updatesFromLoad; + final StreamController _buildResultsController = + StreamController.broadcast(); + /// Whether the next build is the first build. bool firstBuild = true; @@ -57,7 +58,6 @@ class BuildSeries { this.buildPlan, this.assetGraph, this.buildScriptUpdates, - this.finalizedReader, this.updatesFromLoad, ) : readerWriter = buildPlan.readerWriter.copyWith( generatedAssetHider: assetGraph, @@ -67,6 +67,19 @@ class BuildSeries { : InMemoryFilesystemCache(), ); + /// Broadcast stream of build results. + Stream get buildResults => _buildResultsController.stream; + Future? _currentBuildResult; + + /// 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. /// /// For the first build, pass any changes since the `BuildSeries` was created @@ -93,7 +106,6 @@ class BuildSeries { } } - finalizedReader.reset(BuildDirectory.buildPaths(buildDirs), buildFilters); final build = Build( buildPlan: buildPlan.copyWith( buildDirs: buildDirs, @@ -104,24 +116,19 @@ class BuildSeries { 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; 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_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/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/daemon_builder.dart b/build_runner/lib/src/commands/daemon/daemon_builder.dart index 8fc0ce423c..d39fcb9b73 100644 --- a/build_runner/lib/src/commands/daemon/daemon_builder.dart +++ b/build_runner/lib/src/commands/daemon/daemon_builder.dart @@ -21,7 +21,6 @@ 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'; @@ -36,7 +35,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 +46,7 @@ class BuildRunnerDaemonBuilder implements DaemonBuilder { BuildRunnerDaemonBuilder._( this._buildPlan, - this._buildSeries, + this.buildSeries, this._outputStreamController, this.changeProvider, ) : logs = _outputStreamController.stream.asBroadcastStream(); @@ -59,8 +58,6 @@ class BuildRunnerDaemonBuilder implements DaemonBuilder { @override Stream get builds => _buildResults.stream; - FinalizedReader get reader => _buildSeries.finalizedReader; - final _buildScriptUpdateCompleter = Completer(); Future get buildScriptUpdated => _buildScriptUpdateCompleter.future; @@ -80,7 +77,7 @@ class BuildRunnerDaemonBuilder implements DaemonBuilder { .toList(); if (!_buildPlan.buildOptions.skipBuildScriptCheck && - _buildSeries.buildScriptUpdates!.hasBeenUpdated( + buildSeries.buildScriptUpdates!.hasBeenUpdated( changes.map((change) => change.id).toSet(), )) { if (!_buildScriptUpdateCompleter.isCompleted) { @@ -122,7 +119,7 @@ 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(), @@ -175,7 +172,7 @@ class BuildRunnerDaemonBuilder implements DaemonBuilder { @override Future stop() async { - await _buildSeries.beforeExit(); + await buildSeries.beforeExit(); } void _logMessage(Level level, String message) => _outputStreamController.add( diff --git a/build_runner/lib/src/commands/serve/server.dart b/build_runner/lib/src/commands/serve/server.dart index f7c761f79e..8d4cd7c8a0 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,10 +63,10 @@ class ServeHandler implements BuildState { 'Only top level directories such as `web` or `test` can be served, got', ); } - _state.currentBuild?.then((_) { + _watcher.currentBuildResult.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; + if (_watcher.assetGraph == null) return; _warnForEmptyDirectory(rootDir); }); var cascade = shelf.Cascade(); @@ -95,7 +79,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 +96,7 @@ class ServeHandler implements BuildState { } Future _blockOnCurrentBuild(void _) async { - await currentBuild; + await currentBuildResult; return shelf.Response.notFound(''); } @@ -117,10 +104,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)); @@ -150,8 +138,8 @@ class ServeHandler implements BuildState { } void _warnForEmptyDirectory(String rootDir) { - if (!_state.assetGraph! - .packageNodes(_rootPackage) + if (!_watcher.assetGraph! + .packageNodes(_watcher.packageGraph.root.name) .any((n) => n.id.path.startsWith('$rootDir/'))) { buildLog.warning( 'Requested a server for `$rootDir` but this directory ' @@ -172,12 +160,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 +177,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 +263,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 +290,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 +305,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 +330,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 +352,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 +370,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/watcher.dart b/build_runner/lib/src/commands/watch/watcher.dart index 2159e6d729..d9b2c5fc1f 100644 --- a/build_runner/lib/src/commands/watch/watcher.dart +++ b/build_runner/lib/src/commands/watch/watcher.dart @@ -15,8 +15,7 @@ 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'; @@ -25,7 +24,7 @@ import 'collect_changes.dart'; import 'graph_watcher.dart'; import 'node_watcher.dart'; -class Watcher implements BuildState { +class Watcher { late final BuildPlan buildPlan; BuildSeries? _buildSeries; @@ -40,17 +39,11 @@ class Watcher implements BuildState { /// 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, @@ -69,7 +62,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,7 +71,7 @@ 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 { @@ -95,9 +87,13 @@ class Watcher implements BuildState { _terminateCompleter.complete(); buildLog.error('Terminating builds due to build script update.'); return BuildResult( - BuildStatus.failure, - [], + status: BuildStatus.failure, failureType: FailureType.buildScriptChanged, + buildOutputReader: BuildOutputReader( + buildPlan: buildPlan, + readerWriter: readerWriter, + assetGraph: assetGraph!, + ), ); } } @@ -152,9 +148,9 @@ class Watcher implements BuildState { _isPackageBuildYamlOverride(id)) { controller.add( BuildResult( - BuildStatus.failure, - [], + status: BuildStatus.failure, failureType: FailureType.buildConfigChanged, + buildOutputReader: BuildOutputReader.empty(), ), ); @@ -167,7 +163,6 @@ class Watcher implements BuildState { return change; }) .asyncWhere((change) { - assert(_readerCompleter.isCompleted); return shouldProcess( change, assetGraph!, @@ -181,17 +176,13 @@ class Watcher implements BuildState { 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 (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 +206,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 = await BuildSeries.create(buildPlan: 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..162a335e88 100644 --- a/build_runner/lib/src/commands/watch_command.dart +++ b/build_runner/lib/src/commands/watch_command.dart @@ -74,7 +74,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/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..a74cf393c8 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, assetGraph); + 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(); @@ -607,9 +645,10 @@ class MockWatchImpl implements Watcher { 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 +663,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, this.assetGraph) { final firstBuild = Completer(); _currentBuild = firstBuild.future; _futureBuildResultsController.stream.listen((futureBuildResult) { 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');