blob: 0603703181577772c02f11ef5c2391f2199553a6 [file] [log] [blame]
// Copyright (c) 2023, 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:code_assets/code_assets.dart';
import 'package:glob/glob.dart';
import 'package:glob/list_local_fs.dart';
import 'package:logging/logging.dart';
import 'package:pub_semver/pub_semver.dart';
import '../utils/run_process.dart';
import '../utils/sem_version.dart';
import 'tool.dart';
import 'tool_error.dart';
import 'tool_instance.dart';
abstract class ToolResolver {
/// Resolves tools on the host system.
Future<List<ToolInstance>> resolve({required Logger? logger});
}
/// Tries to resolve a tool on the `PATH`.
///
/// Uses `which` (`where` on Windows) to resolve a tool.
class PathToolResolver extends ToolResolver {
/// The [Tool.name] of the [Tool] to find on the `PATH`.
final String toolName;
final String executableName;
PathToolResolver({required this.toolName, String? executableName})
: executableName =
executableName ??
OS.current.executableFileName(toolName.toLowerCase());
@override
Future<List<ToolInstance>> resolve({required Logger? logger}) async {
logger?.finer('Looking for $toolName on PATH.');
final uri = await runWhich(logger: logger);
if (uri == null) {
logger?.fine('Did not find $toolName on PATH.');
return [];
}
final toolInstances = [
ToolInstance(
tool: Tool(name: toolName),
uri: uri,
),
];
logger?.fine('Found ${toolInstances.single}.');
return toolInstances;
}
static Uri get which => Uri.file(Platform.isWindows ? 'where' : 'which');
Future<Uri?> runWhich({required Logger? logger}) async {
final process = await runProcess(
executable: which,
arguments: [executableName],
logger: logger,
);
if (process.exitCode == 0) {
final file = File(LineSplitter.split(process.stdout).first);
final uri = File(await file.resolveSymbolicLinks()).uri;
if (uri.pathSegments.last == 'llvm') {
// https://github.com/dart-lang/native/issues/136
return file.uri;
}
return uri;
}
// The exit code for executable not being on the `PATH`.
assert(process.exitCode == 1);
return null;
}
}
class CliVersionResolver implements ToolResolver {
final ToolResolver wrappedResolver;
final List<String> arguments;
final int expectedExitCode;
CliVersionResolver({
required this.wrappedResolver,
this.arguments = const ['--version'],
this.expectedExitCode = 0,
});
@override
Future<List<ToolInstance>> resolve({required Logger? logger}) async {
final toolInstances = await wrappedResolver.resolve(logger: logger);
return [
for (final toolInstance in toolInstances)
await lookupVersion(
toolInstance,
arguments: arguments,
expectedExitCode: expectedExitCode,
logger: logger,
),
];
}
static Future<ToolInstance> lookupVersion(
ToolInstance toolInstance, {
List<String> arguments = const ['--version'],
int expectedExitCode = 0,
required Logger? logger,
}) async {
if (toolInstance.version != null) return toolInstance;
logger?.finer('Looking up version with --version for $toolInstance.');
final version = await executableVersion(
toolInstance.uri,
arguments: arguments,
expectedExitCode: expectedExitCode,
logger: logger,
);
final result = toolInstance.copyWith(version: version);
logger?.fine('Found version for $result.');
return result;
}
static Future<Version> executableVersion(
Uri executable, {
List<String> arguments = const ['--version'],
int expectedExitCode = 0,
required Logger? logger,
}) async {
final process = await runProcess(
executable: executable,
arguments: arguments,
logger: logger,
);
if (process.exitCode != expectedExitCode) {
final executablePath = executable.toFilePath();
throw ToolError(
'`$executablePath ${arguments.join(' ')}` returned unexpected exit'
' code: ${process.exitCode}.',
);
}
return versionFromString(process.stderr) ??
versionFromString(process.stdout)!;
}
}
class PathVersionResolver implements ToolResolver {
ToolResolver wrappedResolver;
PathVersionResolver({required this.wrappedResolver});
@override
Future<List<ToolInstance>> resolve({required Logger? logger}) async {
final toolInstances = await wrappedResolver.resolve(logger: logger);
return [
for (final toolInstance in toolInstances) lookupVersion(toolInstance),
];
}
static ToolInstance lookupVersion(ToolInstance toolInstance) {
if (toolInstance.version != null) {
return toolInstance;
}
return toolInstance.copyWith(version: version(toolInstance.uri));
}
static Version? version(Uri uri) {
final versionString = uri.pathSegments.where((e) => e != '').last;
final version = versionFromString(versionString);
return version;
}
}
/// A resolver which invokes all [resolvers] tools.
class ToolResolvers implements ToolResolver {
final List<ToolResolver> resolvers;
ToolResolvers(this.resolvers);
@override
Future<List<ToolInstance>> resolve({required Logger? logger}) async => [
for (final resolver in resolvers) ...await resolver.resolve(logger: logger),
];
}
class InstallLocationResolver implements ToolResolver {
final String toolName;
final List<String> paths;
InstallLocationResolver({required this.toolName, required this.paths});
static const home = '\$HOME';
@override
Future<List<ToolInstance>> resolve({required Logger? logger}) async {
logger?.finer('Looking for $toolName in $paths.');
final resolvedPaths = [
for (final path in paths) ...await tryResolvePath(path),
];
final toolInstances = [
for (final uri in resolvedPaths)
ToolInstance(
tool: Tool(name: toolName),
uri: uri,
),
];
if (toolInstances.isNotEmpty) {
logger?.fine('Found $toolInstances.');
} else {
logger?.finer('Found no $toolName in $paths.');
}
return toolInstances;
}
Future<List<Uri>> tryResolvePath(String path) async {
if (path.startsWith(home)) {
final homeDir_ = homeDir;
if (homeDir_ == null) return [];
path = path.replaceAll(
'$home/',
homeDir!.toFilePath().replaceAll('\\', '/'),
);
}
final result = <Uri>[];
final fileSystemEntities = await Glob(path).list().toList();
for (final fileSystemEntity in fileSystemEntities) {
if (!await fileSystemEntity.exists()) {
continue;
}
if (fileSystemEntity is! Directory && path.endsWith('/')) {
continue;
}
result.add(fileSystemEntity.uri);
}
return result;
}
static final Uri? homeDir = () {
final path =
Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'];
if (path == null) return null;
return Directory(path).uri;
}();
}
class RelativeToolResolver implements ToolResolver {
final String toolName;
final ToolResolver wrappedResolver;
final Uri relativePath;
RelativeToolResolver({
required this.toolName,
required this.wrappedResolver,
required this.relativePath,
});
@override
Future<List<ToolInstance>> resolve({required Logger? logger}) async {
final otherToolInstances = await wrappedResolver.resolve(logger: logger);
logger?.finer(
'Looking for $toolName relative to $otherToolInstances '
'with $relativePath.',
);
final globs = [
for (final toolInstance in otherToolInstances)
Glob(
[
Glob.quote(
toolInstance.uri.resolve('.').toFilePath().replaceAll('\\', '/'),
),
relativePath.path,
].join(),
),
];
final fileSystemEntities = [
for (final glob in globs) ...await glob.list().toList(),
];
final result = [
for (final fileSystemEntity in fileSystemEntities)
ToolInstance(
tool: Tool(name: toolName),
uri: fileSystemEntity.uri,
),
];
if (result.isNotEmpty) {
logger?.fine('Found $result.');
} else {
logger?.finer('Found no $toolName relative to $otherToolInstances.');
}
return result;
}
}
class CliFilter implements ToolResolver {
final ToolResolver wrappedResolver;
final List<String> cliArguments;
final bool Function({required String stdout}) keepIf;
CliFilter({
required this.wrappedResolver,
required this.cliArguments,
required this.keepIf,
});
@override
Future<List<ToolInstance>> resolve({required Logger? logger}) async {
final toolInstances = await wrappedResolver.resolve(logger: logger);
return [
for (final toolInstance in toolInstances)
await filter(toolInstance, logger: logger),
].whereType<ToolInstance>().toList();
}
Future<ToolInstance?> filter(
ToolInstance toolInstance, {
required Logger? logger,
}) async {
if (toolInstance.version != null) return toolInstance;
logger?.finer('Checking if $toolInstance satisfies CLI filter.');
final stdout = await executeCli(
toolInstance.uri,
arguments: cliArguments,
logger: logger,
);
final doKeep = keepIf(stdout: stdout);
if (doKeep) {
logger?.fine('$toolInstance satisfies CLI filter.');
return toolInstance;
}
logger?.fine('$toolInstance does not satisfy CLI filter.');
return null;
}
static Future<String> executeCli(
Uri executable, {
required List<String> arguments,
int expectedExitCode = 0,
required Logger? logger,
}) async {
final process = await runProcess(
executable: executable,
arguments: arguments,
logger: logger,
);
final exitCode = process.exitCode;
assert(exitCode == expectedExitCode);
return process.stdout;
}
}