Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit ddbef31

Browse files
authored
A minimal engine_tools_lib to use for local-repo Dart tooling (#45154)
Partial work towards re-landing #44936. Both the `clang_tidy` and `githooks` passage could benefit from being able to automatically find the latest `compile_commands.json` output, which means that some common code should exist in the `tools/` directory. This is a very minimal (but tested) library for doing exactly that.
1 parent 9778c2c commit ddbef31

File tree

6 files changed

+564
-0
lines changed

6 files changed

+564
-0
lines changed

testing/run_tests.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,25 @@ def gather_clang_tidy_tests(build_dir):
953953
)
954954

955955

956+
def gather_engine_repo_tools_tests(build_dir):
957+
test_dir = os.path.join(
958+
BUILDROOT_DIR, 'flutter', 'tools', 'pkg', 'engine_repo_tools'
959+
)
960+
dart_tests = glob.glob('%s/*_test.dart' % test_dir)
961+
for dart_test_file in dart_tests:
962+
opts = [
963+
'--disable-dart-dev',
964+
dart_test_file,
965+
]
966+
yield EngineExecutableTask(
967+
build_dir,
968+
os.path.join('dart-sdk', 'bin', 'dart'),
969+
None,
970+
flags=opts,
971+
cwd=test_dir
972+
)
973+
974+
956975
def gather_api_consistency_tests(build_dir):
957976
test_dir = os.path.join(BUILDROOT_DIR, 'flutter', 'tools', 'api_check')
958977
dart_tests = glob.glob('%s/test/*_test.dart' % test_dir)
@@ -1230,6 +1249,7 @@ def main():
12301249
tasks += list(gather_litetest_tests(build_dir))
12311250
tasks += list(gather_githooks_tests(build_dir))
12321251
tasks += list(gather_clang_tidy_tests(build_dir))
1252+
tasks += list(gather_engine_repo_tools_tests(build_dir))
12331253
tasks += list(gather_api_consistency_tests(build_dir))
12341254
tasks += list(gather_path_ops_tests(build_dir))
12351255
tasks += list(gather_const_finder_tests(build_dir))

tools/pkg/engine_repo_tools/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# engine_repo_tools
2+
3+
This is a repo-internal library for `flutter/engine`, that contains shared code
4+
for writing tools that operate on the engine repository. For example, finding
5+
the latest compiled engine artifacts in the `out/` directory:
6+
7+
```dart
8+
import 'package:engine_repo_tools/engine_repo_tools.dart';
9+
10+
void main() {
11+
final engine = Engine.findWithin();
12+
final latest = engine.latestOutput();
13+
if (latest != null) {
14+
print('Latest compile_commands.json: ${latest.compileCommandsJson?.path}');
15+
}
16+
}
17+
```
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
/// A minimal library for discovering and probing a local engine repository.
6+
///
7+
/// This library is intended to be used by tools that need to interact with a
8+
/// local engine repository, such as `clang_tidy` or `githooks`. For example,
9+
/// finding the `compile_commands.json` file for the most recently built output:
10+
///
11+
/// ```dart
12+
/// final Engine engine = Engine.findWithin();
13+
/// final Output? output = engine.latestOutput();
14+
/// if (output == null) {
15+
/// print('No output targets found.');
16+
/// } else {
17+
/// final io.File? compileCommandsJson = output.compileCommandsJson;
18+
/// if (compileCommandsJson == null) {
19+
/// print('No compile_commands.json file found.');
20+
/// } else {
21+
/// print('Found compile_commands.json file at ${compileCommandsJson.path}');
22+
/// }
23+
/// }
24+
/// ```
25+
library;
26+
27+
import 'dart:io' as io;
28+
29+
import 'package:path/path.dart' as p;
30+
31+
/// Represents the `$ENGINE` directory (i.e. a checked-out Flutter engine).
32+
///
33+
/// If you have a path to the `$ENGINE/src` directory, use [Engine.fromSrcPath].
34+
///
35+
/// If you have a path to a directory within the `$ENGINE/src` directory, or
36+
/// want to use the current working directory, use [Engine.findWithin].
37+
final class Engine {
38+
/// Creates an [Engine] from a path such as `/Users/.../flutter/engine/src`.
39+
///
40+
/// ```dart
41+
/// final Engine engine = Engine.findWithin('/Users/.../engine/src');
42+
/// print(engine.srcDir.path); // /Users/.../engine/src
43+
/// ```
44+
///
45+
/// Throws a [InvalidEngineException] if the path is not a valid engine root.
46+
factory Engine.fromSrcPath(String srcPath) {
47+
// If the path does not end in `/src`, fail.
48+
if (p.basename(srcPath) != 'src') {
49+
throw InvalidEngineException.doesNotEndWithSrc(srcPath);
50+
}
51+
52+
// If the directory does not exist, or is not a directory, fail.
53+
final io.Directory srcDir = io.Directory(srcPath);
54+
if (!srcDir.existsSync()) {
55+
throw InvalidEngineException.notADirectory(srcPath);
56+
}
57+
58+
// Check for the existence of a `flutter` directory within `src`.
59+
final io.Directory flutterDir = io.Directory(p.join(srcPath, 'flutter'));
60+
if (!flutterDir.existsSync()) {
61+
throw InvalidEngineException.missingFlutterDirectory(srcPath);
62+
}
63+
64+
// We do **NOT** check for the existence of a `out` directory within `src`,
65+
// it's not required to exist (i.e. a new checkout of the engine), and we
66+
// don't want to fail if it doesn't exist.
67+
final io.Directory outDir = io.Directory(p.join(srcPath, 'out'));
68+
69+
return Engine._(srcDir, flutterDir, outDir);
70+
}
71+
72+
/// Creates an [Engine] by looking for a `src/` directory in the given path.
73+
///
74+
/// ```dart
75+
/// // Use the current working directory.
76+
/// final Engine engine = Engine.findWithin();
77+
/// print(engine.srcDir.path); // /Users/.../engine/src
78+
///
79+
/// // Use a specific directory.
80+
/// final Engine engine = Engine.findWithin('/Users/.../engine/src/foo/bar/baz');
81+
/// print(engine.srcDir.path); // /Users/.../engine/src
82+
/// ```
83+
///
84+
/// If a path is not provided, the current working directory is used.
85+
///
86+
/// Throws a [StateError] if the path is not within a valid engine.
87+
factory Engine.findWithin([String? path]) {
88+
path ??= p.current;
89+
90+
// Search parent directories for a `src` directory.
91+
io.Directory maybeSrcDir = io.Directory(path);
92+
93+
if (!maybeSrcDir.existsSync()) {
94+
throw StateError(
95+
'The path "$path" does not exist or is not a directory.'
96+
);
97+
}
98+
99+
do {
100+
try {
101+
return Engine.fromSrcPath(maybeSrcDir.path);
102+
} on InvalidEngineException {
103+
// Ignore, we'll keep searching.
104+
}
105+
maybeSrcDir = maybeSrcDir.parent;
106+
} while (maybeSrcDir.parent.path != maybeSrcDir.path /* at root */);
107+
108+
throw StateError(
109+
'The path "$path" is not within a Flutter engine source directory.'
110+
);
111+
}
112+
113+
const Engine._(
114+
this.srcDir,
115+
this.flutterDir,
116+
this.outDir,
117+
);
118+
119+
/// The path to the `$ENGINE/src` directory.
120+
final io.Directory srcDir;
121+
122+
/// The path to the `$ENGINE/src/flutter` directory.
123+
final io.Directory flutterDir;
124+
125+
/// The path to the `$ENGINE/src/out` directory.
126+
///
127+
/// **NOTE**: This directory may not exist.
128+
final io.Directory outDir;
129+
130+
/// Returns a list of all output targets in [outDir].
131+
List<Output> outputs() {
132+
return outDir
133+
.listSync()
134+
.whereType<io.Directory>()
135+
.map<Output>(Output._)
136+
.toList();
137+
}
138+
139+
/// Returns the most recently modified output target in [outDir].
140+
///
141+
/// If there are no output targets, returns `null`.
142+
Output? latestOutput() {
143+
final List<Output> outputs = this.outputs();
144+
if (outputs.isEmpty) {
145+
return null;
146+
}
147+
outputs.sort((Output a, Output b) {
148+
return b.dir.statSync().modified.compareTo(a.dir.statSync().modified);
149+
});
150+
return outputs.first;
151+
}
152+
}
153+
154+
/// Thrown when an [Engine] could not be created from a path.
155+
sealed class InvalidEngineException implements Exception {
156+
/// Thrown when an [Engine] was created from a path not ending in `src`.
157+
factory InvalidEngineException.doesNotEndWithSrc(String path) {
158+
return InvalidEngineSrcPathException._(path);
159+
}
160+
161+
/// Thrown when an [Engine] was created from a directory that does not exist.
162+
factory InvalidEngineException.notADirectory(String path) {
163+
return InvalidEngineNotADirectoryException._(path);
164+
}
165+
166+
/// Thrown when an [Engine] was created from a path not containing `flutter/`.
167+
factory InvalidEngineException.missingFlutterDirectory(String path) {
168+
return InvalidEngineMissingFlutterDirectoryException._(path);
169+
}
170+
}
171+
172+
/// Thrown when an [Engine] was created from a path not ending in `src`.
173+
final class InvalidEngineSrcPathException implements InvalidEngineException {
174+
InvalidEngineSrcPathException._(this.path);
175+
176+
/// The path that was used to create the [Engine].
177+
final String path;
178+
179+
@override
180+
String toString() {
181+
return 'The path $path does not end in `${p.separator}src`.';
182+
}
183+
}
184+
185+
/// Thrown when an [Engine] was created from a path that is not a directory.
186+
final class InvalidEngineNotADirectoryException implements InvalidEngineException {
187+
InvalidEngineNotADirectoryException._(this.path);
188+
189+
/// The path that was used to create the [Engine].
190+
final String path;
191+
192+
@override
193+
String toString() {
194+
return 'The path "$path" does not exist or is not a directory.';
195+
}
196+
}
197+
198+
/// Thrown when an [Engine] was created from a path not containing `flutter/`.
199+
final class InvalidEngineMissingFlutterDirectoryException implements InvalidEngineException {
200+
InvalidEngineMissingFlutterDirectoryException._(this.path);
201+
202+
/// The path that was used to create the [Engine].
203+
final String path;
204+
205+
@override
206+
String toString() {
207+
return 'The path "$path" does not contain a "flutter" directory.';
208+
}
209+
}
210+
211+
/// Represents a single output target in the `$ENGINE/src/out` directory.
212+
final class Output {
213+
const Output._(this.dir);
214+
215+
/// The directory containing the output target.
216+
final io.Directory dir;
217+
218+
/// The `compile_commands.json` file for this output target.
219+
///
220+
/// Returns `null` if the file does not exist.
221+
io.File? get compileCommandsJson {
222+
final io.File file = io.File(p.join(dir.path, 'compile_commands.json'));
223+
if (!file.existsSync()) {
224+
return null;
225+
}
226+
return file;
227+
}
228+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Copyright 2013 The Flutter Authors. All rights reserved.
2+
# Use of this source code is governed by a BSD-style license that can be
3+
# found in the LICENSE file.
4+
5+
name: engine_repo_tools
6+
publish_to: none
7+
environment:
8+
sdk: ^3.0.0
9+
10+
# Do not add any dependencies that require more than what is provided in
11+
# //third_party/pkg, //third_party/dart/pkg, or
12+
# //third_party/dart/third_party/pkg. In particular, package:test is not usable
13+
# here.
14+
15+
# If you do add packages here, make sure you can run `pub get --offline`, and
16+
# check the .packages and .package_config to make sure all the paths are
17+
# relative to this directory into //third_party/dart
18+
19+
dependencies:
20+
meta: any
21+
path: any
22+
23+
dev_dependencies:
24+
async_helper: any
25+
expect: any
26+
litetest: any
27+
smith: any
28+
29+
dependency_overrides:
30+
async_helper:
31+
path: ../../../../third_party/dart/pkg/async_helper
32+
expect:
33+
path: ../../../../third_party/dart/pkg/expect
34+
litetest:
35+
path: ../../../testing/litetest
36+
meta:
37+
path: ../../../../third_party/dart/pkg/meta
38+
path:
39+
path: ../../../../third_party/dart/third_party/pkg/path
40+
smith:
41+
path: ../../../../third_party/dart/pkg/smith

0 commit comments

Comments
 (0)