Skip to content

Commit cdbf06f

Browse files
authored
Introduce BuildProcessState, sent to and returned from spawned isolates. (#4024)
Always rebuild if script changed, even if there is an asset graph.
1 parent 836f545 commit cdbf06f

File tree

6 files changed

+299
-65
lines changed

6 files changed

+299
-65
lines changed

_test/test/goldens/generated_build_script.dart

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import 'package:build_config/build_config.dart' as _i5;
99
import 'package:build_modules/builders.dart' as _i6;
1010
import 'package:build/build.dart' as _i7;
1111
import 'dart:isolate' as _i8;
12-
import 'package:build_runner/build_runner.dart' as _i9;
13-
import 'dart:io' as _i10;
12+
import 'package:build_runner/src/build_script_generate/build_process_state.dart'
13+
as _i9;
14+
import 'package:build_runner/build_runner.dart' as _i10;
15+
import 'dart:io' as _i11;
1416

1517
final _builders = <_i1.BuilderApplication>[
1618
_i1.apply(
@@ -169,10 +171,11 @@ void main(
169171
List<String> args, [
170172
_i8.SendPort? sendPort,
171173
]) async {
172-
var result = await _i9.run(
174+
await _i9.buildProcessState.receive(sendPort);
175+
_i9.buildProcessState.isolateExitCode = await _i10.run(
173176
args,
174177
_builders,
175178
);
176-
sendPort?.send(result);
177-
_i10.exitCode = result;
179+
_i11.exitCode = _i9.buildProcessState.isolateExitCode!;
180+
await _i9.buildProcessState.send(sendPort);
178181
}

build_runner/lib/src/build_script_generate/bootstrap.dart

Lines changed: 50 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'package:logging/logging.dart';
1313
import 'package:path/path.dart' as p;
1414
import 'package:stack_trace/stack_trace.dart';
1515

16+
import 'build_process_state.dart';
1617
import 'build_script_generate.dart';
1718

1819
final _logger = Logger('Bootstrap');
@@ -39,9 +40,8 @@ Future<int> generateAndRun(
3940
logger ??= _logger;
4041
ReceivePort? exitPort;
4142
ReceivePort? errorPort;
42-
ReceivePort? messagePort;
43+
RawReceivePort? messagePort;
4344
StreamSubscription? errorListener;
44-
int? scriptExitCode;
4545

4646
var tryCount = 0;
4747
var succeeded = false;
@@ -52,6 +52,7 @@ Future<int> generateAndRun(
5252
messagePort?.close();
5353
await errorListener?.cancel();
5454

55+
var buildScriptChanged = false;
5556
try {
5657
var buildScript = File(scriptLocation);
5758
var oldContents = '';
@@ -61,6 +62,7 @@ Future<int> generateAndRun(
6162
var newContents = await generateBuildScript();
6263
// Only trigger a build script update if necessary.
6364
if (newContents != oldContents) {
65+
buildScriptChanged = true;
6466
buildScript
6567
..createSync(recursive: true)
6668
..writeAsStringSync(newContents);
@@ -69,19 +71,27 @@ Future<int> generateAndRun(
6971
return ExitCode.config.code;
7072
}
7173

72-
scriptExitCode = await _createKernelIfNeeded(logger, experiments);
73-
if (scriptExitCode != 0) return scriptExitCode!;
74+
if (!await _createKernelIfNeeded(
75+
logger,
76+
experiments,
77+
buildScriptChanged: buildScriptChanged,
78+
)) {
79+
return buildProcessState.isolateExitCode = ExitCode.config.code;
80+
}
7481

7582
exitPort = ReceivePort();
7683
errorPort = ReceivePort();
77-
messagePort = ReceivePort();
84+
messagePort = RawReceivePort();
7885
errorListener = errorPort.listen((e) {
7986
e = e as List<Object?>;
8087
final error = e[0] ?? TypeError();
8188
final trace = Trace.parse(e[1] as String? ?? '').terse;
8289

8390
handleUncaughtError(error, trace);
84-
if (scriptExitCode == 0) scriptExitCode = 1;
91+
if (buildProcessState.isolateExitCode == null ||
92+
buildProcessState.isolateExitCode == 0) {
93+
buildProcessState.isolateExitCode = 1;
94+
}
8595
});
8696
try {
8797
await Isolate.spawnUri(
@@ -113,56 +123,64 @@ Future<int> generateAndRun(
113123
}
114124
}
115125

116-
StreamSubscription? exitCodeListener;
117-
exitCodeListener = messagePort!.listen((isolateExitCode) {
118-
if (isolateExitCode is int) {
119-
scriptExitCode = isolateExitCode;
120-
} else {
121-
throw StateError(
122-
'Bad response from isolate, expected an exit code but got '
123-
'$isolateExitCode',
124-
);
125-
}
126-
exitCodeListener!.cancel();
127-
exitCodeListener = null;
128-
});
126+
final sendPortCompleter = Completer<SendPort>();
127+
messagePort!.handler = (Object? message) {
128+
sendPortCompleter.complete(message as SendPort);
129+
};
130+
final sendPort = await sendPortCompleter.future;
131+
132+
await buildProcessState.send(sendPort);
133+
buildProcessState.isolateExitCode = null;
134+
final buildProcessStateListener = buildProcessState.listen(
135+
ReceivePort.fromRawReceivePort(messagePort),
136+
);
137+
129138
await exitPort?.first;
130139
await errorListener?.cancel();
131-
await exitCodeListener?.cancel();
140+
await buildProcessStateListener.cancel();
141+
142+
// Can be null if the isolate did not set any exit code.
143+
buildProcessState.isolateExitCode ??= 1;
132144

133-
return scriptExitCode ?? 1;
145+
return buildProcessState.isolateExitCode!;
134146
}
135147

136148
/// Creates a precompiled Kernel snapshot for the build script if necessary.
137149
///
138150
/// A snapshot is generated if:
139151
///
152+
/// - [buildScriptChanged] is `true`
140153
/// - It doesn't exist currently
141154
/// - Either build_runner or build_daemon point at a different location than
142155
/// they used to, see https://github.com/dart-lang/build/issues/1929.
143156
///
144-
/// Returns zero for success or a number for failure which should be set to the
145-
/// exit code.
146-
Future<int> _createKernelIfNeeded(
157+
/// Returns `true` on success or `false` on failure.
158+
Future<bool> _createKernelIfNeeded(
147159
Logger logger,
148-
List<String> experiments,
149-
) async {
160+
List<String> experiments, {
161+
bool buildScriptChanged = false,
162+
}) async {
150163
var assetGraphFile = File(assetGraphPathFor(scriptKernelLocation));
151164
var kernelFile = File(scriptKernelLocation);
152165
var kernelCacheFile = File(scriptKernelCachedLocation);
153166

154167
if (await kernelFile.exists()) {
155-
// If we failed to serialize an asset graph for the snapshot, then we don't
156-
// want to re-use it because we can't check if it is up to date.
157-
if (!await assetGraphFile.exists()) {
168+
if (buildScriptChanged) {
169+
await kernelFile.rename(scriptKernelCachedLocation);
170+
logger.warning(
171+
'Invalidated precompiled build script because script changed.',
172+
);
173+
} else if (!await assetGraphFile.exists()) {
174+
// If we failed to serialize an asset graph for the snapshot, then we
175+
// don't want to re-use it because we can't check if it is up to date.
158176
await kernelFile.rename(scriptKernelCachedLocation);
159177
logger.warning(
160178
'Invalidated precompiled build script due to missing asset graph.',
161179
);
162180
} else if (!await _checkImportantPackageDepsAndExperiments(experiments)) {
163181
await kernelFile.rename(scriptKernelCachedLocation);
164182
logger.warning(
165-
'Invalidated precompiled build script due to core package update',
183+
'Invalidated precompiled build script due to core package update.',
166184
);
167185
}
168186
}
@@ -223,12 +241,12 @@ Future<int> _createKernelIfNeeded(
223241
Failed to precompile build script $scriptLocation.
224242
This is likely caused by a misconfigured builder definition.
225243
''');
226-
return ExitCode.config.code;
244+
return false;
227245
}
228246
// Create _previousLocationsFile.
229247
await _checkImportantPackageDepsAndExperiments(experiments);
230248
}
231-
return 0;
249+
return true;
232250
}
233251

234252
const _importantPackages = ['build_daemon', 'build_runner'];
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:isolate';
7+
8+
/// State for the whole build process.
9+
///
10+
/// It's passed from the entrypoint to the spawned build script isolate, then
11+
/// updated in the host when the isolate exits.
12+
final BuildProcessState buildProcessState = BuildProcessState({});
13+
14+
extension type BuildProcessState(Map<String, Object?> state) {
15+
/// The exit code of the most recent build script isolate, or `null` if there
16+
/// was none or it is currently running.
17+
int? get isolateExitCode => state['isolateExitCode'] as int?;
18+
set isolateExitCode(int? value) => state['isolateExitCode'] = value;
19+
20+
/// Sends `this` to [sendPort].
21+
Future<void> send(SendPort? sendPort) async {
22+
sendPort?.send(state);
23+
}
24+
25+
/// Receives `this` from [sendPort], by sending a `SendPort` then listening
26+
/// on its corresponding `ReceivePort`.
27+
Future<void> receive(SendPort? sendPort) async {
28+
if (sendPort == null) {
29+
state.clear();
30+
return;
31+
}
32+
final receivePort = ReceivePort();
33+
sendPort.send(receivePort.sendPort);
34+
final received = await receivePort.first;
35+
state
36+
..clear()
37+
..addAll(received as Map<String, Object?>);
38+
}
39+
40+
/// Receives `this` from [receivePort].
41+
StreamSubscription<void> listen(ReceivePort receivePort) {
42+
StreamSubscription<void>? result;
43+
result = receivePort.listen((isolateState) {
44+
if (isolateState is Map<String, Object?>) {
45+
state
46+
..clear()
47+
..addAll(isolateState);
48+
} else {
49+
throw StateError(
50+
'Bad response from isolate, expected Map but got $isolateState',
51+
);
52+
}
53+
result!.cancel();
54+
});
55+
return result;
56+
}
57+
}

build_runner/lib/src/build_script_generate/build_script_generate.dart

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -227,19 +227,28 @@ Method _main() => Method((b) {
227227
});
228228
}),
229229
);
230+
final isolateExitCode = refer(
231+
'buildProcessState.isolateExitCode',
232+
'package:build_runner/src/build_script_generate/build_process_state.dart',
233+
);
230234
b.body = Block.of([
231-
declareVar('result')
235+
refer(
236+
'buildProcessState.receive',
237+
'package:build_runner/src/build_script_generate/build_process_state.dart',
238+
).call([refer('sendPort')]).awaited.statement,
239+
isolateExitCode
232240
.assign(
233241
refer(
234242
'run',
235243
'package:build_runner/build_runner.dart',
236244
).call([refer('args'), refer('_builders')]).awaited,
237245
)
238246
.statement,
247+
refer('exitCode', 'dart:io').assign(isolateExitCode).nullChecked.statement,
239248
refer(
240-
'sendPort',
241-
).nullSafeProperty('send').call([refer('result')]).statement,
242-
refer('exitCode', 'dart:io').assign(refer('result')).statement,
249+
'buildProcessState.send',
250+
'package:build_runner/src/build_script_generate/build_process_state.dart',
251+
).call([refer('sendPort')]).awaited.statement,
243252
]);
244253
});
245254

0 commit comments

Comments
 (0)