Skip to content
Draft
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
18 changes: 9 additions & 9 deletions _test/test/goldens/generated_build_script.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@
// ignore_for_file: directives_ordering
// build_runner >=2.4.16
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:build_runner_core/build_runner_core.dart' as _i1;
import 'package:provides_builder/builders.dart' as _i2;
import 'package:build_web_compilers/builders.dart' as _i3;
import 'package:build_test/builder.dart' as _i4;
import 'dart:io' as _i11;
import 'dart:isolate' as _i8;

import 'package:build/build.dart' as _i7;
import 'package:build_config/build_config.dart' as _i5;
import 'package:build_modules/builders.dart' as _i6;
import 'package:build/build.dart' as _i7;
import 'dart:isolate' as _i8;
import 'package:build_runner/src/build_script_generate/build_process_state.dart'
as _i9;
import 'package:build_runner/build_runner.dart' as _i10;
import 'dart:io' as _i11;
import 'package:build_runner/src/bootstrapper/build_process_state.dart' as _i9;
import 'package:build_runner_core/build_runner_core.dart' as _i1;
import 'package:build_test/builder.dart' as _i4;
import 'package:build_web_compilers/builders.dart' as _i3;
import 'package:provides_builder/builders.dart' as _i2;

final _builders = <_i1.BuilderApplication>[
_i1.apply(
Expand Down
2 changes: 2 additions & 0 deletions build/lib/src/library_cycle_graph/phased_reader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ abstract class PhasedReader {
/// current phase.
Future<String?> readAtPhase(AssetId id);

//

/// Whether [id] is a generated asset that changes between [phase] and
/// [comparedToPhase].
///
Expand Down
8 changes: 4 additions & 4 deletions build_runner/bin/build_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import 'dart:io';

import 'package:args/args.dart';
import 'package:args/command_runner.dart';
import 'package:build_runner/src/build_script_generate/bootstrap.dart';
import 'package:build_runner/src/build_script_generate/build_process_state.dart';
import 'package:build_runner/src/bootstrapper/bootstrapper.dart';
import 'package:build_runner/src/bootstrapper/build_process_state.dart';
import 'package:build_runner/src/entrypoint/options.dart';
import 'package:build_runner/src/entrypoint/runner.dart';
import 'package:build_runner_core/build_runner_core.dart';
Expand Down Expand Up @@ -94,7 +94,7 @@ Future<void> main(List<String> args) async {
b.mode = BuildLogMode.daemon;
});
}
while ((exitCode = await generateAndRun(args, experiments: experiments)) ==
ExitCode.tempFail.code) {}
exitCode = await Bootstrapper().run(args, experiments: experiments);
return;
}
}
138 changes: 138 additions & 0 deletions build_runner/lib/src/bootstrapper/bootstrapper.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// 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 'package:build_runner_core/build_runner_core.dart';
import 'package:io/io.dart';
import 'package:package_config/package_config.dart';

import '../build_script_generate/build_script_generate.dart';
import 'compiler.dart';
import 'depfiles.dart';
import 'runner.dart';

// DO NOT SUBMIT
// ignore_for_file: deprecated_member_use
// ignore_for_file: only_throw_errors

class Bootstrapper {
final Depfile buildRunnerDepfile = Depfile(
depfilePath: '.dart_tool/build/entrypoint/build_runner.deps',
digestPath: '.dart_tool/build/entrypoint/build_runner.digest',
output: null,
inputs: [],
);
final Depfile buildSourceDepfile = Depfile(
depfilePath: '.dart_tool/build/entrypoint/build.dart.deps',
digestPath: '.dart_tool/build/entrypoint/build.dart.digest',
output: '.dart_tool/build/entrypoint/build.dart',
inputs: [],
);
final Depfile dillDepfile = Depfile(
depfilePath: '.dart_tool/build/entrypoint/build.dart.dill.deps',
digestPath: '.dart_tool/build/entrypoint/build.dart.dill.digest',
output: '.dart_tool/build/entrypoint/build.dart.dill',
inputs: [],
);

PackageConfig? config;
bool? buildRunnerHasChanged;
bool? buildYamlHasChanged;
bool? buildDillHasChanged;

bool runningFromBuildScript() {
return StackTrace.current.toString().contains(
'.dart_tool/build/entrypoint/build.dart',
);
}

Future<bool> needsRebuild() async {
if (!runningFromBuildScript()) return false;
buildLog.debug(Platform.script.toString());
// TODO(davidmorgan): fix for workspace, error handling.
config ??= (await findPackageConfig(Directory.current, recurse: true))!;

// TODO(davidmorgan): is this the right thing to check?
buildLog.debug('needsRebuild?');
final r1 = buildRunnerDepfile.outputIsUpToDate();
final r2 = buildSourceDepfile.outputIsUpToDate();
final r3 = dillDepfile.outputIsUpToDate();
final result = !r1 || !r2 || !r3;
buildLog.debug('needsRebuild? $r1 $r2 $r3 --> $result');
return result;
}

Future<int> run(List<String> args, {List<String>? experiments}) async {
buildRunnerDepfile.output = Platform.script.path;
while (true) {
// TODO(davidmorgan): fix for workspace, error handling.
config = (await findPackageConfig(Directory.current, recurse: true))!;

_checkBuildRunner();
await _checkBuildSource(force: buildRunnerHasChanged!);
await _checkBuildDill();

final exitCode = await Runner().run(args);
if (exitCode != ExitCode.tempFail.code) {
return exitCode;
}
}
}

void _checkBuildRunner() {
buildLog.debug('Check build_runner.');
final package = config!['build_runner']!.packageUriRoot;
final script = package.resolve('../bin/build_runner.dart');
if (buildRunnerDepfile.outputIsUpToDate()) {
buildLog.debug('build runner is up to date');
buildRunnerHasChanged = false;
} else {
buildLog.debug('build runner update');
buildRunnerHasChanged = true;
buildRunnerDepfile.clear();
buildRunnerDepfile.addScriptDeps(
scriptPath: script.path,
packageConfig: config!,
);
buildRunnerDepfile.write();
}
}

Future<void> _checkBuildSource({required bool force}) async {
if (!force && buildSourceDepfile.outputIsUpToDate()) {
buildLog.debug('build script up to date');
buildYamlHasChanged = false;
} else {
buildLog.debug('build script update (force: $force)');
buildYamlHasChanged = true;
final buildScript = await generateBuildScript();
File(buildSourceDepfile.output!).writeAsStringSync(buildScript.content);
buildSourceDepfile.clear();
buildSourceDepfile.addDeps(buildScript.inputs);
buildSourceDepfile.addScriptDeps(
scriptPath: buildSourceDepfile.output!,
packageConfig: config!,
);
buildSourceDepfile.write();
}
}

Future<void> _checkBuildDill() async {
final compiler = Compiler();
if (dillDepfile.outputIsUpToDate()) {
buildLog.debug('dill up to date');
buildDillHasChanged = false;
} else {
buildLog.debug('dill update');
buildDillHasChanged = true;

final result = await compiler.compile();
if (!result.succeeded) throw 'failed';
// TODO(davidmorgan): this is weird.
dillDepfile.loadDeps();
dillDepfile.write();
}
}
}
36 changes: 36 additions & 0 deletions build_runner/lib/src/bootstrapper/compiler.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// 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 'package:build_runner_core/build_runner_core.dart';

class CompileResult {
final String? messages;

CompileResult({required this.messages});

bool get succeeded => messages == null;
}

// TODO(davidmorgan): experiments.
class Compiler {
Future<CompileResult> compile() async {
buildLog.doing('Compiling the build script.');
final result = await Process.run('dart', [
'compile',
'kernel',
'.dart_tool/build/entrypoint/build.dart',
'--output',
'.dart_tool/build/entrypoint/build.dart.dill',
'--depfile',
'.dart_tool/build/entrypoint/build.dart.dill.deps',
]);
if (result.exitCode != 0) {
print('Compile failed: ${result.stdout} ${result.stderr}');
return CompileResult(messages: result.stderr as String);
}
return CompileResult(messages: null);
}
}
135 changes: 135 additions & 0 deletions build_runner/lib/src/bootstrapper/depfiles.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// 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:convert';
import 'dart:io';

import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart';
import 'package:package_config/package_config.dart';
import 'package:path/path.dart' as p;

class Depfile {
final String depfilePath;
final String digestPath;
String? output;
List<String> inputs;

Depfile({
required this.depfilePath,
required this.digestPath,
required this.output,
required this.inputs,
});

void addDeps(Iterable<String> inputs) {
this.inputs.addAll(inputs);
}

void addScriptDeps({
required String scriptPath,
required PackageConfig packageConfig,
}) {
final seenPaths = <String>{scriptPath};
final nextPaths = [scriptPath];

while (nextPaths.isNotEmpty) {
final nextPath = nextPaths.removeLast();
final dirname = p.dirname(nextPath);

final file = File(nextPath);
final content = file.existsSync() ? file.readAsStringSync() : null;
if (content == null) continue;
final parsed =
parseString(content: content, throwIfDiagnostics: false).unit;
for (final directive in parsed.directives) {
if (directive is! UriBasedDirective) continue;
final uri = directive.uri.stringValue;
if (uri == null) continue;
final parsedUri = Uri.parse(uri);
if (parsedUri.scheme == 'dart') continue;
final path =
parsedUri.scheme == 'package'
? packageConfig.resolve(parsedUri)!.toFilePath()
: p.canonicalize(p.join(dirname, parsedUri.path));
if (seenPaths.add(path)) nextPaths.add(path);
}
}

inputs = seenPaths.toList()..sort();
}

bool outputIsUpToDate() {
final depsFile = File(depfilePath);
if (!depsFile.existsSync()) return false;
final digestFile = File(digestPath);
if (!digestFile.existsSync()) return false;
final digests = digestFile.readAsStringSync();
final expectedDigests = computeDigests();
return digests == expectedDigests;
}

void loadDeps() {
inputs = _loadDeps();
}

String _loadOutput() {
final depsFile = File(depfilePath);
final deps = depsFile.readAsStringSync();
// TODO(davidmorgan): unescape spaces.
var result = deps.split(' ').first;
// Strip off trailing ':'.
result = result.substring(0, result.length - 1);
return result;
}

List<String> _loadDeps() {
final depsFile = File(depfilePath);
final deps = depsFile.readAsStringSync();
// TODO(davidmorgan): unescape spaces.
final result = deps.split(' ').skip(1).toList();
// File ends in a newline.
result.last = result.last.substring(0, result.last.length - 1);
return result;
}

void clear() {
inputs.clear();
}

void write() {
File(depfilePath)
..createSync(recursive: true)
..writeAsStringSync(
'$output: '
// TODO(davidmorgan): escaping.
'${inputs.join(' ')}'
'\n',
);
File(digestPath).writeAsStringSync(computeDigests());
}

String computeDigests() => '''
inputs digest: ${_computeDigest(_loadDeps())}
output digest: ${_computeDigest([output ?? _loadOutput()])}
''';
//////////
String _computeDigest(Iterable<String> deps) {
final digestSink = AccumulatorSink<Digest>();
final result = md5.startChunkedConversion(digestSink);
for (final dep in deps) {
final file = File(dep);
if (file.existsSync()) {
result.add([1]);
result.add(File(dep).readAsBytesSync());
} else {
result.add([0]);
}
}
result.close();
return base64.encode(digestSink.events.first.bytes);
}
}
17 changes: 17 additions & 0 deletions build_runner/lib/src/bootstrapper/runner.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// 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';

// TODO(davidmorgan): pass state.
// TODO(davidmorgan): handle uncaught errors--in `run` method?
class Runner {
Future<int> run(List<String> arguments) async {
final process = await Process.start('dart', [
'.dart_tool/build/entrypoint/build.dart.dill',
...arguments,
], mode: ProcessStartMode.inheritStdio);
return process.exitCode;
}
}
Loading
Loading