Skip to content

Commit ce2491c

Browse files
authored
Compile AOT. (#4250)
* Compile AOT. * Address review comments.
1 parent 1c3d472 commit ce2491c

19 files changed

+506
-76
lines changed

build_runner/CHANGELOG.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
## 2.9.1-wip
22

3-
- Require `analyzer` 8.0.0. Remove use of deprecated `analyzer` members, use
4-
their recommended and compatible replacements.
5-
- Internal changes for `build_test`.
3+
- Add AOT compilation of builders. A future release will AOT compile builders
4+
automatically, for this release it's behind a flag. AOT compiled builders
5+
start up faster and have higher throughput, for faster builds overall.
6+
Builders that use `dart:mirrors` cannot be AOT compiled.
7+
- Add `force-aot` flag to AOT compile builders.
8+
- Add `force-jit` flag to force the current default of JIT compiling builders.
69
- Add the `--dart-jit-vm-arg` option. Its values are passed to `dart run` when
710
a build script is started in JIT mode. This allows specifying options to
811
attach a debugger to builders.
12+
- Require `analyzer` 8.0.0. Remove use of deprecated `analyzer` members, use
13+
their recommended and compatible replacements.
14+
- Internal changes for `build_test`.
915

1016
## 2.9.0
1117

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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:io';
6+
7+
import '../constants.dart';
8+
import 'compiler.dart';
9+
import 'depfile.dart';
10+
import 'processes.dart';
11+
12+
const entrypointAotPath = '$entrypointScriptPath.aot';
13+
const entrypointAotDepfilePath = '$entrypointScriptPath.aot.deps';
14+
const entrypointAotDigestPath = '$entrypointScriptPath.aot.digest';
15+
16+
/// Compiles the build script to an AOT snapshot.
17+
class AotCompiler implements Compiler {
18+
final Depfile _outputDepfile = Depfile(
19+
outputPath: entrypointAotPath,
20+
depfilePath: entrypointAotDepfilePath,
21+
digestPath: entrypointAotDigestPath,
22+
);
23+
24+
@override
25+
FreshnessResult checkFreshness({required bool digestsAreFresh}) =>
26+
_outputDepfile.checkFreshness(digestsAreFresh: digestsAreFresh);
27+
28+
@override
29+
bool isDependency(String path) => _outputDepfile.isDependency(path);
30+
31+
@override
32+
Future<CompileResult> compile({Iterable<String>? experiments}) async {
33+
final dart = Platform.resolvedExecutable;
34+
final result = await ParentProcess.run(dart, [
35+
'compile',
36+
'aot-snapshot',
37+
entrypointScriptPath,
38+
'--output',
39+
entrypointAotPath,
40+
'--depfile',
41+
entrypointAotDepfilePath,
42+
if (experiments != null)
43+
for (final experiment in experiments) '--enable-experiment=$experiment',
44+
]);
45+
46+
if (result.exitCode == 0) {
47+
final stdout = result.stdout as String;
48+
49+
// Convert "unknown experiment" warnings to errors.
50+
if (stdout.contains('Unknown experiment:')) {
51+
if (File(entrypointAotPath).existsSync()) {
52+
File(entrypointAotPath).deleteSync();
53+
}
54+
final messages = stdout
55+
.split('\n')
56+
.where((e) => e.startsWith('Unknown experiment'))
57+
.join('\n');
58+
return CompileResult(messages: messages);
59+
}
60+
61+
// Update depfile digest on successful compile.
62+
_outputDepfile.writeDigest();
63+
}
64+
65+
var stderr = result.stderr as String;
66+
// Tidy up the compiler output to leave just the failure.
67+
stderr =
68+
stderr
69+
.replaceAll('Error: AOT compilation failed', '')
70+
.replaceAll('Bad state: Generating AOT snapshot failed!', '')
71+
.trim();
72+
return CompileResult(messages: result.exitCode == 0 ? null : stderr);
73+
}
74+
}

build_runner/lib/src/bootstrap/bootstrapper.dart

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'dart:async';
56
import 'dart:io';
67

78
import 'package:built_collection/built_collection.dart';
89
import 'package:io/io.dart';
910

1011
import '../exceptions.dart';
1112
import '../internal.dart';
13+
import 'aot_compiler.dart';
14+
import 'compiler.dart';
1215
import 'depfile.dart';
1316
import 'kernel_compiler.dart';
1417
import 'processes.dart';
@@ -24,9 +27,14 @@ import 'processes.dart';
2427
/// contents of all these files so they can be checked for freshness later.
2528
///
2629
/// The entrypoint script is launched using [ParentProcess.runAndSend]
27-
/// which passes initial state to it and received updated state when it exits.
30+
/// or [ParentProcess.runAotSnapshotAndSend] which passes initial state to it
31+
/// and receives updated state when it exits.
2832
class Bootstrapper {
29-
final KernelCompiler _compiler = KernelCompiler();
33+
final bool compileAot;
34+
final Compiler _compiler;
35+
36+
Bootstrapper({required this.compileAot})
37+
: _compiler = compileAot ? AotCompiler() : KernelCompiler();
3038

3139
/// Generates the entrypoint script, compiles it and runs it with [arguments].
3240
///
@@ -52,24 +60,46 @@ class Bootstrapper {
5260
if (!_compiler.checkFreshness(digestsAreFresh: false).outputIsFresh) {
5361
final result = await _compiler.compile(experiments: experiments);
5462
if (!result.succeeded) {
55-
if (result.messages != null) {
63+
final bool failedDueToMirrors;
64+
if (result.messages == null) {
65+
failedDueToMirrors = false;
66+
} else {
5667
buildLog.error(result.messages!);
68+
failedDueToMirrors =
69+
compileAot && result.messages!.contains('dart:mirrors');
70+
}
71+
if (failedDueToMirrors) {
72+
// TODO(davidmorgan): when build_runner manages use of AOT compile
73+
// this will be an automatic fallback to JIT instead of a message.
74+
buildLog.error(
75+
'Failed to compile build script. A configured builder '
76+
'uses `dart:mirrors` and cannot be compiled AOT. Try again '
77+
'without --force-aot to use a JIT compile.',
78+
);
79+
} else {
80+
buildLog.error(
81+
'Failed to compile build script. '
82+
'Check builder definitions and generated script '
83+
'$entrypointScriptPath.',
84+
);
5785
}
58-
buildLog.error(
59-
'Failed to compile build script. '
60-
'Check builder definitions and generated script '
61-
'$entrypointScriptPath.',
62-
);
6386
throw const CannotBuildException();
6487
}
6588
}
6689

67-
final result = await ParentProcess.runAndSend(
68-
script: entrypointDillPath,
69-
arguments: arguments,
70-
message: buildProcessState.serialize(),
71-
jitVmArgs: jitVmArgs,
72-
);
90+
final result =
91+
compileAot
92+
? await ParentProcess.runAotSnapshotAndSend(
93+
aotSnapshot: entrypointAotPath,
94+
arguments: arguments,
95+
message: buildProcessState.serialize(),
96+
)
97+
: await ParentProcess.runAndSend(
98+
script: entrypointDillPath,
99+
arguments: arguments,
100+
message: buildProcessState.serialize(),
101+
jitVmArgs: jitVmArgs,
102+
);
73103
buildProcessState.deserializeAndSet(result.message);
74104
final exitCode = result.exitCode;
75105

@@ -94,11 +124,11 @@ class Bootstrapper {
94124
}
95125
}
96126

97-
/// Checks freshness of the entrypoint script compiled to kernel.
127+
/// Checks freshness of the compiled entrypoint script.
98128
///
99129
/// Set [digestsAreFresh] if digests were very recently updated. Then, they
100130
/// will be re-used from disk if possible instead of recomputed.
101-
Future<FreshnessResult> checkKernelFreshness({
131+
Future<FreshnessResult> checkCompileFreshness({
102132
required bool digestsAreFresh,
103133
}) async {
104134
if (!ChildProcess.isRunning) {
@@ -116,9 +146,8 @@ class Bootstrapper {
116146
return _compiler.checkFreshness(digestsAreFresh: false);
117147
}
118148

119-
/// Whether [path] is a dependency of the entrypoint script compiled to
120-
/// kernel.
121-
bool isKernelDependency(String path) {
149+
/// Whether [path] is a dependency of the compiled entrypoint script.
150+
bool isCompileDependency(String path) {
122151
if (!ChildProcess.isRunning) {
123152
// Any real use or realistic test has a child process; so this is only hit
124153
// in small tests. Return "not a dependency" so nothing related to

build_runner/lib/src/bootstrap/build_process_state.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ class BuildProcessState {
4545
set elapsedMillis(int elapsedMillis) =>
4646
_state['elapsedMillis'] = elapsedMillis;
4747

48+
/// The package config URI.
49+
String get packageConfigUri =>
50+
(_state['packageConfigUri'] ??= Isolate.packageConfigSync!.toString())
51+
as String;
52+
4853
void resetForTests() {
4954
_state.clear();
5055
}
@@ -55,6 +60,10 @@ class BuildProcessState {
5560
}
5661

5762
String serialize() {
63+
// Set any unset values that should be set by the parent process.
64+
stdio;
65+
packageConfigUri;
66+
5867
for (final beforeSend in _beforeSends) {
5968
beforeSend();
6069
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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 'depfile.dart';
6+
7+
/// Compiles the build script.
8+
abstract class Compiler {
9+
/// Checks freshness of the build script compile output.
10+
///
11+
/// Set [digestsAreFresh] if digests were very recently updated. Then, they
12+
/// will be re-used from disk if possible instead of recomputed.
13+
FreshnessResult checkFreshness({required bool digestsAreFresh});
14+
15+
/// Checks whether [path] in a dependency of the build script compile.
16+
///
17+
/// Call [checkFreshness] first to load the depfile.
18+
bool isDependency(String path);
19+
20+
/// Compiles the entrypoint script.
21+
Future<CompileResult> compile({Iterable<String>? experiments});
22+
}
23+
24+
class CompileResult {
25+
final String? messages;
26+
27+
CompileResult({required this.messages});
28+
29+
bool get succeeded => messages == null;
30+
31+
@override
32+
String toString() =>
33+
'CompileResult(succeeded: $succeeded, messages: $messages)';
34+
}

build_runner/lib/src/bootstrap/kernel_compiler.dart

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'dart:io';
66

77
import '../constants.dart';
8+
import 'compiler.dart';
89
import 'depfile.dart';
910
import 'processes.dart';
1011

@@ -13,26 +14,21 @@ const entrypointDillDepfilePath = '$entrypointScriptPath.dill.deps';
1314
const entrypointDillDigestPath = '$entrypointScriptPath.dill.digest';
1415

1516
/// Compiles the build script to kernel.
16-
class KernelCompiler {
17+
class KernelCompiler implements Compiler {
1718
final Depfile _outputDepfile = Depfile(
1819
outputPath: entrypointDillPath,
1920
depfilePath: entrypointDillDepfilePath,
2021
digestPath: entrypointDillDigestPath,
2122
);
2223

23-
/// Checks freshness of the build script compiled kernel.
24-
///
25-
/// Set [digestsAreFresh] if digests were very recently updated. Then, they
26-
/// will be re-used from disk if possible instead of recomputed.
24+
@override
2725
FreshnessResult checkFreshness({required bool digestsAreFresh}) =>
2826
_outputDepfile.checkFreshness(digestsAreFresh: digestsAreFresh);
2927

30-
/// Checks whether [path] in a dependency of the build script compiled kernel.
31-
///
32-
/// Call [checkFreshness] first to load the depfile.
28+
@override
3329
bool isDependency(String path) => _outputDepfile.isDependency(path);
3430

35-
/// Compiles the entrypoint script to kernel.
31+
@override
3632
Future<CompileResult> compile({Iterable<String>? experiments}) async {
3733
final dart = Platform.resolvedExecutable;
3834
final result = await ParentProcess.run(dart, [
@@ -58,8 +54,7 @@ class KernelCompiler {
5854
final messages = stdout
5955
.split('\n')
6056
.where((e) => e.startsWith('Unknown experiment'))
61-
.map((l) => '$l\n')
62-
.join('');
57+
.join('\n');
6358
return CompileResult(messages: messages);
6459
}
6560

@@ -74,15 +69,3 @@ class KernelCompiler {
7469
return CompileResult(messages: result.exitCode == 0 ? null : stderr);
7570
}
7671
}
77-
78-
class CompileResult {
79-
final String? messages;
80-
81-
CompileResult({required this.messages});
82-
83-
bool get succeeded => messages == null;
84-
85-
@override
86-
String toString() =>
87-
'CompileResult(succeeded: $succeeded, messages: $messages)';
88-
}

0 commit comments

Comments
 (0)