Skip to content

Commit 88283d1

Browse files
committed
Adding more complete tool options.
1 parent 2056fe1 commit 88283d1

File tree

4 files changed

+143
-28
lines changed

4 files changed

+143
-28
lines changed

lib/src/dartdoc_options.dart

Lines changed: 111 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,103 @@ class CategoryConfiguration {
110110
}
111111
}
112112

113+
class ToolDefinition {
114+
final List<String> command;
115+
final String description;
116+
117+
ToolDefinition(this.command, this.description)
118+
: assert(command != null),
119+
assert(command.isNotEmpty),
120+
assert(description != null);
121+
122+
@override
123+
String toString() => '$runtimeType: "${command.join(' ')}" ($description)';
124+
}
125+
126+
class ToolConfiguration {
127+
final Map<String, ToolDefinition> tools;
128+
129+
ToolConfiguration._(this.tools);
130+
131+
static ToolConfiguration get empty {
132+
return new ToolConfiguration._({});
133+
}
134+
135+
static ToolConfiguration fromYamlMap(
136+
YamlMap yamlMap, pathLib.Context pathContext) {
137+
Map<String, ToolDefinition> newToolDefinitions = {};
138+
for (MapEntry entry in yamlMap.entries) {
139+
String name = entry.key.toString();
140+
var toolMap = entry.value;
141+
String description;
142+
List<String> command;
143+
if (toolMap is Map) {
144+
description = toolMap['description']?.toString();
145+
// If the command key is given, then it applies to all platforms.
146+
String commandFrom = toolMap.containsKey('command')
147+
? 'command'
148+
: Platform.operatingSystem;
149+
if (toolMap.containsKey(commandFrom)) {
150+
if (toolMap[commandFrom].value is String) {
151+
command = [toolMap[commandFrom].toString()];
152+
if (command[0].isEmpty) {
153+
throw new DartdocOptionError(
154+
'Tool commands must not be empty. Tool $name command entry '
155+
'"$commandFrom" must contain at least one path.');
156+
}
157+
} else if (toolMap[commandFrom] is YamlList) {
158+
command = (toolMap[commandFrom] as YamlList)
159+
.map<String>((node) => node.toString())
160+
.toList();
161+
if (command.isEmpty) {
162+
throw new DartdocOptionError(
163+
'Tool commands must not be empty. Tool $name command entry '
164+
'"$commandFrom" must contain at least one path.');
165+
}
166+
} else {
167+
throw new DartdocOptionError(
168+
'Tool commands must be a path to an executable, or a list of '
169+
'strings that starts with a path to an executable. '
170+
'The tool $name has a $commandFrom entry that is a '
171+
'${toolMap[commandFrom].runtimeType}');
172+
}
173+
}
174+
} else {
175+
throw new DartdocOptionError(
176+
'Tools must be defined as a map of tool names to definitions. Tool '
177+
'$name is not a map.');
178+
}
179+
if (command == null) {
180+
throw new DartdocOptionError(
181+
'At least one of "command" or "${Platform.operatingSystem}" must '
182+
'be defined for the tool $name.');
183+
}
184+
String executable = command.removeAt(0);
185+
executable = pathContext.canonicalize(executable);
186+
File executableFile = new File(executable);
187+
FileStat exeStat = executableFile.statSync();
188+
if (exeStat.type == FileSystemEntityType.notFound) {
189+
throw new DartdocOptionError('Command executables must exist. '
190+
'The file "$executable" does not exist for tool $name.');
191+
}
192+
// Dart scripts don't need to be executable, because they'll be
193+
// executed with the Dart binary.
194+
bool isExecutable(int mode) {
195+
return (0x1 & ((mode >> 6) | (mode >> 3) | mode)) != 0;
196+
}
197+
198+
if (!executable.endsWith('.dart') && !isExecutable(exeStat.mode)) {
199+
throw new DartdocOptionError('Non-Dart commands must be '
200+
'executable. The file "$executable" for tool $name does not have '
201+
'executable permission.');
202+
}
203+
newToolDefinitions[name] =
204+
new ToolDefinition([executable] + command, description);
205+
}
206+
return new ToolConfiguration._(newToolDefinitions);
207+
}
208+
}
209+
113210
/// A container class to keep track of where our yaml data came from.
114211
class _YamlFileData {
115212
/// The map from the yaml file.
@@ -212,7 +309,10 @@ abstract class DartdocOption<T> {
212309
assert(!(isDir && isExecutable));
213310
assert(!(isFile && isExecutable));
214311
if (isDir || isFile || isExecutable)
215-
assert(_isString || _isListString || _isMapString);
312+
assert(_isString ||
313+
_isListString ||
314+
_isMapString ||
315+
_convertYamlToType != null);
216316
if (mustExist) assert(isDir || isFile || isExecutable);
217317
}
218318

@@ -271,6 +371,9 @@ abstract class DartdocOption<T> {
271371
resolvedPaths = valueWithContext.resolvedValue.toList();
272372
} else if (valueWithContext.value is Map<String, String>) {
273373
resolvedPaths = valueWithContext.resolvedValue.values.toList();
374+
} else {
375+
assert(false, "Trying to ensure existence of wrong type "
376+
"${valueWithContext.value.runtimeType}");
274377
}
275378
for (String path in resolvedPaths) {
276379
FileSystemEntity f = isDir ? new Directory(path) : new File(path);
@@ -1073,7 +1176,7 @@ class DartdocOptionContext {
10731176
List<String> get includeExternal =>
10741177
optionSet['includeExternal'].valueAt(context);
10751178
bool get includeSource => optionSet['includeSource'].valueAt(context);
1076-
Map<String, String> get tools => optionSet['tools'].valueAt(context);
1179+
ToolConfiguration get tools => optionSet['tools'].valueAt(context);
10771180

10781181
/// _input is only used to construct synthetic options.
10791182
// ignore: unused_element
@@ -1299,10 +1402,12 @@ Future<List<DartdocOption>> createDartdocOptions() async {
12991402
new DartdocOptionArgOnly<bool>('verboseWarnings', true,
13001403
help: 'Display extra debugging information and help with warnings.',
13011404
negatable: true),
1302-
new DartdocOptionArgFile<Map<String, String>>('tools', <String, String>{},
1303-
isExecutable: true,
1304-
mustExist: true,
1405+
new DartdocOptionFileOnly<ToolConfiguration>(
1406+
'tools', ToolConfiguration.empty,
1407+
convertYamlToType: ToolConfiguration.fromYamlMap,
13051408
help: 'A map of tool names to executable paths. Each executable must '
1306-
'exist.'),
1409+
'exist. Executables for different platforms are specified by '
1410+
'giving the platform name as a key, and a list of strings as the '
1411+
'command.'),
13071412
];
13081413
}

lib/src/tool_runner.dart

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ library dartdoc.tool_runner;
77
import 'dart:io';
88

99
import 'package:path/path.dart' as pathLib;
10+
import 'dartdoc_options.dart';
1011

1112
typedef ToolErrorCallback = void Function(String message);
1213
typedef FakeResultCallback = String Function(String tool,
@@ -16,14 +17,14 @@ typedef FakeResultCallback = String Function(String tool,
1617
class ToolRunner {
1718
/// Creates a new ToolRunner.
1819
///
19-
/// Takes a [toolMap] that describes all of the available tools.
20+
/// Takes a [toolConfiguration] that describes all of the available tools.
2021
/// An optional `errorCallback` will be called for each error message
2122
/// generated by the tool.
22-
ToolRunner(this.toolMap, [this._errorCallback])
23+
ToolRunner(this.toolConfiguration, [this._errorCallback])
2324
: temporaryDirectory =
2425
Directory.systemTemp.createTempSync('dartdoc_tools_');
2526

26-
final Map<String, String> toolMap;
27+
final ToolConfiguration toolConfiguration;
2728
final Directory temporaryDirectory;
2829
final ToolErrorCallback _errorCallback;
2930
int _temporaryFileCount = 0;
@@ -61,17 +62,16 @@ class ToolRunner {
6162
assert(args.isNotEmpty);
6263
content ??= '';
6364
String tool = args.removeAt(0);
64-
if (!toolMap.containsKey(tool)) {
65+
if (!toolConfiguration.tools.containsKey(tool)) {
6566
_error('Unable to find definition for tool "$tool" in tool map. '
6667
'Did you add it to dartdoc_options.yaml?');
6768
return '';
6869
}
69-
String toolPath = toolMap[tool];
70-
List<String> toolArgs = <String>[];
71-
if (pathLib.extension(toolPath) == '.dart') {
70+
ToolDefinition toolDefinition = toolConfiguration.tools[tool];
71+
List<String> toolArgs = toolDefinition.command;
72+
if (pathLib.extension(toolDefinition.command.first) == '.dart') {
7273
// For dart tools, we want to invoke them with Dart.
73-
toolArgs = [toolPath];
74-
toolPath = Platform.resolvedExecutable;
74+
toolArgs.insert(0, Platform.resolvedExecutable);
7575
}
7676

7777
// Ideally, we would just be able to send the input text into stdin,
@@ -94,9 +94,10 @@ class ToolRunner {
9494
}
9595

9696
argsWithInput = toolArgs + argsWithInput;
97-
String commandString() => ([toolPath] + argsWithInput).join(' ');
97+
final String commandPath = argsWithInput.removeAt(0);
98+
String commandString() => ([commandPath] + argsWithInput).join(' ');
9899
try {
99-
ProcessResult result = Process.runSync(toolPath, argsWithInput);
100+
ProcessResult result = Process.runSync(commandPath, argsWithInput);
100101
if (result.exitCode != 0) {
101102
_error('Tool "$tool" returned non-zero exit code '
102103
'(${result.exitCode}) when run as '

test/tool_runner_test.dart

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,27 @@ library dartdoc.model_test;
66

77
import 'dart:io';
88

9+
import 'package:dartdoc/src/dartdoc_options.dart';
910
import 'package:dartdoc/src/tool_runner.dart';
1011
import 'package:path/path.dart' as pathLib;
1112
import 'package:test/test.dart';
1213

1314
import 'src/utils.dart' as utils;
1415

1516
void main() {
16-
Map<String, String> toolMap = {
17-
'missing': '/a/missing/executable',
18-
'drill':
19-
pathLib.join(utils.testPackageDir.absolute.path, 'bin', 'drill.dart'),
17+
ToolConfiguration toolMap = ToolConfiguration.empty;
18+
19+
toolMap.tools.addAll({
20+
'missing': new ToolDefinition(['/a/missing/executable'], "missing"),
21+
'drill': new ToolDefinition(
22+
[pathLib.join(utils.testPackageDir.absolute.path, 'bin', 'drill.dart')],
23+
'Makes holes'),
2024
// We use the Dart executable for our "non-dart" tool
2125
// test, because it's the only executable that we know the
2226
// exact location of that works on all platforms.
23-
'non_dart': Platform.resolvedExecutable,
24-
};
27+
'non_dart':
28+
new ToolDefinition([Platform.resolvedExecutable], 'non-dart tool'),
29+
});
2530
ToolRunner runner;
2631
final List<String> errors = <String>[];
2732

@@ -42,10 +47,7 @@ void main() {
4247
'TEST INPUT',
4348
);
4449
expect(errors, isEmpty);
45-
expect(
46-
result,
47-
contains(new RegExp(
48-
r'Args: \[--file=<INPUT_FILE>]')));
50+
expect(result, contains(new RegExp(r'Args: \[--file=<INPUT_FILE>]')));
4951
expect(result, contains('## `TEST INPUT`'));
5052
});
5153
test('can invoke a non-Dart tool', () {

testing/test_package/dartdoc_options.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,11 @@ dartdoc:
88
markdown: "Unreal.md"
99
Real Libraries:
1010
tools:
11-
drill: "bin/drill.dart"
11+
drill:
12+
command: ["bin/drill.dart"]
13+
description: "Puts holes in things."
14+
echo:
15+
macos: ['/bin/sh', '-c', 'echo']
16+
linux: ['/bin/sh', '-c', 'echo']
17+
windows: ['C:\\Windows\\System32\\cmd.exe', '/c', 'echo']
18+
description: 'Works on everything'

0 commit comments

Comments
 (0)