Skip to content

Commit

Permalink
A minimal engine_tools_lib to use for local-repo Dart tooling (#45154)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
matanlurey authored Aug 29, 2023
1 parent 9778c2c commit ddbef31
Show file tree
Hide file tree
Showing 6 changed files with 564 additions and 0 deletions.
20 changes: 20 additions & 0 deletions testing/run_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,25 @@ def gather_clang_tidy_tests(build_dir):
)


def gather_engine_repo_tools_tests(build_dir):
test_dir = os.path.join(
BUILDROOT_DIR, 'flutter', 'tools', 'pkg', 'engine_repo_tools'
)
dart_tests = glob.glob('%s/*_test.dart' % test_dir)
for dart_test_file in dart_tests:
opts = [
'--disable-dart-dev',
dart_test_file,
]
yield EngineExecutableTask(
build_dir,
os.path.join('dart-sdk', 'bin', 'dart'),
None,
flags=opts,
cwd=test_dir
)


def gather_api_consistency_tests(build_dir):
test_dir = os.path.join(BUILDROOT_DIR, 'flutter', 'tools', 'api_check')
dart_tests = glob.glob('%s/test/*_test.dart' % test_dir)
Expand Down Expand Up @@ -1230,6 +1249,7 @@ def main():
tasks += list(gather_litetest_tests(build_dir))
tasks += list(gather_githooks_tests(build_dir))
tasks += list(gather_clang_tidy_tests(build_dir))
tasks += list(gather_engine_repo_tools_tests(build_dir))
tasks += list(gather_api_consistency_tests(build_dir))
tasks += list(gather_path_ops_tests(build_dir))
tasks += list(gather_const_finder_tests(build_dir))
Expand Down
17 changes: 17 additions & 0 deletions tools/pkg/engine_repo_tools/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# engine_repo_tools

This is a repo-internal library for `flutter/engine`, that contains shared code
for writing tools that operate on the engine repository. For example, finding
the latest compiled engine artifacts in the `out/` directory:

```dart
import 'package:engine_repo_tools/engine_repo_tools.dart';
void main() {
final engine = Engine.findWithin();
final latest = engine.latestOutput();
if (latest != null) {
print('Latest compile_commands.json: ${latest.compileCommandsJson?.path}');
}
}
```
228 changes: 228 additions & 0 deletions tools/pkg/engine_repo_tools/lib/engine_repo_tools.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/// A minimal library for discovering and probing a local engine repository.
///
/// This library is intended to be used by tools that need to interact with a
/// local engine repository, such as `clang_tidy` or `githooks`. For example,
/// finding the `compile_commands.json` file for the most recently built output:
///
/// ```dart
/// final Engine engine = Engine.findWithin();
/// final Output? output = engine.latestOutput();
/// if (output == null) {
/// print('No output targets found.');
/// } else {
/// final io.File? compileCommandsJson = output.compileCommandsJson;
/// if (compileCommandsJson == null) {
/// print('No compile_commands.json file found.');
/// } else {
/// print('Found compile_commands.json file at ${compileCommandsJson.path}');
/// }
/// }
/// ```
library;

import 'dart:io' as io;

import 'package:path/path.dart' as p;

/// Represents the `$ENGINE` directory (i.e. a checked-out Flutter engine).
///
/// If you have a path to the `$ENGINE/src` directory, use [Engine.fromSrcPath].
///
/// If you have a path to a directory within the `$ENGINE/src` directory, or
/// want to use the current working directory, use [Engine.findWithin].
final class Engine {
/// Creates an [Engine] from a path such as `/Users/.../flutter/engine/src`.
///
/// ```dart
/// final Engine engine = Engine.findWithin('/Users/.../engine/src');
/// print(engine.srcDir.path); // /Users/.../engine/src
/// ```
///
/// Throws a [InvalidEngineException] if the path is not a valid engine root.
factory Engine.fromSrcPath(String srcPath) {
// If the path does not end in `/src`, fail.
if (p.basename(srcPath) != 'src') {
throw InvalidEngineException.doesNotEndWithSrc(srcPath);
}

// If the directory does not exist, or is not a directory, fail.
final io.Directory srcDir = io.Directory(srcPath);
if (!srcDir.existsSync()) {
throw InvalidEngineException.notADirectory(srcPath);
}

// Check for the existence of a `flutter` directory within `src`.
final io.Directory flutterDir = io.Directory(p.join(srcPath, 'flutter'));
if (!flutterDir.existsSync()) {
throw InvalidEngineException.missingFlutterDirectory(srcPath);
}

// We do **NOT** check for the existence of a `out` directory within `src`,
// it's not required to exist (i.e. a new checkout of the engine), and we
// don't want to fail if it doesn't exist.
final io.Directory outDir = io.Directory(p.join(srcPath, 'out'));

return Engine._(srcDir, flutterDir, outDir);
}

/// Creates an [Engine] by looking for a `src/` directory in the given path.
///
/// ```dart
/// // Use the current working directory.
/// final Engine engine = Engine.findWithin();
/// print(engine.srcDir.path); // /Users/.../engine/src
///
/// // Use a specific directory.
/// final Engine engine = Engine.findWithin('/Users/.../engine/src/foo/bar/baz');
/// print(engine.srcDir.path); // /Users/.../engine/src
/// ```
///
/// If a path is not provided, the current working directory is used.
///
/// Throws a [StateError] if the path is not within a valid engine.
factory Engine.findWithin([String? path]) {
path ??= p.current;

// Search parent directories for a `src` directory.
io.Directory maybeSrcDir = io.Directory(path);

if (!maybeSrcDir.existsSync()) {
throw StateError(
'The path "$path" does not exist or is not a directory.'
);
}

do {
try {
return Engine.fromSrcPath(maybeSrcDir.path);
} on InvalidEngineException {
// Ignore, we'll keep searching.
}
maybeSrcDir = maybeSrcDir.parent;
} while (maybeSrcDir.parent.path != maybeSrcDir.path /* at root */);

throw StateError(
'The path "$path" is not within a Flutter engine source directory.'
);
}

const Engine._(
this.srcDir,
this.flutterDir,
this.outDir,
);

/// The path to the `$ENGINE/src` directory.
final io.Directory srcDir;

/// The path to the `$ENGINE/src/flutter` directory.
final io.Directory flutterDir;

/// The path to the `$ENGINE/src/out` directory.
///
/// **NOTE**: This directory may not exist.
final io.Directory outDir;

/// Returns a list of all output targets in [outDir].
List<Output> outputs() {
return outDir
.listSync()
.whereType<io.Directory>()
.map<Output>(Output._)
.toList();
}

/// Returns the most recently modified output target in [outDir].
///
/// If there are no output targets, returns `null`.
Output? latestOutput() {
final List<Output> outputs = this.outputs();
if (outputs.isEmpty) {
return null;
}
outputs.sort((Output a, Output b) {
return b.dir.statSync().modified.compareTo(a.dir.statSync().modified);
});
return outputs.first;
}
}

/// Thrown when an [Engine] could not be created from a path.
sealed class InvalidEngineException implements Exception {
/// Thrown when an [Engine] was created from a path not ending in `src`.
factory InvalidEngineException.doesNotEndWithSrc(String path) {
return InvalidEngineSrcPathException._(path);
}

/// Thrown when an [Engine] was created from a directory that does not exist.
factory InvalidEngineException.notADirectory(String path) {
return InvalidEngineNotADirectoryException._(path);
}

/// Thrown when an [Engine] was created from a path not containing `flutter/`.
factory InvalidEngineException.missingFlutterDirectory(String path) {
return InvalidEngineMissingFlutterDirectoryException._(path);
}
}

/// Thrown when an [Engine] was created from a path not ending in `src`.
final class InvalidEngineSrcPathException implements InvalidEngineException {
InvalidEngineSrcPathException._(this.path);

/// The path that was used to create the [Engine].
final String path;

@override
String toString() {
return 'The path $path does not end in `${p.separator}src`.';
}
}

/// Thrown when an [Engine] was created from a path that is not a directory.
final class InvalidEngineNotADirectoryException implements InvalidEngineException {
InvalidEngineNotADirectoryException._(this.path);

/// The path that was used to create the [Engine].
final String path;

@override
String toString() {
return 'The path "$path" does not exist or is not a directory.';
}
}

/// Thrown when an [Engine] was created from a path not containing `flutter/`.
final class InvalidEngineMissingFlutterDirectoryException implements InvalidEngineException {
InvalidEngineMissingFlutterDirectoryException._(this.path);

/// The path that was used to create the [Engine].
final String path;

@override
String toString() {
return 'The path "$path" does not contain a "flutter" directory.';
}
}

/// Represents a single output target in the `$ENGINE/src/out` directory.
final class Output {
const Output._(this.dir);

/// The directory containing the output target.
final io.Directory dir;

/// The `compile_commands.json` file for this output target.
///
/// Returns `null` if the file does not exist.
io.File? get compileCommandsJson {
final io.File file = io.File(p.join(dir.path, 'compile_commands.json'));
if (!file.existsSync()) {
return null;
}
return file;
}
}
41 changes: 41 additions & 0 deletions tools/pkg/engine_repo_tools/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright 2013 The Flutter Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

name: engine_repo_tools
publish_to: none
environment:
sdk: ^3.0.0

# Do not add any dependencies that require more than what is provided in
# //third_party/pkg, //third_party/dart/pkg, or
# //third_party/dart/third_party/pkg. In particular, package:test is not usable
# here.

# If you do add packages here, make sure you can run `pub get --offline`, and
# check the .packages and .package_config to make sure all the paths are
# relative to this directory into //third_party/dart

dependencies:
meta: any
path: any

dev_dependencies:
async_helper: any
expect: any
litetest: any
smith: any

dependency_overrides:
async_helper:
path: ../../../../third_party/dart/pkg/async_helper
expect:
path: ../../../../third_party/dart/pkg/expect
litetest:
path: ../../../testing/litetest
meta:
path: ../../../../third_party/dart/pkg/meta
path:
path: ../../../../third_party/dart/third_party/pkg/path
smith:
path: ../../../../third_party/dart/pkg/smith
Loading

0 comments on commit ddbef31

Please sign in to comment.