Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions build_runner/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
## 2.9.1-wip

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

## 2.9.0

Expand Down
74 changes: 74 additions & 0 deletions build_runner/lib/src/bootstrap/aot_compiler.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) 2025, 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:io';

import '../constants.dart';
import 'compiler.dart';
import 'depfile.dart';
import 'processes.dart';

const entrypointAotPath = '$entrypointScriptPath.aot';
const entrypointAotDepfilePath = '$entrypointScriptPath.aot.deps';
const entrypointAotDigestPath = '$entrypointScriptPath.aot.digest';

/// Compiles the build script to an AOT snapshot.
class AotCompiler implements Compiler {
final Depfile _outputDepfile = Depfile(
outputPath: entrypointAotPath,
depfilePath: entrypointAotDepfilePath,
digestPath: entrypointAotDigestPath,
);

@override
FreshnessResult checkFreshness({required bool digestsAreFresh}) =>
_outputDepfile.checkFreshness(digestsAreFresh: digestsAreFresh);

@override
bool isDependency(String path) => _outputDepfile.isDependency(path);

@override
Future<CompileResult> compile({Iterable<String>? experiments}) async {
final dart = Platform.resolvedExecutable;
final result = await ParentProcess.run(dart, [
'compile',
'aot-snapshot',
entrypointScriptPath,
'--output',
entrypointAotPath,
'--depfile',
entrypointAotDepfilePath,
if (experiments != null)
for (final experiment in experiments) '--enable-experiment=$experiment',
]);

if (result.exitCode == 0) {
final stdout = result.stdout as String;

// Convert "unknown experiment" warnings to errors.
if (stdout.contains('Unknown experiment:')) {
if (File(entrypointAotPath).existsSync()) {
File(entrypointAotPath).deleteSync();
}
final messages = stdout
.split('\n')
.where((e) => e.startsWith('Unknown experiment'))
.join('\n');
return CompileResult(messages: messages);
}

// Update depfile digest on successful compile.
_outputDepfile.writeDigest();
}

var stderr = result.stderr as String;
// Tidy up the compiler output to leave just the failure.
stderr =
stderr
.replaceAll('Error: AOT compilation failed', '')
.replaceAll('Bad state: Generating AOT snapshot failed!', '')
.trim();
return CompileResult(messages: result.exitCode == 0 ? null : stderr);
}
}
67 changes: 48 additions & 19 deletions build_runner/lib/src/bootstrap/bootstrapper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
// 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:io';

import 'package:built_collection/built_collection.dart';
import 'package:io/io.dart';

import '../exceptions.dart';
import '../internal.dart';
import 'aot_compiler.dart';
import 'compiler.dart';
import 'depfile.dart';
import 'kernel_compiler.dart';
import 'processes.dart';
Expand All @@ -24,9 +27,14 @@ import 'processes.dart';
/// contents of all these files so they can be checked for freshness later.
///
/// The entrypoint script is launched using [ParentProcess.runAndSend]
/// which passes initial state to it and received updated state when it exits.
/// or [ParentProcess.runAotSnapshotAndSend] which passes initial state to it
/// and receives updated state when it exits.
class Bootstrapper {
final KernelCompiler _compiler = KernelCompiler();
final bool compileAot;
final Compiler _compiler;

Bootstrapper({required this.compileAot})
: _compiler = compileAot ? AotCompiler() : KernelCompiler();

/// Generates the entrypoint script, compiles it and runs it with [arguments].
///
Expand All @@ -52,24 +60,46 @@ class Bootstrapper {
if (!_compiler.checkFreshness(digestsAreFresh: false).outputIsFresh) {
final result = await _compiler.compile(experiments: experiments);
if (!result.succeeded) {
if (result.messages != null) {
final bool failedDueToMirrors;
if (result.messages == null) {
failedDueToMirrors = false;
} else {
buildLog.error(result.messages!);
failedDueToMirrors =
compileAot && result.messages!.contains('dart:mirrors');
}
if (failedDueToMirrors) {
// TODO(davidmorgan): when build_runner manages use of AOT compile
// this will be an automatic fallback to JIT instead of a message.
buildLog.error(
'Failed to compile build script. A configured builder '
'uses `dart:mirrors` and cannot be compiled AOT. Try again '
'without --force-aot to use a JIT compile.',
);
} else {
buildLog.error(
'Failed to compile build script. '
'Check builder definitions and generated script '
'$entrypointScriptPath.',
);
}
buildLog.error(
'Failed to compile build script. '
'Check builder definitions and generated script '
'$entrypointScriptPath.',
);
throw const CannotBuildException();
}
}

final result = await ParentProcess.runAndSend(
script: entrypointDillPath,
arguments: arguments,
message: buildProcessState.serialize(),
jitVmArgs: jitVmArgs,
);
final result =
compileAot
? await ParentProcess.runAotSnapshotAndSend(
aotSnapshot: entrypointAotPath,
arguments: arguments,
message: buildProcessState.serialize(),
)
: await ParentProcess.runAndSend(
script: entrypointDillPath,
arguments: arguments,
message: buildProcessState.serialize(),
jitVmArgs: jitVmArgs,
);
buildProcessState.deserializeAndSet(result.message);
final exitCode = result.exitCode;

Expand All @@ -94,11 +124,11 @@ class Bootstrapper {
}
}

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

/// Whether [path] is a dependency of the entrypoint script compiled to
/// kernel.
bool isKernelDependency(String path) {
/// Whether [path] is a dependency of the compiled entrypoint script.
bool isCompileDependency(String path) {
if (!ChildProcess.isRunning) {
// Any real use or realistic test has a child process; so this is only hit
// in small tests. Return "not a dependency" so nothing related to
Expand Down
9 changes: 9 additions & 0 deletions build_runner/lib/src/bootstrap/build_process_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ class BuildProcessState {
set elapsedMillis(int elapsedMillis) =>
_state['elapsedMillis'] = elapsedMillis;

/// The package config URI.
String get packageConfigUri =>
(_state['packageConfigUri'] ??= Isolate.packageConfigSync!.toString())
as String;

void resetForTests() {
_state.clear();
}
Expand All @@ -55,6 +60,10 @@ class BuildProcessState {
}

String serialize() {
// Set any unset values that should be set by the parent process.
stdio;
packageConfigUri;

for (final beforeSend in _beforeSends) {
beforeSend();
}
Expand Down
34 changes: 34 additions & 0 deletions build_runner/lib/src/bootstrap/compiler.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) 2025, 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 'depfile.dart';

/// Compiles the build script.
abstract class Compiler {
/// Checks freshness of the build script compile output.
///
/// Set [digestsAreFresh] if digests were very recently updated. Then, they
/// will be re-used from disk if possible instead of recomputed.
FreshnessResult checkFreshness({required bool digestsAreFresh});

/// Checks whether [path] in a dependency of the build script compile.
///
/// Call [checkFreshness] first to load the depfile.
bool isDependency(String path);

/// Compiles the entrypoint script.
Future<CompileResult> compile({Iterable<String>? experiments});
}

class CompileResult {
final String? messages;

CompileResult({required this.messages});

bool get succeeded => messages == null;

@override
String toString() =>
'CompileResult(succeeded: $succeeded, messages: $messages)';
}
29 changes: 6 additions & 23 deletions build_runner/lib/src/bootstrap/kernel_compiler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import 'dart:io';

import '../constants.dart';
import 'compiler.dart';
import 'depfile.dart';
import 'processes.dart';

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

/// Compiles the build script to kernel.
class KernelCompiler {
class KernelCompiler implements Compiler {
final Depfile _outputDepfile = Depfile(
outputPath: entrypointDillPath,
depfilePath: entrypointDillDepfilePath,
digestPath: entrypointDillDigestPath,
);

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

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

/// Compiles the entrypoint script to kernel.
@override
Future<CompileResult> compile({Iterable<String>? experiments}) async {
final dart = Platform.resolvedExecutable;
final result = await ParentProcess.run(dart, [
Expand All @@ -58,8 +54,7 @@ class KernelCompiler {
final messages = stdout
.split('\n')
.where((e) => e.startsWith('Unknown experiment'))
.map((l) => '$l\n')
.join('');
.join('\n');
return CompileResult(messages: messages);
}

Expand All @@ -74,15 +69,3 @@ class KernelCompiler {
return CompileResult(messages: result.exitCode == 0 ? null : stderr);
}
}

class CompileResult {
final String? messages;

CompileResult({required this.messages});

bool get succeeded => messages == null;

@override
String toString() =>
'CompileResult(succeeded: $succeeded, messages: $messages)';
}
Loading