Skip to content

Commit ec9b757

Browse files
committed
Rewrite bootstrap code not using mirrors.
1 parent bcbbcb8 commit ec9b757

File tree

29 files changed

+440
-538
lines changed

29 files changed

+440
-538
lines changed

_test/test/goldens/generated_build_script.dart

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@
22
// ignore_for_file: directives_ordering
33
// build_runner >=2.4.16
44
// ignore_for_file: no_leading_underscores_for_library_prefixes
5-
import 'package:build_runner_core/build_runner_core.dart' as _i1;
6-
import 'package:provides_builder/builders.dart' as _i2;
7-
import 'package:build_web_compilers/builders.dart' as _i3;
8-
import 'package:build_test/builder.dart' as _i4;
5+
import 'dart:io' as _i11;
6+
import 'dart:isolate' as _i8;
7+
8+
import 'package:build/build.dart' as _i7;
99
import 'package:build_config/build_config.dart' as _i5;
1010
import 'package:build_modules/builders.dart' as _i6;
11-
import 'package:build/build.dart' as _i7;
12-
import 'dart:isolate' as _i8;
13-
import 'package:build_runner/src/build_script_generate/build_process_state.dart'
14-
as _i9;
1511
import 'package:build_runner/build_runner.dart' as _i10;
16-
import 'dart:io' as _i11;
12+
import 'package:build_runner/src/bootstrapper/build_process_state.dart' as _i9;
13+
import 'package:build_runner_core/build_runner_core.dart' as _i1;
14+
import 'package:build_test/builder.dart' as _i4;
15+
import 'package:build_web_compilers/builders.dart' as _i3;
16+
import 'package:provides_builder/builders.dart' as _i2;
1717

1818
final _builders = <_i1.BuilderApplication>[
1919
_i1.apply(

build/lib/src/library_cycle_graph/phased_reader.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ abstract class PhasedReader {
3434
/// current phase.
3535
Future<String?> readAtPhase(AssetId id);
3636

37+
//
38+
3739
/// Whether [id] is a generated asset that changes between [phase] and
3840
/// [comparedToPhase].
3941
///

build_runner/bin/build_runner.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import 'dart:io';
77

88
import 'package:args/args.dart';
99
import 'package:args/command_runner.dart';
10-
import 'package:build_runner/src/build_script_generate/bootstrap.dart';
11-
import 'package:build_runner/src/build_script_generate/build_process_state.dart';
10+
import 'package:build_runner/src/bootstrapper/bootstrapper.dart';
11+
import 'package:build_runner/src/bootstrapper/build_process_state.dart';
1212
import 'package:build_runner/src/entrypoint/options.dart';
1313
import 'package:build_runner/src/entrypoint/runner.dart';
1414
import 'package:build_runner_core/build_runner_core.dart';
@@ -94,7 +94,7 @@ Future<void> main(List<String> args) async {
9494
b.mode = BuildLogMode.daemon;
9595
});
9696
}
97-
while ((exitCode = await generateAndRun(args, experiments: experiments)) ==
98-
ExitCode.tempFail.code) {}
97+
exitCode = await Bootstrapper().run(args, experiments: experiments);
98+
return;
9999
}
100100
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
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 'package:build_runner_core/build_runner_core.dart';
8+
import 'package:io/io.dart';
9+
import 'package:package_config/package_config.dart';
10+
11+
import '../build_script_generate/build_script_generate.dart';
12+
import 'compiler.dart';
13+
import 'depfiles.dart';
14+
import 'runner.dart';
15+
16+
// DO NOT SUBMIT
17+
// ignore_for_file: deprecated_member_use
18+
// ignore_for_file: only_throw_errors
19+
20+
class Bootstrapper {
21+
final Depfile buildRunnerDepfile = Depfile(
22+
depfilePath: '.dart_tool/build/entrypoint/build_runner.deps',
23+
digestPath: '.dart_tool/build/entrypoint/build_runner.digest',
24+
output: null,
25+
inputs: [],
26+
);
27+
final Depfile buildSourceDepfile = Depfile(
28+
depfilePath: '.dart_tool/build/entrypoint/build.dart.deps',
29+
digestPath: '.dart_tool/build/entrypoint/build.dart.digest',
30+
output: '.dart_tool/build/entrypoint/build.dart',
31+
inputs: [],
32+
);
33+
final Depfile dillDepfile = Depfile(
34+
depfilePath: '.dart_tool/build/entrypoint/build.dart.dill.deps',
35+
digestPath: '.dart_tool/build/entrypoint/build.dart.dill.digest',
36+
output: '.dart_tool/build/entrypoint/build.dart.dill',
37+
inputs: [],
38+
);
39+
40+
PackageConfig? config;
41+
bool? buildRunnerHasChanged;
42+
bool? buildYamlHasChanged;
43+
bool? buildDillHasChanged;
44+
45+
bool runningFromBuildScript() {
46+
return StackTrace.current.toString().contains(
47+
'.dart_tool/build/entrypoint/build.dart',
48+
);
49+
}
50+
51+
Future<bool> needsRebuild() async {
52+
if (!runningFromBuildScript()) return false;
53+
buildLog.debug(Platform.script.toString());
54+
// TODO(davidmorgan): fix for workspace, error handling.
55+
config ??= (await findPackageConfig(Directory.current, recurse: true))!;
56+
57+
// TODO(davidmorgan): is this the right thing to check?
58+
buildLog.debug('needsRebuild?');
59+
final r1 = buildRunnerDepfile.outputIsUpToDate();
60+
final r2 = buildSourceDepfile.outputIsUpToDate();
61+
final r3 = dillDepfile.outputIsUpToDate();
62+
final result = !r1 || !r2 || !r3;
63+
buildLog.debug('needsRebuild? $r1 $r2 $r3 --> $result');
64+
return result;
65+
}
66+
67+
Future<int> run(List<String> args, {List<String>? experiments}) async {
68+
buildRunnerDepfile.output = Platform.script.path;
69+
while (true) {
70+
// TODO(davidmorgan): fix for workspace, error handling.
71+
config = (await findPackageConfig(Directory.current, recurse: true))!;
72+
73+
_checkBuildRunner();
74+
await _checkBuildSource(force: buildRunnerHasChanged!);
75+
await _checkBuildDill();
76+
77+
final exitCode = await Runner().run(args);
78+
if (exitCode != ExitCode.tempFail.code) {
79+
return exitCode;
80+
}
81+
}
82+
}
83+
84+
void _checkBuildRunner() {
85+
buildLog.debug('Check build_runner.');
86+
final package = config!['build_runner']!.packageUriRoot;
87+
final script = package.resolve('../bin/build_runner.dart');
88+
if (buildRunnerDepfile.outputIsUpToDate()) {
89+
buildLog.debug('build runner is up to date');
90+
buildRunnerHasChanged = false;
91+
} else {
92+
buildLog.debug('build runner update');
93+
buildRunnerHasChanged = true;
94+
buildRunnerDepfile.clear();
95+
buildRunnerDepfile.addScriptDeps(
96+
scriptPath: script.path,
97+
packageConfig: config!,
98+
);
99+
buildRunnerDepfile.write();
100+
}
101+
}
102+
103+
Future<void> _checkBuildSource({required bool force}) async {
104+
if (!force && buildSourceDepfile.outputIsUpToDate()) {
105+
buildLog.debug('build script up to date');
106+
buildYamlHasChanged = false;
107+
} else {
108+
buildLog.debug('build script update (force: $force)');
109+
buildYamlHasChanged = true;
110+
final buildScript = await generateBuildScript();
111+
File(buildSourceDepfile.output!).writeAsStringSync(buildScript.content);
112+
buildSourceDepfile.clear();
113+
buildSourceDepfile.addDeps(buildScript.inputs);
114+
buildSourceDepfile.addScriptDeps(
115+
scriptPath: buildSourceDepfile.output!,
116+
packageConfig: config!,
117+
);
118+
buildSourceDepfile.write();
119+
}
120+
}
121+
122+
Future<void> _checkBuildDill() async {
123+
final compiler = Compiler();
124+
if (dillDepfile.outputIsUpToDate()) {
125+
buildLog.debug('dill up to date');
126+
buildDillHasChanged = false;
127+
} else {
128+
buildLog.debug('dill update');
129+
buildDillHasChanged = true;
130+
131+
final result = await compiler.compile();
132+
if (!result.succeeded) throw 'failed';
133+
// TODO(davidmorgan): this is weird.
134+
dillDepfile.loadDeps();
135+
dillDepfile.write();
136+
}
137+
}
138+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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 'package:build_runner_core/build_runner_core.dart';
8+
9+
class CompileResult {
10+
final String? messages;
11+
12+
CompileResult({required this.messages});
13+
14+
bool get succeeded => messages == null;
15+
}
16+
17+
// TODO(davidmorgan): experiments.
18+
class Compiler {
19+
Future<CompileResult> compile() async {
20+
buildLog.doing('Compiling the build script.');
21+
final result = await Process.run('dart', [
22+
'compile',
23+
'kernel',
24+
'.dart_tool/build/entrypoint/build.dart',
25+
'--output',
26+
'.dart_tool/build/entrypoint/build.dart.dill',
27+
'--depfile',
28+
'.dart_tool/build/entrypoint/build.dart.dill.deps',
29+
]);
30+
if (result.exitCode != 0) {
31+
print('Compile failed: ${result.stdout} ${result.stderr}');
32+
return CompileResult(messages: result.stderr as String);
33+
}
34+
return CompileResult(messages: null);
35+
}
36+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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:convert';
6+
import 'dart:io';
7+
8+
import 'package:analyzer/dart/analysis/utilities.dart';
9+
import 'package:analyzer/dart/ast/ast.dart';
10+
import 'package:convert/convert.dart';
11+
import 'package:crypto/crypto.dart';
12+
import 'package:package_config/package_config.dart';
13+
import 'package:path/path.dart' as p;
14+
15+
class Depfile {
16+
final String depfilePath;
17+
final String digestPath;
18+
String? output;
19+
List<String> inputs;
20+
21+
Depfile({
22+
required this.depfilePath,
23+
required this.digestPath,
24+
required this.output,
25+
required this.inputs,
26+
});
27+
28+
void addDeps(Iterable<String> inputs) {
29+
this.inputs.addAll(inputs);
30+
}
31+
32+
void addScriptDeps({
33+
required String scriptPath,
34+
required PackageConfig packageConfig,
35+
}) {
36+
final seenPaths = <String>{scriptPath};
37+
final nextPaths = [scriptPath];
38+
39+
while (nextPaths.isNotEmpty) {
40+
final nextPath = nextPaths.removeLast();
41+
final dirname = p.dirname(nextPath);
42+
43+
final file = File(nextPath);
44+
final content = file.existsSync() ? file.readAsStringSync() : null;
45+
if (content == null) continue;
46+
final parsed =
47+
parseString(content: content, throwIfDiagnostics: false).unit;
48+
for (final directive in parsed.directives) {
49+
if (directive is! UriBasedDirective) continue;
50+
final uri = directive.uri.stringValue;
51+
if (uri == null) continue;
52+
final parsedUri = Uri.parse(uri);
53+
if (parsedUri.scheme == 'dart') continue;
54+
final path =
55+
parsedUri.scheme == 'package'
56+
? packageConfig.resolve(parsedUri)!.toFilePath()
57+
: p.canonicalize(p.join(dirname, parsedUri.path));
58+
if (seenPaths.add(path)) nextPaths.add(path);
59+
}
60+
}
61+
62+
inputs = seenPaths.toList()..sort();
63+
}
64+
65+
bool outputIsUpToDate() {
66+
final depsFile = File(depfilePath);
67+
if (!depsFile.existsSync()) return false;
68+
final digestFile = File(digestPath);
69+
if (!digestFile.existsSync()) return false;
70+
final digests = digestFile.readAsStringSync();
71+
final expectedDigests = computeDigests();
72+
return digests == expectedDigests;
73+
}
74+
75+
void loadDeps() {
76+
inputs = _loadDeps();
77+
}
78+
79+
String _loadOutput() {
80+
final depsFile = File(depfilePath);
81+
final deps = depsFile.readAsStringSync();
82+
// TODO(davidmorgan): unescape spaces.
83+
var result = deps.split(' ').first;
84+
// Strip off trailing ':'.
85+
result = result.substring(0, result.length - 1);
86+
return result;
87+
}
88+
89+
List<String> _loadDeps() {
90+
final depsFile = File(depfilePath);
91+
final deps = depsFile.readAsStringSync();
92+
// TODO(davidmorgan): unescape spaces.
93+
final result = deps.split(' ').skip(1).toList();
94+
// File ends in a newline.
95+
result.last = result.last.substring(0, result.last.length - 1);
96+
return result;
97+
}
98+
99+
void clear() {
100+
inputs.clear();
101+
}
102+
103+
void write() {
104+
File(depfilePath)
105+
..createSync(recursive: true)
106+
..writeAsStringSync(
107+
'$output: '
108+
// TODO(davidmorgan): escaping.
109+
'${inputs.join(' ')}'
110+
'\n',
111+
);
112+
File(digestPath).writeAsStringSync(computeDigests());
113+
}
114+
115+
String computeDigests() => '''
116+
inputs digest: ${_computeDigest(_loadDeps())}
117+
output digest: ${_computeDigest([output ?? _loadOutput()])}
118+
''';
119+
//////////
120+
String _computeDigest(Iterable<String> deps) {
121+
final digestSink = AccumulatorSink<Digest>();
122+
final result = md5.startChunkedConversion(digestSink);
123+
for (final dep in deps) {
124+
final file = File(dep);
125+
if (file.existsSync()) {
126+
result.add([1]);
127+
result.add(File(dep).readAsBytesSync());
128+
} else {
129+
result.add([0]);
130+
}
131+
}
132+
result.close();
133+
return base64.encode(digestSink.events.first.bytes);
134+
}
135+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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+
// TODO(davidmorgan): pass state.
8+
// TODO(davidmorgan): handle uncaught errors--in `run` method?
9+
class Runner {
10+
Future<int> run(List<String> arguments) async {
11+
final process = await Process.start('dart', [
12+
'.dart_tool/build/entrypoint/build.dart.dill',
13+
...arguments,
14+
], mode: ProcessStartMode.inheritStdio);
15+
return process.exitCode;
16+
}
17+
}

0 commit comments

Comments
 (0)