Skip to content

Commit de722ae

Browse files
authored
Handle build config changes without restarting. (#4224)
1 parent 573b427 commit de722ae

18 files changed

+182
-280
lines changed

build_runner/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## 2.8.1-wip
22

3+
- Watch mode: handle `build.yaml` changes without restarting.
4+
- Remove log output about `build_runner` internals.
35
- Print the port that gets picked if you pass 0 for a port number, for example
46
with `dart run build_runner serve web:0`.
57
- Improved warnings when an option is specified for an unknown builder.

build_runner/lib/src/bootstrap/bootstrap.dart

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ Future<int> generateAndRun(
6464
if (kernelFile.existsSync()) {
6565
kernelFile.deleteSync();
6666
}
67-
buildLog.fullBuildBecause(FullBuildReason.incompatibleScript);
6867
}
6968
} on CannotBuildException {
7069
return ExitCode.config.code;
@@ -109,8 +108,6 @@ Future<int> generateAndRun(
109108
);
110109
messagePort.sendPort.send(ExitCode.config.code);
111110
exitPort.sendPort.send(null);
112-
} else {
113-
buildLog.fullBuildBecause(FullBuildReason.incompatibleScript);
114111
}
115112
File(scriptKernelLocation).renameSync(scriptKernelCachedLocation);
116113
}
@@ -157,10 +154,8 @@ Future<bool> _createKernelIfNeeded(Iterable<String> experiments) async {
157154
// If we failed to serialize an asset graph for the snapshot, then we
158155
// don't want to re-use it because we can't check if it is up to date.
159156
kernelFile.renameSync(scriptKernelCachedLocation);
160-
buildLog.fullBuildBecause(FullBuildReason.incompatibleAssetGraph);
161157
} else if (!await _checkImportantPackageDepsAndExperiments(experiments)) {
162158
kernelFile.renameSync(scriptKernelCachedLocation);
163-
buildLog.fullBuildBecause(FullBuildReason.incompatibleScript);
164159
}
165160
}
166161

@@ -175,7 +170,6 @@ Future<bool> _createKernelIfNeeded(Iterable<String> experiments) async {
175170
);
176171

177172
var hadErrors = false;
178-
buildLog.doing('Compiling the build script.');
179173
try {
180174
final result = await client.compile();
181175
hadErrors = result.errorCount > 0 || !kernelCacheFile.existsSync();

build_runner/lib/src/bootstrap/build_process_state.dart

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,6 @@ class BuildProcessState {
3333
int get displayedLines => (_state['displayedLines'] as int?) ?? 0;
3434
set displayedLines(int? value) => _state['displayedLines'] = value;
3535

36-
/// For `buildLog`, the reason why a full build was needed.
37-
FullBuildReason get fullBuildReason => FullBuildReason.values.singleWhere(
38-
(v) => v.name == _state['fullBuildReason'],
39-
orElse: () => FullBuildReason.clean,
40-
);
41-
set fullBuildReason(FullBuildReason buildType) =>
42-
_state['fullBuildReason'] = buildType.name;
43-
4436
/// For `buildLog`, the elapsed time since the process started.
4537
int get elapsedMillis => _state['elapsedMillis'] as int? ?? 0;
4638
set elapsedMillis(int elapsedMillis) =>
@@ -104,20 +96,6 @@ class BuildProcessState {
10496
}
10597
}
10698

107-
/// Reason why `build_runner` will do a full build; or `none` for an
108-
/// incremental build.
109-
enum FullBuildReason {
110-
clean('full build'),
111-
incompatibleScript('full build because builders changed'),
112-
incompatibleAssetGraph('full build because there is no valid asset graph'),
113-
incompatibleBuild('full build because target changed'),
114-
none('incremental build');
115-
116-
const FullBuildReason(this.message);
117-
118-
final String message;
119-
}
120-
12199
/// The `BuildLog` mode for the process.
122100
enum BuildLogMode {
123101
/// Line by line logging.

build_runner/lib/src/bootstrap/build_script_generate.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'dart:async';
6+
import 'dart:io';
67

78
import 'package:build_config/build_config.dart';
89
import 'package:code_builder/code_builder.dart';
@@ -27,8 +28,13 @@ const scriptKernelCachedSuffix = '.cached';
2728

2829
final _lastShortFormatDartVersion = Version(3, 6, 0);
2930

31+
Future<bool> hasGeneratedBuildScriptChanged() async {
32+
final script = await generateBuildScript();
33+
final file = File(scriptLocation);
34+
return !file.existsSync() || file.readAsStringSync() != script;
35+
}
36+
3037
Future<String> generateBuildScript() async {
31-
buildLog.doing('Generating the build script.');
3238
final builderFactories = await loadBuilderFactories();
3339
final library = Library(
3440
(b) => b.body.addAll([

build_runner/lib/src/build/build.dart

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import 'package:glob/glob.dart';
1414
import 'package:path/path.dart' as p;
1515
import 'package:watcher/watcher.dart';
1616

17-
import '../bootstrap/build_process_state.dart';
1817
import '../build_plan/build_options.dart';
1918
import '../build_plan/build_phases.dart';
2019
import '../build_plan/build_plan.dart';
@@ -137,9 +136,6 @@ class Build {
137136
BuildPhases get buildPhases => buildPlan.buildPhases;
138137

139138
Future<BuildResult> run(Map<AssetId, ChangeType> updates) async {
140-
if (!assetGraph.cleanBuild) {
141-
buildLog.fullBuildBecause(FullBuildReason.none);
142-
}
143139
buildLog.configuration = buildLog.configuration.rebuild(
144140
(b) => b..rootPackageName = packageGraph.root.name,
145141
);
@@ -234,14 +230,11 @@ class Build {
234230
final done = Completer<BuildResult>();
235231
runZonedGuarded(
236232
() async {
237-
buildLog.doing('Updating the asset graph.');
238233
if (!assetGraph.cleanBuild) {
239234
await _updateAssetGraph(updates);
240235
}
241236

242-
buildLog.startBuild();
243237
final result = await _runPhases();
244-
buildLog.doing('Writing the asset graph.');
245238

246239
assetGraph.previousBuildTriggersDigest =
247240
targetGraph.buildTriggers.digest;
@@ -269,7 +262,6 @@ class Build {
269262

270263
// Log performance information if requested
271264
if (buildOptions.logPerformanceDir != null) {
272-
buildLog.doing('Writing the performance log.');
273265
assert(result.performance != null);
274266
final now = DateTime.now();
275267
final logPath = p.join(
@@ -365,7 +357,6 @@ class Build {
365357
}
366358

367359
// Post build phase.
368-
buildLog.doing('Running the post build.');
369360
if (buildPhases.postBuildPhase.builderActions.isNotEmpty) {
370361
outputs.addAll(
371362
await performanceTracker.trackBuildPhase(

build_runner/lib/src/build/build_result.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ enum BuildStatus { success, failure }
7878
class FailureType {
7979
static final general = FailureType._(1);
8080
static final cantCreate = FailureType._(73);
81-
static final buildConfigChanged = FailureType._(75);
8281
static final buildScriptChanged = FailureType._(75);
8382
final int exitCode;
8483
FailureType._(this.exitCode);

build_runner/lib/src/build/build_series.dart

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@ 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_generate.dart';
1112
import '../build_plan/build_directory.dart';
1213
import '../build_plan/build_filter.dart';
1314
import '../build_plan/build_plan.dart';
1415
import '../commands/watch/asset_change.dart';
1516
import '../constants.dart';
1617
import '../io/asset_tracker.dart';
17-
import '../io/build_output_reader.dart';
1818
import '../io/filesystem_cache.dart';
1919
import '../io/reader_writer.dart';
20+
import '../logging/build_log.dart';
2021
import 'asset_graph/graph.dart';
2122
import 'asset_graph/node.dart';
2223
import 'build.dart';
@@ -35,11 +36,10 @@ import 'build_result.dart';
3536
/// this serialized state is not actually used: the `AssetGraph` instance
3637
/// already in memory is used directly.
3738
class BuildSeries {
38-
final BuildPlan _buildPlan;
39+
BuildPlan _buildPlan;
40+
AssetGraph _assetGraph;
41+
ReaderWriter _readerWriter;
3942

40-
final AssetGraph _assetGraph;
41-
42-
final ReaderWriter _readerWriter;
4343
final ResourceManager _resourceManager = ResourceManager();
4444

4545
/// For the first build only, updates from the previous serialized build
@@ -105,19 +105,21 @@ class BuildSeries {
105105
return false;
106106
}
107107

108-
final node =
109-
_assetGraph.contains(change.id) ? _assetGraph.get(change.id) : null;
108+
final id = change.id;
109+
if (_isBuildConfiguration(id)) return true;
110+
111+
final node = _assetGraph.contains(id) ? _assetGraph.get(id) : null;
110112

111113
// Changes to files that are not currently part of the build.
112114
if (node == null) {
113115
// Ignore under `.dart_tool/build`.
114-
if (change.id.path.startsWith(cacheDir)) return false;
116+
if (id.path.startsWith(cacheDir)) return false;
115117

116118
// Ignore modifications and deletes.
117119
if (change.type != ChangeType.ADD) return false;
118120

119121
// It's an add: return whether it's a new input.
120-
return _buildPlan.targetGraph.anyMatchesAsset(change.id);
122+
return _buildPlan.targetGraph.anyMatchesAsset(id);
121123
}
122124

123125
// Changes to files that are part of the build.
@@ -136,15 +138,21 @@ class BuildSeries {
136138

137139
// For modifications, confirm that the content actually changed.
138140
if (change.type == ChangeType.MODIFY) {
139-
_readerWriter.cache.invalidate([change.id]);
140-
final newDigest = await _readerWriter.digest(change.id);
141+
_readerWriter.cache.invalidate([id]);
142+
final newDigest = await _readerWriter.digest(id);
141143
return node.digest != newDigest;
142144
}
143145

144146
// It's an add of "missing source" node or a deletion of an input.
145147
return true;
146148
}
147149

150+
bool _isBuildConfiguration(AssetId id) =>
151+
id.path == 'build.yaml' ||
152+
id.path.endsWith('.build.yaml') ||
153+
(id.package == _buildPlan.packageGraph.root.name &&
154+
id.path == 'build.${_buildPlan.buildOptions.configKey}.yaml');
155+
148156
Future<List<WatchEvent>> checkForChanges() async {
149157
final updates = await AssetTracker(
150158
_buildPlan.readerWriter,
@@ -178,14 +186,29 @@ class BuildSeries {
178186
BuiltSet<BuildFilter>? buildFilters,
179187
}) async {
180188
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+
return BuildResult.buildScriptChanged();
190+
}
191+
192+
if (updates.keys.any(_isBuildConfiguration)) {
193+
_buildPlan = await _buildPlan.reload();
194+
await _buildPlan.deleteFilesAndFolders();
195+
// A config change might have caused new builders to be needed, which
196+
// needs a restart to change the build script.
197+
if (_buildPlan.restartIsNeeded) {
198+
return BuildResult.buildScriptChanged();
199+
}
200+
// A config change might have changed builder factories, which needs a
201+
// restart to change the build script.
202+
if (await hasGeneratedBuildScriptChanged()) {
203+
return BuildResult.buildScriptChanged();
204+
}
205+
_assetGraph = _buildPlan.takeAssetGraph();
206+
_readerWriter = _buildPlan.readerWriter.copyWith(
207+
generatedAssetHider: _assetGraph,
208+
cache:
209+
_buildPlan.buildOptions.enableLowResourcesMode
210+
? const PassthroughFilesystemCache()
211+
: InMemoryFilesystemCache(),
189212
);
190213
}
191214

@@ -202,6 +225,7 @@ class BuildSeries {
202225
}
203226
}
204227

228+
if (!firstBuild) buildLog.nextBuild();
205229
final build = Build(
206230
buildPlan: _buildPlan.copyWith(
207231
buildDirs: buildDirs,

build_runner/lib/src/build_plan/build_plan.dart

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import 'package:build/experiments.dart';
99
import 'package:built_collection/built_collection.dart';
1010
import 'package:watcher/watcher.dart';
1111

12-
import '../bootstrap/build_process_state.dart';
1312
import '../bootstrap/build_script_updates.dart';
1413
import '../build/asset_graph/exceptions.dart';
1514
import '../build/asset_graph/graph.dart';
@@ -125,7 +124,6 @@ class BuildPlan {
125124
var restartIsNeeded = false;
126125
if (builderApplications == null) {
127126
restartIsNeeded = true;
128-
buildLog.fullBuildBecause(FullBuildReason.incompatibleScript);
129127
builderApplications = BuiltList();
130128
}
131129

@@ -139,11 +137,8 @@ class BuildPlan {
139137
);
140138
buildPhases.checkOutputLocations(packageGraph.root.name);
141139
if (buildPhases.inBuildPhases.isEmpty &&
142-
buildPhases.postBuildPhase.builderActions.isEmpty) {
143-
buildLog.warning('Nothing to build.');
144-
}
140+
buildPhases.postBuildPhase.builderActions.isEmpty) {}
145141

146-
buildLog.doing('Reading the asset graph.');
147142
AssetGraph? previousAssetGraph;
148143
final filesToDelete = <AssetId>{};
149144
final foldersToDelete = <AssetId>{};
@@ -158,9 +153,7 @@ class BuildPlan {
158153
previousAssetGraph = AssetGraph.deserialize(
159154
await readerWriter.readAsBytes(assetGraphId),
160155
);
161-
if (previousAssetGraph == null) {
162-
buildLog.fullBuildBecause(FullBuildReason.incompatibleAssetGraph);
163-
} else {
156+
if (previousAssetGraph != null) {
164157
final buildPhasesChanged =
165158
buildPhases.digest != previousAssetGraph.buildPhasesDigest;
166159
final pkgVersionsChanged =
@@ -175,7 +168,6 @@ class BuildPlan {
175168
previousAssetGraph.dartVersion,
176169
Platform.version,
177170
)) {
178-
buildLog.fullBuildBecause(FullBuildReason.incompatibleBuild);
179171
// Mark old outputs for deletion.
180172
filesToDelete.addAll(
181173
previousAssetGraph.outputsToDelete(packageGraph),
@@ -211,7 +203,6 @@ class BuildPlan {
211203
BuildScriptUpdates? buildScriptUpdates;
212204
Map<AssetId, ChangeType>? updates;
213205
if (previousAssetGraph != null) {
214-
buildLog.doing('Checking for updates.');
215206
updates = await assetTracker.computeSourceUpdates(
216207
inputSources,
217208
cacheDirSources,
@@ -229,7 +220,6 @@ class BuildPlan {
229220
!buildOptions.skipBuildScriptCheck &&
230221
buildScriptUpdates.hasBeenUpdated(updates.keys.toSet());
231222
if (buildScriptUpdated) {
232-
buildLog.fullBuildBecause(FullBuildReason.incompatibleScript);
233223
// Mark old outputs for deletion.
234224
filesToDelete.addAll(previousAssetGraph.outputsToDelete(packageGraph));
235225
foldersToDelete.add(generatedOutputDirectoryId);
@@ -255,8 +245,6 @@ class BuildPlan {
255245
}
256246

257247
if (assetGraph == null) {
258-
buildLog.doing('Creating the asset graph.');
259-
260248
// Files marked for deletion are not inputs.
261249
inputSources.removeAll(filesToDelete);
262250

@@ -369,8 +357,6 @@ class BuildPlan {
369357
}
370358

371359
Future<void> deleteFilesAndFolders() async {
372-
buildLog.doing('Doing initial build cleanup.');
373-
374360
// Hidden outputs are deleted if needed by deleting the entire folder. So,
375361
// only outputs in the source folder need to be deleted explicitly. Use a
376362
// `ReaderWriter` that only acts on the source folder.
@@ -386,6 +372,19 @@ class BuildPlan {
386372
await cleanupReaderWriter.deleteDirectory(id);
387373
}
388374
}
375+
376+
/// Reloads the build plan.
377+
///
378+
/// Works just like a new load of the build plan, but supresses the usual log
379+
/// output.
380+
///
381+
/// The caller must call [deleteFilesAndFolders] on the result and check
382+
/// [restartIsNeeded].
383+
Future<BuildPlan> reload() => BuildPlan.load(
384+
builderFactories: builderFactories,
385+
buildOptions: buildOptions,
386+
testingOverrides: testingOverrides,
387+
);
389388
}
390389

391390
bool isSameSdkVersion(String? thisVersion, String? thatVersion) =>

0 commit comments

Comments
 (0)