blob: 94cf9753e1380553caf75411761cc341a8a39c9f [file] [log] [blame] [edit]
// Copyright (c) 2024, 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' show Platform;
import 'package:analyzer/file_system/file_system.dart';
import 'package:dartdoc/src/dartdoc_options.dart';
import 'package:dartdoc/src/io_utils.dart';
import 'package:dartdoc/src/tool_definition.dart';
import 'package:dartdoc/src/tool_runner.dart';
import 'package:yaml/yaml.dart';
/// A configuration class that can interpret [ToolDefinition]s from a YAML map.
class ToolConfiguration {
final Map<String, ToolDefinition> tools;
final ResourceProvider resourceProvider;
late final ToolRunner runner = ToolRunner(this);
ToolConfiguration._(this.tools, this.resourceProvider);
static ToolConfiguration empty(ResourceProvider resourceProvider) {
return ToolConfiguration._(const {}, resourceProvider);
}
// TODO(jcollins-g): consider caching these.
static ToolConfiguration fromYamlMap(YamlMap yamlMap,
String canonicalYamlPath, ResourceProvider resourceProvider) {
var newToolDefinitions = <String, ToolDefinition>{};
var pathContext = resourceProvider.pathContext;
for (var MapEntry(:key, value: toolMap) in yamlMap.entries) {
var name = key.toString();
if (toolMap is! Map) {
throw DartdocOptionError(
'Tools must be defined as a map of tool names to definitions. Tool '
'$name is not a map.');
}
var description = toolMap['description'].toString();
List<String>? findCommand([String prefix = '']) {
// If the command key is given, then it applies to all platforms.
var commandFromKey = toolMap.containsKey('${prefix}command')
? '${prefix}command'
: '$prefix${Platform.operatingSystem}';
if (!toolMap.containsKey(commandFromKey)) {
return null;
}
var commandFrom = toolMap[commandFromKey] as YamlNode;
List<String> command;
if (commandFrom.value is String) {
command = [commandFrom.toString()];
} else if (commandFrom is YamlList) {
command = commandFrom.map((node) => node.toString()).toList();
} else {
throw DartdocOptionError(
'Tool commands must be a path to an executable, or a list of '
'strings that starts with a path to an executable. The tool '
"'$name' has a '$commandFromKey' entry that is a "
'${commandFrom.runtimeType}');
}
if (command.isEmpty || command[0].isEmpty) {
throw DartdocOptionError(
'Tool commands must not be empty. Tool $name command entry '
'"$commandFromKey" must contain at least one path.');
}
return command;
}
var command = findCommand();
if (command == null) {
throw DartdocOptionError(
'At least one of "command" or "${Platform.operatingSystem}" must '
'be defined for the tool $name.');
}
var setupCommand = findCommand('setup_');
var rawCompileArgs = toolMap[compileArgsTagName];
var compileArgs = switch (rawCompileArgs) {
null => const <String>[],
String() => [toolMap[compileArgsTagName].toString()],
YamlList() =>
rawCompileArgs.map((node) => node.toString()).toList(growable: false),
_ => throw DartdocOptionError(
'Tool compile arguments must be a list of strings. The tool '
"'$name' has a '$compileArgsTagName' entry that is a "
'${rawCompileArgs.runtimeType}',
),
};
/// Validates [executable] and returns whether it is a Dart script.
bool isDartScript(String executable) {
var executableFile = resourceProvider.getFile(executable);
if (resourceProvider.isNotFound(executableFile)) {
throw DartdocOptionError('Command executables must exist. '
'The file "$executable" does not exist for tool $name.');
}
var isDartCommand = ToolDefinition.isDartExecutable(executable);
// Dart scripts don't need to be executable, because they'll be
// executed with the Dart binary.
if (!isDartCommand && !resourceProvider.isExecutable(executableFile)) {
throw DartdocOptionError('Non-Dart commands must be '
'executable. The file "$executable" for tool $name does not have '
'execute permission.');
}
return isDartCommand;
}
var executableRelativePath = command.removeAt(0);
var executable = pathContext.canonicalize(
pathContext.join(canonicalYamlPath, executableRelativePath));
var isDartSetupScript = isDartScript(executable);
if (setupCommand != null) {
var setupExecutableRelativePath = setupCommand.removeAt(0);
var setupExecutable = pathContext.canonicalize(
pathContext.join(canonicalYamlPath, setupExecutableRelativePath));
// Setup commands aren't snapshotted, since they're only run once.
setupCommand = [
if (isDartSetupScript) Platform.resolvedExecutable,
setupExecutable,
...setupCommand,
];
}
newToolDefinitions[name] = ToolDefinition.fromCommand(
[executable, ...command],
setupCommand ?? const [],
description,
resourceProvider,
compileArgs: compileArgs);
}
return ToolConfiguration._(newToolDefinitions, resourceProvider);
}
}