From b09b37c0080eb333bc31b41f0fba7a41e790c459 Mon Sep 17 00:00:00 2001 From: David Morgan Date: Thu, 16 Oct 2025 15:14:41 +0200 Subject: [PATCH 1/2] Compile AOT. --- build_runner/CHANGELOG.md | 12 +- .../lib/src/bootstrap/aot_compiler.dart | 75 ++++++++ .../lib/src/bootstrap/bootstrapper.dart | 67 +++++-- .../src/bootstrap/build_process_state.dart | 10 + build_runner/lib/src/bootstrap/compiler.dart | 34 ++++ .../lib/src/bootstrap/kernel_compiler.dart | 26 +-- build_runner/lib/src/bootstrap/processes.dart | 51 +++-- build_runner/lib/src/build/build_series.dart | 4 +- .../lib/src/build/resolver/resolver.dart | 11 +- .../lib/src/build/resolver/sdk_summary.dart | 11 +- .../lib/src/build_plan/build_options.dart | 22 ++- .../lib/src/build_plan/build_plan.dart | 6 +- build_runner/lib/src/build_runner.dart | 7 +- .../lib/src/build_runner_command_line.dart | 18 ++ build_runner/lib/src/logging/build_log.dart | 3 + .../integration_tests/aot_compiler_test.dart | 175 ++++++++++++++++++ .../build_command_aot_test.dart | 44 +++++ .../logging/build_log_console_mode_test.dart | 2 +- .../logging/build_log_line_mode_test.dart | 2 +- 19 files changed, 506 insertions(+), 74 deletions(-) create mode 100644 build_runner/lib/src/bootstrap/aot_compiler.dart create mode 100644 build_runner/lib/src/bootstrap/compiler.dart create mode 100644 build_runner/test/integration_tests/aot_compiler_test.dart create mode 100644 build_runner/test/integration_tests/build_command_aot_test.dart diff --git a/build_runner/CHANGELOG.md b/build_runner/CHANGELOG.md index daa853b0dd..85658230d1 100644 --- a/build_runner/CHANGELOG.md +++ b/build_runner/CHANGELOG.md @@ -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 diff --git a/build_runner/lib/src/bootstrap/aot_compiler.dart b/build_runner/lib/src/bootstrap/aot_compiler.dart new file mode 100644 index 0000000000..297d632755 --- /dev/null +++ b/build_runner/lib/src/bootstrap/aot_compiler.dart @@ -0,0 +1,75 @@ +// 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 compile({Iterable? 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')) + .map((l) => '$l\n') + .join(''); + 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); + } +} diff --git a/build_runner/lib/src/bootstrap/bootstrapper.dart b/build_runner/lib/src/bootstrap/bootstrapper.dart index 431ac6fc5f..ef37b774ef 100644 --- a/build_runner/lib/src/bootstrap/bootstrapper.dart +++ b/build_runner/lib/src/bootstrap/bootstrapper.dart @@ -2,6 +2,7 @@ // 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'; @@ -9,6 +10,8 @@ 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'; @@ -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]. /// @@ -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; @@ -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 checkKernelFreshness({ + Future checkCompileFreshness({ required bool digestsAreFresh, }) async { if (!ChildProcess.isRunning) { @@ -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 diff --git a/build_runner/lib/src/bootstrap/build_process_state.dart b/build_runner/lib/src/bootstrap/build_process_state.dart index 8b734e8f9f..65d50edd0c 100644 --- a/build_runner/lib/src/bootstrap/build_process_state.dart +++ b/build_runner/lib/src/bootstrap/build_process_state.dart @@ -45,6 +45,12 @@ class BuildProcessState { set elapsedMillis(int elapsedMillis) => _state['elapsedMillis'] = elapsedMillis; + /// The package config URI. + String get packageConfigUri { + _state['packageConfigUri'] ??= Isolate.packageConfigSync!.toString(); + return _state['packageConfigUri'] as String; + } + void resetForTests() { _state.clear(); } @@ -55,6 +61,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(); } diff --git a/build_runner/lib/src/bootstrap/compiler.dart b/build_runner/lib/src/bootstrap/compiler.dart new file mode 100644 index 0000000000..e27296c490 --- /dev/null +++ b/build_runner/lib/src/bootstrap/compiler.dart @@ -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 compile({Iterable? experiments}); +} + +class CompileResult { + final String? messages; + + CompileResult({required this.messages}); + + bool get succeeded => messages == null; + + @override + String toString() => + 'CompileResult(succeeded: $succeeded, messages: $messages)'; +} diff --git a/build_runner/lib/src/bootstrap/kernel_compiler.dart b/build_runner/lib/src/bootstrap/kernel_compiler.dart index 414905a310..be12a13b44 100644 --- a/build_runner/lib/src/bootstrap/kernel_compiler.dart +++ b/build_runner/lib/src/bootstrap/kernel_compiler.dart @@ -5,6 +5,7 @@ import 'dart:io'; import '../constants.dart'; +import 'compiler.dart'; import 'depfile.dart'; import 'processes.dart'; @@ -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 compile({Iterable? experiments}) async { final dart = Platform.resolvedExecutable; final result = await ParentProcess.run(dart, [ @@ -74,15 +70,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)'; -} diff --git a/build_runner/lib/src/bootstrap/processes.dart b/build_runner/lib/src/bootstrap/processes.dart index 6fac55c0a0..4a55de5601 100644 --- a/build_runner/lib/src/bootstrap/processes.dart +++ b/build_runner/lib/src/bootstrap/processes.dart @@ -7,6 +7,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; import '../build_plan/builder_factories.dart'; import '../build_runner.dart'; @@ -20,9 +21,10 @@ class ParentProcess { /// Runs Dart [script] with [arguments], sends it [message], listens for and /// returns the response. /// - /// When the underlying script is run with `dart run`, the [jitVmArgs] are - /// forwarded to the Dart VM. This can be used to e.g. start the VM with - /// debugging options. + /// [script] can be a kernel file or Dart source. + /// + /// The [jitVmArgs] are forwarded to the Dart VM. This can be used to e.g. + /// start the VM with debugging options. /// /// The child process should use [ChildProcess] to communicate with the /// parent. @@ -32,12 +34,39 @@ class ParentProcess { required String message, required Iterable jitVmArgs, }) async { - final process = await _startWithReaper(Platform.resolvedExecutable, [ - 'run', - ...jitVmArgs, - script, - ...arguments, - ]); + return await _runAndSend( + executable: Platform.resolvedExecutable, + arguments: ['run', ...jitVmArgs, script, ...arguments], + message: message, + ); + } + + /// Runs Dart [aotSnapshot] with [arguments], sends it [message], listens for + /// and returns the response. + /// + /// The child process should use [ChildProcess] to communicate with the + /// parent. + static Future runAotSnapshotAndSend({ + required String aotSnapshot, + required Iterable arguments, + required String message, + }) async { + return await _runAndSend( + executable: p.join( + p.dirname(Platform.resolvedExecutable), + 'dartaotruntime', + ), + arguments: [aotSnapshot, ...arguments], + message: message, + ); + } + + static Future _runAndSend({ + required String executable, + required List arguments, + required String message, + }) async { + final process = await _startWithReaper(executable, arguments); // Copy output from the child stdout and stderr to current stdout and // stderr. The child response is sent on stdout after `_sentinal`, so watch @@ -155,8 +184,8 @@ class RunAndSendResult { RunAndSendResult({required this.exitCode, required this.message}); } -/// Methods for child processes launched with [ParentProcess.runAndSend] to -/// communicate with the parent. +/// Methods for child processes launched with [ParentProcess.runAndSend] +/// or [ParentProcess.runAotSnapshotAndSend] to communicate with the parent. class ChildProcess { static bool _isRunning = false; diff --git a/build_runner/lib/src/build/build_series.dart b/build_runner/lib/src/build/build_series.dart index e6755d2737..60d2ac1cca 100644 --- a/build_runner/lib/src/build/build_series.dart +++ b/build_runner/lib/src/build/build_series.dart @@ -109,7 +109,7 @@ class BuildSeries { final id = change.id; // Changes to the entrypoint are handled via depfiles. - if (_buildPlan.bootstrapper.isKernelDependency( + if (_buildPlan.bootstrapper.isCompileDependency( _buildPlan.packageGraph.pathFor(id), )) { result.add(change); @@ -229,7 +229,7 @@ class BuildSeries { } } else { final kernelFreshness = await _buildPlan.bootstrapper - .checkKernelFreshness(digestsAreFresh: false); + .checkCompileFreshness(digestsAreFresh: false); if (!kernelFreshness.outputIsFresh) { final result = BuildResult.buildScriptChanged(); _buildResultsController.add(result); diff --git a/build_runner/lib/src/build/resolver/resolver.dart b/build_runner/lib/src/build/resolver/resolver.dart index bcc4d70677..bfcc9e22ca 100644 --- a/build_runner/lib/src/build/resolver/resolver.dart +++ b/build_runner/lib/src/build/resolver/resolver.dart @@ -19,9 +19,9 @@ import 'package:build/build.dart'; import 'package:build/experiments.dart'; import 'package:collection/collection.dart' show IterableExtension; import 'package:package_config/package_config.dart'; -import 'package:path/path.dart' as p; import 'package:pool/pool.dart'; +import '../../bootstrap/build_process_state.dart'; import '../../logging/build_log.dart'; import '../../logging/timed_activities.dart'; import 'analysis_driver.dart'; @@ -508,7 +508,7 @@ class AnalyzerResolvers implements Resolvers { _warnOnLanguageVersionMismatch(); final loadedConfig = _packageConfig ??= await loadPackageConfigUri( - (await Isolate.packageConfig)!, + Uri.parse(buildProcessState.packageConfigUri), ); final driver = await analysisDriver( _analysisDriverModel, @@ -586,13 +586,6 @@ current version by running `pub deps`. ); } -Future packagePath(String package) async { - final libRoot = await Isolate.resolvePackageUri( - Uri.parse('package:$package/'), - ); - return p.dirname(p.fromUri(libRoot)); -} - /// Wraps [pool] so resource use is timed as [TimedActivity.analyze]. class AnalyzeActivityPool { final Pool pool; diff --git a/build_runner/lib/src/build/resolver/sdk_summary.dart b/build_runner/lib/src/build/resolver/sdk_summary.dart index 6ef5958017..89beacc885 100644 --- a/build_runner/lib/src/build/resolver/sdk_summary.dart +++ b/build_runner/lib/src/build/resolver/sdk_summary.dart @@ -7,10 +7,11 @@ import 'dart:io'; import 'package:analyzer/dart/sdk/build_sdk_summary.dart'; import 'package:analyzer/file_system/physical_file_system.dart'; +import 'package:package_config/package_config.dart'; import 'package:path/path.dart' as p; +import '../../bootstrap/build_process_state.dart'; import '../../logging/timed_activities.dart'; -import 'resolver.dart' show packagePath; /// `true` if the currently running dart was provided by the Flutter SDK. final isFlutter = @@ -46,6 +47,14 @@ Future defaultSdkSummaryGenerator() async { final depsFile = File('$summaryPath.deps'); final summaryFile = File(summaryPath); + final packageConfig = await loadPackageConfigUri( + Uri.parse(buildProcessState.packageConfigUri), + ); + Future packagePath(String package) async { + final libRoot = packageConfig.resolve(Uri.parse('package:$package/')); + return p.dirname(p.fromUri(libRoot)); + } + final currentDeps = { 'sdk': Platform.version, for (final package in _packageDepsToCheck) diff --git a/build_runner/lib/src/build_plan/build_options.dart b/build_runner/lib/src/build_plan/build_options.dart index acf8b60747..d404108c5e 100644 --- a/build_runner/lib/src/build_plan/build_options.dart +++ b/build_runner/lib/src/build_plan/build_options.dart @@ -23,6 +23,8 @@ class BuildOptions { final String? configKey; final BuiltList enableExperiments; final bool enableLowResourcesMode; + final bool forceAot; + final bool forceJit; final bool isReleaseBuild; final String? logPerformanceDir; final bool outputSymlinksOnly; @@ -40,6 +42,8 @@ class BuildOptions { required this.configKey, required this.enableExperiments, required this.enableLowResourcesMode, + required this.forceAot, + required this.forceJit, required this.isReleaseBuild, required this.logPerformanceDir, required this.outputSymlinksOnly, @@ -53,6 +57,8 @@ class BuildOptions { /// command line arg parsing configuration. @visibleForTesting factory BuildOptions.forTests({ + bool? forceAot, + bool? forceJit, BuiltMap>? builderConfigOverrides, BuiltSet? buildDirs, BuiltSet? buildFilters, @@ -71,6 +77,8 @@ class BuildOptions { configKey: configKey, enableExperiments: enableExperiments ?? BuiltList(), enableLowResourcesMode: enableLowResourcesMode ?? false, + forceAot: forceAot ?? false, + forceJit: forceJit ?? false, isReleaseBuild: isReleaseBuild ?? false, logPerformanceDir: logPerformanceDir, outputSymlinksOnly: outputSymlinksOnly ?? false, @@ -91,7 +99,7 @@ class BuildOptions { required String rootPackage, required bool restIsBuildDirs, }) { - return BuildOptions( + final result = BuildOptions( buildDirs: { ..._parseBuildDirs(commandLine), @@ -105,6 +113,8 @@ class BuildOptions { configKey: commandLine.config, enableExperiments: commandLine.enableExperiments!, enableLowResourcesMode: commandLine.lowResourcesMode!, + forceAot: commandLine.forceAot!, + forceJit: commandLine.forceJit!, isReleaseBuild: commandLine.release!, logPerformanceDir: _parseLogPerformance(commandLine), outputSymlinksOnly: commandLine.symlink!, @@ -112,6 +122,14 @@ class BuildOptions { commandLine.trackPerformance! || commandLine.logPerformance != null, verbose: commandLine.verbose!, ); + + if (result.forceAot && result.forceJit) { + throw UsageException( + 'Only one compile mode can be used, got --force-aot and --force-jit.', + commandLine.usage, + ); + } + return result; } BuildOptions copyWith({ @@ -124,6 +142,8 @@ class BuildOptions { configKey: configKey, enableExperiments: enableExperiments, enableLowResourcesMode: enableLowResourcesMode, + forceAot: forceAot, + forceJit: forceJit, isReleaseBuild: isReleaseBuild, logPerformanceDir: logPerformanceDir, outputSymlinksOnly: outputSymlinksOnly, diff --git a/build_runner/lib/src/build_plan/build_plan.dart b/build_runner/lib/src/build_plan/build_plan.dart index b7f329cfd9..09e6ad2e7b 100644 --- a/build_runner/lib/src/build_plan/build_plan.dart +++ b/build_runner/lib/src/build_plan/build_plan.dart @@ -105,9 +105,11 @@ class BuildPlan { required TestingOverrides testingOverrides, bool recentlyBootstrapped = true, }) async { - final bootstrapper = Bootstrapper(); + final bootstrapper = Bootstrapper( + compileAot: buildOptions.forceAot ? true : false, + ); var restartIsNeeded = false; - final kernelFreshness = await bootstrapper.checkKernelFreshness( + final kernelFreshness = await bootstrapper.checkCompileFreshness( digestsAreFresh: recentlyBootstrapped, ); if (!kernelFreshness.outputIsFresh) { diff --git a/build_runner/lib/src/build_runner.dart b/build_runner/lib/src/build_runner.dart index eb3f0d7e74..c9eed6141a 100644 --- a/build_runner/lib/src/build_runner.dart +++ b/build_runner/lib/src/build_runner.dart @@ -74,7 +74,7 @@ class BuildRunner { as String; if (commandLine.type.requiresBuilders && builderFactories == null) { - return await _runWithBuilders(); + return await _runWithBuilders(compileAot: commandLine.forceAot!); } BuildRunnerCommand command; @@ -155,12 +155,13 @@ class BuildRunner { /// /// The nested `build_runner` invocation reaches [run] with [builderFactories] /// set, so it runs the command instead of bootstrapping. - Future _runWithBuilders() async { + Future _runWithBuilders({required bool compileAot}) async { buildLog.configuration = buildLog.configuration.rebuild((b) { b.mode = commandLine.type.buildLogMode; }); - return await Bootstrapper().run( + final bootstrapper = Bootstrapper(compileAot: compileAot); + return await bootstrapper.run( arguments, jitVmArgs: commandLine.jitVmArgs ?? const Iterable.empty(), experiments: commandLine.enableExperiments, diff --git a/build_runner/lib/src/build_runner_command_line.dart b/build_runner/lib/src/build_runner_command_line.dart index cd95c4adc8..66ef712cc7 100644 --- a/build_runner/lib/src/build_runner_command_line.dart +++ b/build_runner/lib/src/build_runner_command_line.dart @@ -45,6 +45,8 @@ class BuildRunnerCommandLine { final String? config; final BuiltList? defines; final BuiltList? enableExperiments; + final bool? forceAot; + final bool? forceJit; final BuiltList? jitVmArgs; final String? hostname; final bool? liveReload; @@ -68,6 +70,8 @@ class BuildRunnerCommandLine { config = argResults.stringNamed(configOption), defines = argResults.listNamed(defineOption), enableExperiments = argResults.listNamed(enableExperimentOption), + forceAot = argResults.boolNamed(forceAotOption), + forceJit = argResults.boolNamed(forceJitOption), jitVmArgs = argResults.listNamed(dartJitVmArgOption), hostname = argResults.stringNamed(hostnameOption), liveReload = argResults.boolNamed(liveReloadOption), @@ -123,6 +127,8 @@ const configOption = 'config'; const defineOption = 'define'; const deleteFilesByDefaultOption = 'delete-conflicting-outputs'; const enableExperimentOption = 'enable-experiment'; +const forceAotOption = 'force-aot'; +const forceJitOption = 'force-jit'; const dartJitVmArgOption = 'dart-jit-vm-arg'; const hostnameOption = 'hostname'; const liveReloadOption = 'live-reload'; @@ -198,6 +204,18 @@ class _Build extends Command { defaultsTo: true, hide: true, ) + ..addFlag( + forceAotOption, + defaultsTo: false, + negatable: false, + help: 'Compiles builders with AOT mode for faster builds.', + ) + ..addFlag( + forceJitOption, + defaultsTo: false, + negatable: false, + help: 'Compiles builders with JIT mode.', + ) ..addFlag( trackPerformanceOption, help: r'Enables performance tracking and the /$perf page.', diff --git a/build_runner/lib/src/logging/build_log.dart b/build_runner/lib/src/logging/build_log.dart index 94537be092..2336ff54a2 100644 --- a/build_runner/lib/src/logging/build_log.dart +++ b/build_runner/lib/src/logging/build_log.dart @@ -2,10 +2,12 @@ // 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 'dart:math'; import 'package:build/build.dart' show AssetId; import 'package:built_collection/built_collection.dart'; +import 'package:path/path.dart' as p; import '../bootstrap/build_process_state.dart'; import '../build_plan/phase.dart'; @@ -352,6 +354,7 @@ class BuildLog { final displayingBlocks = _display.displayingBlocks; _status = [ result ? successPattern : failurePattern, + p.basename(Platform.resolvedExecutable) == 'dart' ? '/jit' : '/aot', ' in ', renderDuration(_processDuration), if (_messages.hasWarnings) ' with warnings', diff --git a/build_runner/test/integration_tests/aot_compiler_test.dart b/build_runner/test/integration_tests/aot_compiler_test.dart new file mode 100644 index 0000000000..9fc594b770 --- /dev/null +++ b/build_runner/test/integration_tests/aot_compiler_test.dart @@ -0,0 +1,175 @@ +// 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. + +@Tags(['integration3']) +library; + +import 'dart:io'; + +import 'package:build_runner/src/bootstrap/aot_compiler.dart'; +import 'package:build_runner/src/constants.dart'; +import 'package:test/test.dart'; + +import '../common/common.dart'; + +void main() async { + test('aot compiler', () async { + final pubspecs = await Pubspecs.load(); + final tester = BuildRunnerTester(pubspecs); + + tester.writePackage( + name: 'root_pkg', + dependencies: ['build_runner'], + files: { + 'bin/compile.dart': r''' +import 'dart:io'; +import 'package:build_runner/src/bootstrap/aot_compiler.dart'; +void main() async { + final compiler = AotCompiler(); + if (compiler.checkFreshness(digestsAreFresh: false).outputIsFresh) { + stdout.write('fresh\n'); + } else { + stdout.write('compiling\n'); + final result = await compiler.compile(); + stdout.write('$result\n'); + } +} +''', + }, + ); + + // `stdout.write` is used instead of print to avoid introducing a line + // ending difference on Windows. + + tester.write('root_pkg/$entrypointScriptPath', r''' +import 'dart:io'; +void main() { + stdout.write('build.dart main\n'); +} +'''); + var output = await tester.run('root_pkg', 'dart run root_pkg:compile'); + expect(output, contains('CompileResult(succeeded: true, messages: null)')); + output = await tester.run('root_pkg', 'dartaotruntime $entrypointAotPath'); + expect(output, contains('build.dart main')); + + // No changes, no rebuild. + output = await tester.run('root_pkg', 'dart run root_pkg:compile'); + expect(output, contains('fresh')); + + // Rebuilds if output was removed. + tester.delete('root_pkg/$entrypointAotPath'); + output = await tester.run('root_pkg', 'dart run root_pkg:compile'); + expect(output, contains('succeeded: true')); + + // No changes, no rebuild. + output = await tester.run('root_pkg', 'dart run root_pkg:compile'); + expect(output, contains('fresh')); + + // Rebuilds if input source was changed. + tester.write('root_pkg/$entrypointScriptPath', r''' +import 'dart:io'; +void main() { + stdout.write('updated build.dart main\n'); +} +'''); + output = await tester.run('root_pkg', 'dart run root_pkg:compile'); + expect(output, contains('succeeded: true')); + output = await tester.run('root_pkg', 'dartaotruntime $entrypointAotPath'); + expect(output, contains('updated build.dart main')); + + // No changes, no rebuild. + output = await tester.run('root_pkg', 'dart run root_pkg:compile'); + expect(output, contains('fresh')); + + // Rebuilds if input pubspec was changed. + tester.write( + 'root_pkg/pubspec.yaml', + pubspecs.pubspec( + name: 'root_pkg', + dependencies: ['build', 'build_runner'], + ), + ); + output = await tester.run('root_pkg', 'dart run root_pkg:compile'); + expect(output, contains('succeeded: true')); + + // No changes, no rebuild. + output = await tester.run('root_pkg', 'dart run root_pkg:compile'); + expect(output, contains('fresh')); + + // Rebuild that introduces an error. + tester.write('root_pkg/$entrypointScriptPath', r''' +import 'dart:io'; +void main() { + invalid code +} +'''); + output = await tester.run('root_pkg', 'dart run root_pkg:compile'); + expect(output, contains('CompileResult(succeeded: false')); + expect(output, contains('invalid code')); + + // Same error on retry. + output = await tester.run('root_pkg', 'dart run root_pkg:compile'); + expect(output, contains('CompileResult(succeeded: false')); + expect(output, contains('invalid code')); + + // Rebuild that introduces use of mirrors. + tester.write('root_pkg/$entrypointScriptPath', r''' +import 'dart:mirrors'; +void main() { +} +'''); + output = await tester.run('root_pkg', 'dart run root_pkg:compile'); + expect(output, contains('CompileResult(succeeded: false')); + expect(output, contains('dart:mirrors')); + + // Fix the error. + tester.write('root_pkg/$entrypointScriptPath', r''' +import 'dart:io'; +void main() { + stdout.write('build.dart main #3\n'); +} +'''); + output = await tester.run('root_pkg', 'dart run root_pkg:compile'); + expect(output, contains('succeeded: true')); + output = await tester.run('root_pkg', 'dartaotruntime $entrypointAotPath'); + expect(output, contains('build.dart main #3')); + + // Depfiles are handled correctly if spaces are in the names. + tester.write('root_pkg/$entrypointScriptPath', r''' +import 'dart:io'; +import 'other file.dart'; +void main() { + stdout.write('build.dart main #3\n'); +} +'''); + tester.write('root_pkg/.dart_tool/build/entrypoint/other file.dart', ''); + output = await tester.run('root_pkg', 'dart run root_pkg:compile'); + expect(output, contains('succeeded: true')); + + // No changes, no rebuild. + output = await tester.run('root_pkg', 'dart run root_pkg:compile'); + expect(output, contains('fresh')); + + // Rebuild on change to imported file. + tester.write( + 'root_pkg/.dart_tool/build/entrypoint/other file.dart', + '// updated', + ); + output = await tester.run('root_pkg', 'dart run root_pkg:compile'); + expect(output, contains('succeeded: true')); + + // Without `dart` on the path. + tester.write( + 'root_pkg/.dart_tool/build/entrypoint/other file.dart', + '// updated again', + ); + final dart = Platform.resolvedExecutable; + output = await tester.run( + 'root_pkg', + '$dart run root_pkg:compile', + environment: {'PATH': ''}, + ); + expect(output, contains('succeeded: true')); + }); +} diff --git a/build_runner/test/integration_tests/build_command_aot_test.dart b/build_runner/test/integration_tests/build_command_aot_test.dart new file mode 100644 index 0000000000..20568d6f4b --- /dev/null +++ b/build_runner/test/integration_tests/build_command_aot_test.dart @@ -0,0 +1,44 @@ +// 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. + +@Tags(['integration2']) +library; + +import 'package:test/test.dart'; + +import '../common/common.dart'; + +void main() async { + test('build command AOT', () async { + final pubspecs = await Pubspecs.load(); + final tester = BuildRunnerTester(pubspecs); + + tester.writeFixturePackage(FixturePackages.copyBuilder()); + tester.writePackage( + name: 'root_pkg', + dependencies: ['build_runner'], + pathDependencies: ['builder_pkg'], + files: {'web/a.txt': 'a'}, + ); + + // First build. + await tester.run('root_pkg', 'dart run build_runner build --force-aot'); + expect(tester.read('root_pkg/web/a.txt.copy'), 'a'); + + // With no changes, no rebuild. + var output = await tester.run( + 'root_pkg', + 'dart run build_runner build --force-aot', + ); + expect(output, contains('wrote 0 outputs')); + + // Change the build script, rebuilds. + tester.update('builder_pkg/lib/builder.dart', (script) => '$script\n'); + output = await tester.run( + 'root_pkg', + 'dart run build_runner build --force-aot', + ); + expect(output, contains('wrote 1 output')); + }); +} diff --git a/build_runner/test/logging/build_log_console_mode_test.dart b/build_runner/test/logging/build_log_console_mode_test.dart index f790731c5e..8582266b68 100644 --- a/build_runner/test/logging/build_log_console_mode_test.dart +++ b/build_runner/test/logging/build_log_console_mode_test.dart @@ -300,7 +300,7 @@ E An error.'''), lib/l0.dart builder1 Some info. -Built with build_runner in 0s with warnings; wrote 2 outputs. +Built with build_runner/jit in 0s with warnings; wrote 2 outputs. lib/l0.dart builder2 W A warning. diff --git a/build_runner/test/logging/build_log_line_mode_test.dart b/build_runner/test/logging/build_log_line_mode_test.dart index 060a6e9e19..be4f0adb64 100644 --- a/build_runner/test/logging/build_log_line_mode_test.dart +++ b/build_runner/test/logging/build_log_line_mode_test.dart @@ -267,7 +267,7 @@ void main() { 'E builder2 on lib/l0.dart:\n' 'An error.', ' 0s builder2 on 1 input: 1 output', - ' Built with build_runner in 0s; wrote 2 outputs.', + ' Built with build_runner/jit in 0s; wrote 2 outputs.', ]); }); }); From 18255d4942c57f2954c5ba6f7710263e14296ff7 Mon Sep 17 00:00:00 2001 From: David Morgan Date: Mon, 20 Oct 2025 13:08:13 +0200 Subject: [PATCH 2/2] Address review comments. --- build_runner/lib/src/bootstrap/aot_compiler.dart | 3 +-- build_runner/lib/src/bootstrap/build_process_state.dart | 7 +++---- build_runner/lib/src/bootstrap/kernel_compiler.dart | 3 +-- build_runner/lib/src/build_plan/build_options.dart | 3 ++- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/build_runner/lib/src/bootstrap/aot_compiler.dart b/build_runner/lib/src/bootstrap/aot_compiler.dart index 297d632755..3dd0459db9 100644 --- a/build_runner/lib/src/bootstrap/aot_compiler.dart +++ b/build_runner/lib/src/bootstrap/aot_compiler.dart @@ -54,8 +54,7 @@ class AotCompiler implements Compiler { final messages = stdout .split('\n') .where((e) => e.startsWith('Unknown experiment')) - .map((l) => '$l\n') - .join(''); + .join('\n'); return CompileResult(messages: messages); } diff --git a/build_runner/lib/src/bootstrap/build_process_state.dart b/build_runner/lib/src/bootstrap/build_process_state.dart index 65d50edd0c..d7e0ec9b37 100644 --- a/build_runner/lib/src/bootstrap/build_process_state.dart +++ b/build_runner/lib/src/bootstrap/build_process_state.dart @@ -46,10 +46,9 @@ class BuildProcessState { _state['elapsedMillis'] = elapsedMillis; /// The package config URI. - String get packageConfigUri { - _state['packageConfigUri'] ??= Isolate.packageConfigSync!.toString(); - return _state['packageConfigUri'] as String; - } + String get packageConfigUri => + (_state['packageConfigUri'] ??= Isolate.packageConfigSync!.toString()) + as String; void resetForTests() { _state.clear(); diff --git a/build_runner/lib/src/bootstrap/kernel_compiler.dart b/build_runner/lib/src/bootstrap/kernel_compiler.dart index be12a13b44..1dbade1c07 100644 --- a/build_runner/lib/src/bootstrap/kernel_compiler.dart +++ b/build_runner/lib/src/bootstrap/kernel_compiler.dart @@ -54,8 +54,7 @@ class KernelCompiler implements Compiler { final messages = stdout .split('\n') .where((e) => e.startsWith('Unknown experiment')) - .map((l) => '$l\n') - .join(''); + .join('\n'); return CompileResult(messages: messages); } diff --git a/build_runner/lib/src/build_plan/build_options.dart b/build_runner/lib/src/build_plan/build_options.dart index d404108c5e..e24610c6a5 100644 --- a/build_runner/lib/src/build_plan/build_options.dart +++ b/build_runner/lib/src/build_plan/build_options.dart @@ -125,7 +125,8 @@ class BuildOptions { if (result.forceAot && result.forceJit) { throw UsageException( - 'Only one compile mode can be used, got --force-aot and --force-jit.', + 'Only one compile mode can be used, ' + 'got --$forceAotOption and --$forceJitOption.', commandLine.usage, ); }