Skip to content

Commit 573b427

Browse files
authored
Encapsulate graph and build script updates in BuildSeries. (#4223)
1 parent ab5c986 commit 573b427

File tree

12 files changed

+160
-200
lines changed

12 files changed

+160
-200
lines changed

build_runner/lib/src/build/build_series.dart

Lines changed: 128 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,17 @@ import 'package:build/build.dart';
88
import 'package:built_collection/built_collection.dart';
99
import 'package:watcher/watcher.dart';
1010

11-
import '../bootstrap/build_script_updates.dart';
1211
import '../build_plan/build_directory.dart';
1312
import '../build_plan/build_filter.dart';
1413
import '../build_plan/build_plan.dart';
14+
import '../commands/watch/asset_change.dart';
15+
import '../constants.dart';
16+
import '../io/asset_tracker.dart';
17+
import '../io/build_output_reader.dart';
1518
import '../io/filesystem_cache.dart';
1619
import '../io/reader_writer.dart';
1720
import 'asset_graph/graph.dart';
21+
import 'asset_graph/node.dart';
1822
import 'build.dart';
1923
import 'build_result.dart';
2024

@@ -31,46 +35,126 @@ import 'build_result.dart';
3135
/// this serialized state is not actually used: the `AssetGraph` instance
3236
/// already in memory is used directly.
3337
class BuildSeries {
34-
final BuildPlan buildPlan;
38+
final BuildPlan _buildPlan;
3539

36-
final AssetGraph assetGraph;
37-
final BuildScriptUpdates? buildScriptUpdates;
40+
final AssetGraph _assetGraph;
3841

39-
final ReaderWriter readerWriter;
40-
final ResourceManager resourceManager = ResourceManager();
42+
final ReaderWriter _readerWriter;
43+
final ResourceManager _resourceManager = ResourceManager();
4144

4245
/// For the first build only, updates from the previous serialized build
4346
/// state.
4447
///
4548
/// Null after the first build, or if there was no serialized build state, or
4649
/// if the serialized build state was discarded.
47-
BuiltMap<AssetId, ChangeType>? updatesFromLoad;
50+
BuiltMap<AssetId, ChangeType>? _updatesFromLoad;
4851

4952
final StreamController<BuildResult> _buildResultsController =
5053
StreamController.broadcast();
5154

5255
/// Whether the next build is the first build.
5356
bool firstBuild = true;
5457

55-
Future<void> beforeExit() => resourceManager.beforeExit();
56-
57-
BuildSeries._(
58-
this.buildPlan,
59-
this.assetGraph,
60-
this.buildScriptUpdates,
61-
this.updatesFromLoad,
62-
) : readerWriter = buildPlan.readerWriter.copyWith(
63-
generatedAssetHider: assetGraph,
64-
cache:
65-
buildPlan.buildOptions.enableLowResourcesMode
66-
? const PassthroughFilesystemCache()
67-
: InMemoryFilesystemCache(),
68-
);
58+
BuildSeries._({
59+
required BuildPlan buildPlan,
60+
required AssetGraph assetGraph,
61+
required ReaderWriter readerWriter,
62+
required BuiltMap<AssetId, ChangeType>? updatesFromLoad,
63+
}) : _buildPlan = buildPlan,
64+
_assetGraph = assetGraph,
65+
_readerWriter = readerWriter,
66+
_updatesFromLoad = updatesFromLoad;
67+
68+
factory BuildSeries(BuildPlan buildPlan) {
69+
final assetGraph = buildPlan.takeAssetGraph();
70+
final readerWriter = buildPlan.readerWriter.copyWith(
71+
generatedAssetHider: assetGraph,
72+
cache:
73+
buildPlan.buildOptions.enableLowResourcesMode
74+
? const PassthroughFilesystemCache()
75+
: InMemoryFilesystemCache(),
76+
);
77+
return BuildSeries._(
78+
buildPlan: buildPlan,
79+
assetGraph: assetGraph,
80+
readerWriter: readerWriter,
81+
updatesFromLoad: buildPlan.updates,
82+
);
83+
}
6984

7085
/// Broadcast stream of build results.
7186
Stream<BuildResult> get buildResults => _buildResultsController.stream;
7287
Future<BuildResult>? _currentBuildResult;
7388

89+
bool _hasBuildScriptChanged(Set<AssetId> changes) {
90+
if (_buildPlan.buildOptions.skipBuildScriptCheck) return false;
91+
if (_buildPlan.buildScriptUpdates == null) return true;
92+
return _buildPlan.buildScriptUpdates!.hasBeenUpdated(changes);
93+
}
94+
95+
/// Returns whether [change] might trigger a build.
96+
///
97+
/// Pass expected deletes in [expectedDeletes]. Expected deletes do not
98+
/// trigger a build. A delete that matches is removed from the set.
99+
Future<bool> shouldProcess(
100+
AssetChange change,
101+
Set<AssetId> expectedDeletes,
102+
) async {
103+
// Ignore any expected delete once.
104+
if (change.type == ChangeType.REMOVE && expectedDeletes.remove(change.id)) {
105+
return false;
106+
}
107+
108+
final node =
109+
_assetGraph.contains(change.id) ? _assetGraph.get(change.id) : null;
110+
111+
// Changes to files that are not currently part of the build.
112+
if (node == null) {
113+
// Ignore under `.dart_tool/build`.
114+
if (change.id.path.startsWith(cacheDir)) return false;
115+
116+
// Ignore modifications and deletes.
117+
if (change.type != ChangeType.ADD) return false;
118+
119+
// It's an add: return whether it's a new input.
120+
return _buildPlan.targetGraph.anyMatchesAsset(change.id);
121+
}
122+
123+
// Changes to files that are part of the build.
124+
125+
// If not copying to a merged output directory, ignore changes to files with
126+
// no outputs.
127+
if (!_buildPlan.buildOptions.anyMergedOutputDirectory &&
128+
!node.changesRequireRebuild) {
129+
return false;
130+
}
131+
132+
// Ignore creation or modification of outputs.
133+
if (node.type == NodeType.generated && change.type != ChangeType.REMOVE) {
134+
return false;
135+
}
136+
137+
// For modifications, confirm that the content actually changed.
138+
if (change.type == ChangeType.MODIFY) {
139+
_readerWriter.cache.invalidate([change.id]);
140+
final newDigest = await _readerWriter.digest(change.id);
141+
return node.digest != newDigest;
142+
}
143+
144+
// It's an add of "missing source" node or a deletion of an input.
145+
return true;
146+
}
147+
148+
Future<List<WatchEvent>> checkForChanges() async {
149+
final updates = await AssetTracker(
150+
_buildPlan.readerWriter,
151+
_buildPlan.targetGraph,
152+
).collectChanges(_assetGraph);
153+
return List.of(
154+
updates.entries.map((entry) => WatchEvent(entry.value, '${entry.key}')),
155+
);
156+
}
157+
74158
/// If a build is running, the build result when it's done.
75159
///
76160
/// If no build has ever run, returns the first build result when it's
@@ -93,27 +177,39 @@ class BuildSeries {
93177
BuiltSet<BuildDirectory>? buildDirs,
94178
BuiltSet<BuildFilter>? buildFilters,
95179
}) async {
96-
buildDirs ??= buildPlan.buildOptions.buildDirs;
97-
buildFilters ??= buildPlan.buildOptions.buildFilters;
180+
if (_hasBuildScriptChanged(updates.keys.toSet())) {
181+
return BuildResult(
182+
status: BuildStatus.failure,
183+
failureType: FailureType.buildScriptChanged,
184+
buildOutputReader: BuildOutputReader(
185+
buildPlan: _buildPlan,
186+
readerWriter: _readerWriter,
187+
assetGraph: _assetGraph,
188+
),
189+
);
190+
}
191+
192+
buildDirs ??= _buildPlan.buildOptions.buildDirs;
193+
buildFilters ??= _buildPlan.buildOptions.buildFilters;
98194
if (firstBuild) {
99-
if (updatesFromLoad != null) {
100-
updates = updatesFromLoad!.toMap()..addAll(updates);
101-
updatesFromLoad = null;
195+
if (_updatesFromLoad != null) {
196+
updates = _updatesFromLoad!.toMap()..addAll(updates);
197+
_updatesFromLoad = null;
102198
}
103199
} else {
104-
if (updatesFromLoad != null) {
200+
if (_updatesFromLoad != null) {
105201
throw StateError('Only first build can have updates from load.');
106202
}
107203
}
108204

109205
final build = Build(
110-
buildPlan: buildPlan.copyWith(
206+
buildPlan: _buildPlan.copyWith(
111207
buildDirs: buildDirs,
112208
buildFilters: buildFilters,
113209
),
114-
assetGraph: assetGraph,
115-
readerWriter: readerWriter,
116-
resourceManager: resourceManager,
210+
assetGraph: _assetGraph,
211+
readerWriter: _readerWriter,
212+
resourceManager: _resourceManager,
117213
);
118214
if (firstBuild) firstBuild = false;
119215

@@ -123,14 +219,5 @@ class BuildSeries {
123219
return result;
124220
}
125221

126-
static Future<BuildSeries> create({required BuildPlan buildPlan}) async {
127-
final assetGraph = buildPlan.takeAssetGraph();
128-
final build = BuildSeries._(
129-
buildPlan,
130-
assetGraph,
131-
buildPlan.buildScriptUpdates,
132-
buildPlan.updates,
133-
);
134-
return build;
135-
}
222+
Future<void> beforeExit() => _resourceManager.beforeExit();
136223
}

build_runner/lib/src/build_plan/build_options.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ class BuildOptions {
3030
final bool trackPerformance;
3131
final bool verbose;
3232

33+
late final bool anyMergedOutputDirectory = buildDirs.any(
34+
(target) => target.outputLocation?.path.isNotEmpty ?? false,
35+
);
36+
3337
BuildOptions({
3438
required this.buildDirs,
3539
required this.builderConfigOverrides,

build_runner/lib/src/commands/build_command.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ class BuildCommand implements BuildRunnerCommand {
5656
return BuildResult.buildScriptChanged();
5757
}
5858

59-
final build = await BuildSeries.create(buildPlan: buildPlan);
60-
final result = await build.run({});
61-
await build.beforeExit();
59+
final buildSeries = BuildSeries(buildPlan);
60+
final result = await buildSeries.run({});
61+
await buildSeries.beforeExit();
6262
return result;
6363
}
6464
}

build_runner/lib/src/commands/daemon/change_providers.dart

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@ import 'dart:async';
77
import 'package:build_daemon/change_provider.dart';
88
import 'package:watcher/watcher.dart' show WatchEvent;
99

10-
import '../../build/asset_graph/graph.dart';
11-
import '../../io/asset_tracker.dart';
12-
1310
/// Continually updates the [changes] stream as watch events are seen on the
1411
/// input stream.
1512
class AutoChangeProviderImpl implements AutoChangeProvider {
@@ -22,16 +19,12 @@ class AutoChangeProviderImpl implements AutoChangeProvider {
2219
/// Computes changes with a file scan when requested by a call to
2320
/// [collectChanges].
2421
class ManualChangeProviderImpl implements ManualChangeProvider {
25-
final AssetGraph _assetGraph;
26-
final AssetTracker _assetTracker;
22+
final Future<List<WatchEvent>> Function() _function;
2723

28-
ManualChangeProviderImpl(this._assetTracker, this._assetGraph);
24+
ManualChangeProviderImpl(this._function);
2925

3026
@override
3127
Future<List<WatchEvent>> collectChanges() async {
32-
final updates = await _assetTracker.collectChanges(_assetGraph);
33-
return List.of(
34-
updates.entries.map((entry) => WatchEvent(entry.value, '${entry.key}')),
35-
);
28+
return _function();
3629
}
3730
}

build_runner/lib/src/commands/daemon/daemon_builder.dart

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,9 @@ import '../../build/build_series.dart';
2020
import '../../build_plan/build_directory.dart';
2121
import '../../build_plan/build_filter.dart';
2222
import '../../build_plan/build_plan.dart';
23-
import '../../io/asset_tracker.dart' show AssetTracker;
2423
import '../../logging/build_log.dart';
2524
import '../daemon_options.dart';
2625
import '../watch/asset_change.dart';
27-
import '../watch/change_filter.dart';
2826
import '../watch/collect_changes.dart';
2927
import '../watch/graph_watcher.dart';
3028
import '../watch/node_watcher.dart';
@@ -76,15 +74,6 @@ class BuildRunnerDaemonBuilder implements DaemonBuilder {
7674
)
7775
.toList();
7876

79-
if (!_buildPlan.buildOptions.skipBuildScriptCheck &&
80-
buildSeries.buildScriptUpdates!.hasBeenUpdated(
81-
changes.map<AssetId>((change) => change.id).toSet(),
82-
)) {
83-
if (!_buildScriptUpdateCompleter.isCompleted) {
84-
_buildScriptUpdateCompleter.complete();
85-
}
86-
return;
87-
}
8877
final targetNames = targets.map((t) => t.target).toSet();
8978
_logMessage(Level.INFO, 'About to build ${targetNames.toList()}...');
9079
_signalStart(targetNames);
@@ -124,6 +113,12 @@ class BuildRunnerDaemonBuilder implements DaemonBuilder {
124113
buildDirs: buildDirs.build(),
125114
buildFilters: buildFilters.build(),
126115
);
116+
if (result.failureType == core.FailureType.buildScriptChanged) {
117+
if (!_buildScriptUpdateCompleter.isCompleted) {
118+
_buildScriptUpdateCompleter.complete();
119+
}
120+
return;
121+
}
127122
final interestedInOutputs = targets.any(
128123
(e) => e is DefaultBuildTarget && e.reportChangedAssets,
129124
);
@@ -230,7 +225,7 @@ class BuildRunnerDaemonBuilder implements DaemonBuilder {
230225
),
231226
);
232227

233-
final buildSeries = await BuildSeries.create(buildPlan: buildPlan);
228+
final buildSeries = BuildSeries(buildPlan);
234229

235230
// Only actually used for the AutoChangeProvider.
236231
Stream<List<WatchEvent>> graphEvents() => PackageGraphWatcher(
@@ -239,15 +234,7 @@ class BuildRunnerDaemonBuilder implements DaemonBuilder {
239234
)
240235
.watch()
241236
.asyncWhere(
242-
(change) => shouldProcess(
243-
change,
244-
buildSeries.assetGraph,
245-
buildPlan.targetGraph,
246-
// Assume we will create an outputDir.
247-
true,
248-
expectedDeletes,
249-
buildPlan.readerWriter,
250-
),
237+
(change) => buildSeries.shouldProcess(change, expectedDeletes),
251238
)
252239
.map((data) => WatchEvent(data.type, '${data.id}'))
253240
.debounceBuffer(
@@ -258,10 +245,7 @@ class BuildRunnerDaemonBuilder implements DaemonBuilder {
258245
final changeProvider =
259246
daemonOptions.buildMode == BuildMode.Auto
260247
? AutoChangeProviderImpl(graphEvents())
261-
: ManualChangeProviderImpl(
262-
AssetTracker(buildPlan.readerWriter, buildPlan.targetGraph),
263-
buildSeries.assetGraph,
264-
);
248+
: ManualChangeProviderImpl(buildSeries.checkForChanges);
265249

266250
return BuildRunnerDaemonBuilder._(
267251
buildPlan,

build_runner/lib/src/commands/serve/server.dart

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,6 @@ class ServeHandler {
6363
'Only top level directories such as `web` or `test` can be served, got',
6464
);
6565
}
66-
_watcher.currentBuildResult.then((_) {
67-
// If the first build fails with a handled exception, we might not have
68-
// an asset graph and can't do this check.
69-
if (_watcher.assetGraph == null) return;
70-
_warnForEmptyDirectory(rootDir);
71-
});
7266
var cascade = shelf.Cascade();
7367
if (liveReload) {
7468
cascade = cascade.add(_webSocketHandler.createHandlerByRootDir(rootDir));
@@ -136,18 +130,6 @@ class ServeHandler {
136130
headers: {HttpHeaders.contentTypeHeader: 'application/json'},
137131
);
138132
}
139-
140-
void _warnForEmptyDirectory(String rootDir) {
141-
if (!_watcher.assetGraph!
142-
.packageNodes(_watcher.packageGraph.root.name)
143-
.any((n) => n.id.path.startsWith('$rootDir/'))) {
144-
buildLog.warning(
145-
'Requested a server for `$rootDir` but this directory '
146-
'has no assets in the build. You may need to add some sources or '
147-
'include this directory in some target in your `build.yaml`.',
148-
);
149-
}
150-
}
151133
}
152134

153135
/// Class that manages web socket connection handler to inform clients about

0 commit comments

Comments
 (0)