diff --git a/lib/dartdoc.dart b/lib/dartdoc.dart index b8bb3dcf87..521116bcc4 100644 --- a/lib/dartdoc.dart +++ b/lib/dartdoc.dart @@ -23,6 +23,7 @@ import 'package:dartdoc/src/logging.dart'; import 'package:dartdoc/src/markdown_processor.dart' show markdownStats; import 'package:dartdoc/src/model/model.dart'; import 'package:dartdoc/src/package_meta.dart'; +import 'package:dartdoc/src/tool_definition.dart'; import 'package:dartdoc/src/tool_runner.dart'; import 'package:dartdoc/src/tuple.dart'; import 'package:dartdoc/src/utils.dart'; diff --git a/lib/src/dartdoc_options.dart b/lib/src/dartdoc_options.dart index c0204f6eaf..0e21dd6ff3 100644 --- a/lib/src/dartdoc_options.dart +++ b/lib/src/dartdoc_options.dart @@ -24,10 +24,10 @@ import 'package:dartdoc/dartdoc.dart'; import 'package:dartdoc/src/experiment_options.dart'; import 'package:dartdoc/src/io_utils.dart'; import 'package:dartdoc/src/source_linker.dart'; +import 'package:dartdoc/src/tool_definition.dart'; import 'package:dartdoc/src/tool_runner.dart'; -import 'package:dartdoc/src/tuple.dart'; import 'package:dartdoc/src/warnings.dart'; -import 'package:path/path.dart' as p show Context, canonicalize, extension; +import 'package:path/path.dart' as p show Context, canonicalize; import 'package:yaml/yaml.dart'; /// Constants to help with type checking, because T is int and so forth @@ -39,7 +39,7 @@ const int _kIntVal = 0; const double _kDoubleVal = 0.0; const bool _kBoolVal = true; -const String _kCompileArgsTagName = 'compile_args'; +const String compileArgsTagName = 'compile_args'; int get _usageLineLength => stdout.hasTerminal ? stdout.terminalColumns : null; @@ -114,219 +114,6 @@ class CategoryConfiguration { } } -/// Defines the attributes of a tool in the options file, corresponding to -/// the 'tools' keyword in the options file, and populated by the -/// [ToolConfiguration] class. -class ToolDefinition { - /// A list containing the command and options to be run for this tool. The - /// first argument in the command is the tool executable, and will have its - /// path evaluated relative to the `dartdoc_options.yaml` location. Must not - /// be an empty list, or be null. - final List command; - - /// A list containing the command and options to setup phase for this tool. - /// The first argument in the command is the tool executable, and will have - /// its path evaluated relative to the `dartdoc_options.yaml` location. May - /// be null or empty, in which case it will be ignored at setup time. - final List setupCommand; - - /// A description of the defined tool. Must not be null. - final String description; - - /// If set, then the setup command has been run once for this tool definition. - bool setupComplete = false; - - /// Returns true if the given executable path has an extension recognized as a - /// Dart extension (e.g. '.dart' or '.snapshot'). - static bool isDartExecutable(String executable) { - var extension = p.extension(executable); - return extension == '.dart' || extension == '.snapshot'; - } - - /// Creates a ToolDefinition or subclass that is appropriate for the command - /// given. - factory ToolDefinition.fromCommand( - List command, - List setupCommand, - String description, - ResourceProvider resourceProvider, - {List compileArgs}) { - assert(command != null); - assert(command.isNotEmpty); - assert(description != null); - if (isDartExecutable(command[0])) { - return DartToolDefinition( - command, setupCommand, description, resourceProvider, - compileArgs: compileArgs ?? const []); - } else { - if (compileArgs != null && compileArgs.isNotEmpty) { - throw DartdocOptionError( - 'Compile arguments may only be specified for Dart tools, but ' - '$_kCompileArgsTagName of $compileArgs were specified for ' - '$command.'); - } - return ToolDefinition(command, setupCommand, description); - } - } - - ToolDefinition(this.command, this.setupCommand, this.description) - : assert(command != null), - assert(command.isNotEmpty), - assert(description != null); - - @override - String toString() { - final commandString = - '${this is DartToolDefinition ? '(Dart) ' : ''}"${command.join(' ')}"'; - if (setupCommand == null) { - return '$runtimeType: $commandString ($description)'; - } else { - return '$runtimeType: $commandString, with setup command ' - '"${setupCommand.join(' ')}" ($description)'; - } - } -} - -/// Manages the creation of a single snapshot file in a context where multiple -/// async functions could be trying to use and/or create it. -/// -/// To use: -/// -/// var s = new Snapshot(...); -/// -/// if (s.needsSnapshot) { -/// // create s.snapshotFile, then call: -/// s.snapshotCompleted(); -/// } else { -/// await snapshotValid(); -/// // use existing s.snapshotFile; -/// } -/// -class Snapshot { - File _snapshotFile; - - // TODO(srawlins): Deprecate this public getter; change private field to just - // be the absolute path. - File get snapshotFile => _snapshotFile; - final Completer _snapshotCompleter = Completer(); - - Snapshot(Folder snapshotCache, String toolPath, int serial, - ResourceProvider resourceProvider) { - if (toolPath.endsWith('.snapshot')) { - _needsSnapshot = false; - _snapshotFile = resourceProvider.getFile(toolPath); - snapshotCompleted(); - } else { - _snapshotFile = resourceProvider.getFile(resourceProvider.pathContext - .join(resourceProvider.pathContext.absolute(snapshotCache.path), - 'snapshot_$serial')); - } - } - - bool _needsSnapshot = true; - - /// Will return true precisely once, unless [snapshotFile] was already a - /// snapshot. In that case, will always return false. - bool get needsSnapshot { - if (_needsSnapshot == true) { - _needsSnapshot = false; - return true; - } - return _needsSnapshot; - } - - Future snapshotValid() => _snapshotCompleter.future; - - void snapshotCompleted() => _snapshotCompleter.complete(); -} - -/// A singleton that keeps track of cached snapshot files. The [dispose] -/// function must be called before process exit to clean up snapshots in the -/// cache. -class SnapshotCache { - static SnapshotCache _instance; - - // TODO(srawlins): Make this final. - Folder snapshotCache; - final ResourceProvider _resourceProvider; - final Map snapshots = {}; - int _serial = 0; - - SnapshotCache._(this._resourceProvider) - : snapshotCache = - _resourceProvider.createSystemTemp('dartdoc_snapshot_cache_'); - - static SnapshotCache get instance => _instance; - - static SnapshotCache createInstance(ResourceProvider resourceProvider) => - _instance ??= SnapshotCache._(resourceProvider); - - Snapshot getSnapshot(String toolPath) { - if (snapshots.containsKey(toolPath)) { - return snapshots[toolPath]; - } - snapshots[toolPath] = - Snapshot(snapshotCache, toolPath, _serial, _resourceProvider); - _serial++; - return snapshots[toolPath]; - } - - void dispose() { - _instance = null; - if (snapshotCache != null && snapshotCache.exists) { - return snapshotCache.delete(); - } - return null; - } -} - -/// A special kind of tool definition for Dart commands. -class DartToolDefinition extends ToolDefinition { - final ResourceProvider _resourceProvider; - - /// A list of arguments to add to the snapshot compilation arguments. - final List compileArgs; - - /// Takes a list of args to modify, and returns the name of the executable - /// to run. If no snapshot file existed, then create one and modify the args - /// so that if they are executed with dart, will result in the snapshot being - /// built. - Future> modifyArgsToCreateSnapshotIfNeeded( - List args) async { - assert(args[0] == command.first); - // Set up flags to create a new snapshot, if needed, and use the first run - // as the training run. - SnapshotCache.createInstance(_resourceProvider); - var snapshot = SnapshotCache.instance.getSnapshot(command.first); - var snapshotFile = snapshot.snapshotFile; - var needsSnapshot = snapshot.needsSnapshot; - if (needsSnapshot) { - args.insertAll(0, [ - // TODO(jcollins-g): remove ignore and verbosity resets once - // https://dart-review.googlesource.com/c/sdk/+/181421 is safely - // in the rearview mirror in dev/Flutter. - '--ignore-unrecognized-flags', - '--verbosity=error', - '--snapshot=${_resourceProvider.pathContext.absolute(snapshotFile.path)}', - '--snapshot_kind=app-jit', - ...compileArgs, - ]); - } else { - await snapshot.snapshotValid(); - // replace the first argument with the path to the snapshot. - args[0] = _resourceProvider.pathContext.absolute(snapshotFile.path); - } - return Tuple2(_resourceProvider.resolvedExecutable, - needsSnapshot ? snapshot.snapshotCompleted : null); - } - - DartToolDefinition(List command, List setupCommand, - String description, this._resourceProvider, - {this.compileArgs = const []}) - : assert(compileArgs != null), - super(command, setupCommand, description); -} - /// A configuration class that can interpret [ToolDefinition]s from a YAML map. class ToolConfiguration { final Map tools; @@ -396,17 +183,17 @@ class ToolConfiguration { List findArgs() { List args; - if (toolMap.containsKey(_kCompileArgsTagName)) { - var compileArgs = toolMap[_kCompileArgsTagName]; + if (toolMap.containsKey(compileArgsTagName)) { + var compileArgs = toolMap[compileArgsTagName]; if (compileArgs is String) { - args = [toolMap[_kCompileArgsTagName].toString()]; + args = [toolMap[compileArgsTagName].toString()]; } else if (compileArgs is YamlList) { args = compileArgs.map((node) => node.toString()).toList(); } else { throw DartdocOptionError( 'Tool compile arguments must be a list of strings. The tool ' - '$name has a $_kCompileArgsTagName entry that is a ' + '$name has a $compileArgsTagName entry that is a ' '${compileArgs.runtimeType}'); } } diff --git a/lib/src/tool_definition.dart b/lib/src/tool_definition.dart new file mode 100644 index 0000000000..629f8bf326 --- /dev/null +++ b/lib/src/tool_definition.dart @@ -0,0 +1,247 @@ +// Copyright (c) 2021, 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:async'; + +import 'package:analyzer/file_system/file_system.dart'; +import 'package:dartdoc/src/dartdoc_options.dart'; +import 'package:dartdoc/src/io_utils.dart'; +import 'package:path/path.dart' as p show extension; + +/// Defines the attributes of a tool in the options file, corresponding to +/// the 'tools' keyword in the options file, and populated by the +/// [ToolConfiguration] class. +class ToolDefinition { + /// A list containing the command and options to be run for this tool. The + /// first argument in the command is the tool executable, and will have its + /// path evaluated relative to the `dartdoc_options.yaml` location. Must not + /// be an empty list, or be null. + final List command; + + /// A list containing the command and options to setup phase for this tool. + /// The first argument in the command is the tool executable, and will have + /// its path evaluated relative to the `dartdoc_options.yaml` location. May + /// be null or empty, in which case it will be ignored at setup time. + final List setupCommand; + + /// A description of the defined tool. Must not be null. + final String description; + + /// If set, then the setup command has been run once for this tool definition. + bool setupComplete = false; + + /// Returns true if the given executable path has an extension recognized as a + /// Dart extension (e.g. '.dart' or '.snapshot'). + static bool isDartExecutable(String executable) { + var extension = p.extension(executable); + return extension == '.dart' || extension == '.snapshot'; + } + + /// Creates a ToolDefinition or subclass that is appropriate for the command + /// given. + factory ToolDefinition.fromCommand( + List command, + List setupCommand, + String description, + ResourceProvider resourceProvider, + {List compileArgs}) { + assert(command != null); + assert(command.isNotEmpty); + assert(description != null); + if (isDartExecutable(command[0])) { + return DartToolDefinition( + command, setupCommand, description, resourceProvider, + compileArgs: compileArgs ?? const []); + } else { + if (compileArgs != null && compileArgs.isNotEmpty) { + throw DartdocOptionError( + 'Compile arguments may only be specified for Dart tools, but ' + '$compileArgsTagName of $compileArgs were specified for ' + '$command.'); + } + return ToolDefinition(command, setupCommand, description); + } + } + + ToolDefinition(this.command, this.setupCommand, this.description) + : assert(command != null), + assert(command.isNotEmpty), + assert(description != null); + + @override + String toString() { + final commandString = + '${this is DartToolDefinition ? '(Dart) ' : ''}"${command.join(' ')}"'; + if (setupCommand == null) { + return '$runtimeType: $commandString ($description)'; + } else { + return '$runtimeType: $commandString, with setup command ' + '"${setupCommand.join(' ')}" ($description)'; + } + } + + Future toolStateForArgs(List args) async { + var commandPath = args.removeAt(0); + return ToolStateForArgs(commandPath, args, null); + } +} + +/// A special kind of tool definition for Dart commands. +class DartToolDefinition extends ToolDefinition { + final ResourceProvider _resourceProvider; + + /// A list of arguments to add to the snapshot compilation arguments. + final List compileArgs; + + /// Takes a list of args to modify, and returns the name of the executable + /// to run. + /// + /// If no snapshot file existed, then creates one and modify the args + /// so that if they are executed with dart, will result in the snapshot being + /// built. + @override + Future toolStateForArgs(List args) async { + assert(args[0] == command.first); + // Set up flags to create a new snapshot, if needed, and use the first run + // as the training run. + SnapshotCache.createInstance(_resourceProvider); + var snapshot = SnapshotCache.instance.getSnapshot(command.first); + var snapshotPath = + _resourceProvider.pathContext.absolute(snapshot._snapshotFile.path); + var needsSnapshot = snapshot.needsSnapshot; + if (needsSnapshot) { + return ToolStateForArgs( + _resourceProvider.resolvedExecutable, + [ + // TODO(jcollins-g): remove ignore and verbosity resets once + // https://dart-review.googlesource.com/c/sdk/+/181421 is safely + // in the rearview mirror in dev/Flutter. + '--ignore-unrecognized-flags', + '--verbosity=error', + '--snapshot=$snapshotPath', + '--snapshot_kind=app-jit', + ...compileArgs, + ...args, + ], + snapshot._snapshotCompleted); + } else { + await snapshot._snapshotValid(); + // replace the first argument with the path to the snapshot. + args[0] = snapshotPath; + return ToolStateForArgs(_resourceProvider.resolvedExecutable, args, null); + } + } + + DartToolDefinition(List command, List setupCommand, + String description, this._resourceProvider, + {this.compileArgs = const []}) + : assert(compileArgs != null), + super(command, setupCommand, description); +} + +/// Manages the creation of a single snapshot file in a context where multiple +/// async functions could be trying to use and/or create it. +/// +/// To use: +/// +/// ```dart +/// var s = new Snapshot(...); +/// +/// if (s.needsSnapshot) { +/// // create s.snapshotFile, then call: +/// s.snapshotCompleted(); +/// } else { +/// await snapshotValid(); +/// // use existing s.snapshotFile; +/// } +/// ``` +/// +class _Snapshot { + final File _snapshotFile; + + final Completer _snapshotCompleter = Completer(); + + factory _Snapshot(Folder snapshotCache, String toolPath, int serial, + ResourceProvider resourceProvider) { + if (toolPath.endsWith('.snapshot')) { + return _Snapshot.existing(toolPath, resourceProvider); + } else { + return _Snapshot.create(resourceProvider.getFile( + resourceProvider.pathContext.join( + resourceProvider.pathContext.absolute(snapshotCache.path), + 'snapshot_$serial'))); + } + } + + _Snapshot.existing(String toolPath, ResourceProvider resourceProvider) + : _needsSnapshot = false, + _snapshotFile = resourceProvider.getFile(toolPath) { + _snapshotCompleted(); + } + + _Snapshot.create(this._snapshotFile); + + bool _needsSnapshot = true; + + /// Will return true precisely once, unless [snapshotFile] was already a + /// snapshot. In that case, will always return false. + bool get needsSnapshot { + if (_needsSnapshot == true) { + _needsSnapshot = false; + return true; + } + return _needsSnapshot; + } + + Future _snapshotValid() => _snapshotCompleter.future; + + void _snapshotCompleted() => _snapshotCompleter.complete(); +} + +/// A singleton that keeps track of cached snapshot files. The [dispose] +/// function must be called before process exit to clean up snapshots in the +/// cache. +class SnapshotCache { + static SnapshotCache _instance; + + final Folder snapshotCache; + final ResourceProvider _resourceProvider; + final Map snapshots = {}; + int _serial = 0; + + SnapshotCache._(this._resourceProvider) + : snapshotCache = + _resourceProvider.createSystemTemp('dartdoc_snapshot_cache_'); + + static SnapshotCache get instance => _instance; + + static SnapshotCache createInstance(ResourceProvider resourceProvider) => + _instance ??= SnapshotCache._(resourceProvider); + + _Snapshot getSnapshot(String toolPath) { + if (snapshots.containsKey(toolPath)) { + return snapshots[toolPath]; + } + snapshots[toolPath] = + _Snapshot(snapshotCache, toolPath, _serial, _resourceProvider); + _serial++; + return snapshots[toolPath]; + } + + void dispose() { + _instance = null; + if (snapshotCache != null && snapshotCache.exists) { + return snapshotCache.delete(); + } + return null; + } +} + +class ToolStateForArgs { + final String commandPath; + final List args; + final void Function() onProcessComplete; + + ToolStateForArgs(this.commandPath, this.args, this.onProcessComplete); +} diff --git a/lib/src/tool_runner.dart b/lib/src/tool_runner.dart index b28be74c43..ab05d206b5 100644 --- a/lib/src/tool_runner.dart +++ b/lib/src/tool_runner.dart @@ -8,6 +8,7 @@ import 'dart:io' show Process, ProcessException; import 'package:analyzer/file_system/file_system.dart'; import 'package:dartdoc/src/io_utils.dart'; +import 'package:dartdoc/src/tool_definition.dart'; import 'package:path/path.dart' as p; import 'dartdoc_options.dart'; @@ -206,16 +207,10 @@ class ToolRunner { } argsWithInput = toolArgs + argsWithInput; - String commandPath; - void Function() callCompleter; - if (toolDefinition is DartToolDefinition) { - var modified = await toolDefinition - .modifyArgsToCreateSnapshotIfNeeded(argsWithInput); - commandPath = modified.item1; - callCompleter = modified.item2; - } else { - commandPath = argsWithInput.removeAt(0); - } + var toolStateForArgs = await toolDefinition.toolStateForArgs(argsWithInput); + var commandPath = toolStateForArgs.commandPath; + argsWithInput = toolStateForArgs.args; + var callCompleter = toolStateForArgs.onProcessComplete; if (callCompleter != null) { return _runProcess(tool, content, commandPath, argsWithInput, diff --git a/test/tool_runner_test.dart b/test/tool_runner_test.dart index f49af7840e..562d3faa23 100644 --- a/test/tool_runner_test.dart +++ b/test/tool_runner_test.dart @@ -8,6 +8,7 @@ import 'dart:io'; import 'package:dartdoc/src/dartdoc_options.dart'; import 'package:dartdoc/src/package_meta.dart'; +import 'package:dartdoc/src/tool_definition.dart'; import 'package:dartdoc/src/tool_runner.dart'; import 'package:path/path.dart' as path; import 'package:test/test.dart';