blob: 79463fc029a1fb2200699d9de93f4ea1bfc967c8 [file] [log] [blame]
// Copyright (c) 2014, 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:collection';
import 'dart:convert';
import '../ascii_tree.dart' as tree;
import '../command.dart';
import '../command_runner.dart';
import '../dart.dart';
import '../log.dart' as log;
import '../package.dart';
import '../sdk.dart';
import '../utils.dart';
/// Handles the `deps` pub command.
class DepsCommand extends PubCommand {
@override
String get name => 'deps';
@override
String get description => 'Print package dependencies.';
@override
String get argumentsDescription => '[arguments...]';
@override
String get docUrl => 'https://dart.dev/tools/pub/cmd/pub-deps';
@override
bool get takesArguments => false;
final AnalysisContextManager analysisContextManager =
AnalysisContextManager();
/// The [StringBuffer] used to accumulate the output.
// TODO(sigurdm): use a local variable for this.
final _buffer = StringBuffer();
/// Whether to include dev dependencies.
bool get _includeDev => argResults['dev'];
DepsCommand() {
argParser.addOption('style',
abbr: 's',
help: 'How output should be displayed.',
allowed: ['compact', 'tree', 'list'],
defaultsTo: 'tree');
argParser.addFlag('dev',
help: 'Whether to include dev dependencies.', defaultsTo: true);
argParser.addFlag('executables',
negatable: false, help: 'List all available executables.');
argParser.addFlag('json',
negatable: false,
help: 'Output dependency information in a json format.');
argParser.addOption('directory',
abbr: 'C', help: 'Run this in the directory<dir>.', valueHelp: 'dir');
}
@override
Future<void> runProtected() async {
// Explicitly Run this in the directorycase we don't access `entrypoint.packageGraph`.
entrypoint.assertUpToDate();
_buffer.clear();
if (argResults['json']) {
if (argResults.wasParsed('dev')) {
usageException(
'Cannot combine --json and --dev.\nThe json output contains the dependency type in the output.');
}
if (argResults.wasParsed('executables')) {
usageException(
'Cannot combine --json and --executables.\nThe json output always lists available executables.');
}
if (argResults.wasParsed('style')) {
usageException('Cannot combine --json and --style.');
}
final visited = <String>[];
final toVisit = [entrypoint.root.name];
final packagesJson = <dynamic>[];
while (toVisit.isNotEmpty) {
final current = toVisit.removeLast();
if (visited.contains(current)) continue;
visited.add(current);
final currentPackage = entrypoint.packageGraph.packages[current]!;
final next = (current == entrypoint.root.name
? entrypoint.root.immediateDependencies
: currentPackage.dependencies)
.keys
.toList();
final dependencyType = entrypoint.root.dependencyType(current);
final kind = currentPackage == entrypoint.root
? 'root'
: (dependencyType == DependencyType.direct
? 'direct'
: (dependencyType == DependencyType.dev
? 'dev'
: 'transitive'));
final source =
entrypoint.packageGraph.lockFile.packages[current]?.source?.name ??
'root';
packagesJson.add({
'name': current,
'version': currentPackage.version.toString(),
'kind': kind,
'source': source,
'dependencies': next
});
toVisit.addAll(next);
}
var executables = [
for (final package in [
entrypoint.root,
...entrypoint.root.immediateDependencies.keys
.map((name) => entrypoint.packageGraph.packages[name])
])
...package!.executableNames.map((name) => package == entrypoint.root
? ':$name'
: (package.name == name ? name : '${package.name}:$name'))
];
_buffer.writeln(
JsonEncoder.withIndent(' ').convert(
{
'root': entrypoint.root.name,
'packages': packagesJson,
'sdks': [
for (var sdk in sdks.values)
if (sdk.version != null)
{'name': sdk.name, 'version': sdk.version.toString()}
],
'executables': executables
},
),
);
} else {
if (argResults['executables']) {
_outputExecutables();
} else {
for (var sdk in sdks.values) {
if (!sdk.isAvailable) continue;
_buffer.writeln("${log.bold('${sdk.name} SDK')} ${sdk.version}");
}
_buffer.writeln(_labelPackage(entrypoint.root));
switch (argResults['style']) {
case 'compact':
_outputCompact();
break;
case 'list':
_outputList();
break;
case 'tree':
_outputTree();
break;
}
}
}
log.message(_buffer);
}
/// Outputs a list of all of the package's immediate, dev, override, and
/// transitive dependencies.
///
/// For each dependency listed, *that* package's immediate dependencies are
/// shown. Unlike [_outputList], this prints all of these dependencies on one
/// line.
void _outputCompact() {
var root = entrypoint.root;
_outputCompactPackages('dependencies', root.dependencies.keys);
if (_includeDev) {
_outputCompactPackages('dev dependencies', root.devDependencies.keys);
}
_outputCompactPackages(
'dependency overrides', root.dependencyOverrides.keys);
var transitive = _getTransitiveDependencies();
_outputCompactPackages('transitive dependencies', transitive);
}
/// Outputs one section of packages in the compact output.
void _outputCompactPackages(String section, Iterable<String> names) {
if (names.isEmpty) return;
_buffer.writeln();
_buffer.writeln('$section:');
for (var name in ordered(names)) {
var package = _getPackage(name);
_buffer.write('- ${_labelPackage(package)}');
if (package.dependencies.isEmpty) {
_buffer.writeln();
} else {
var depNames = package.dependencies.keys;
var depsList = "[${depNames.join(' ')}]";
_buffer.writeln(' ${log.gray(depsList)}');
}
}
}
/// Outputs a list of all of the package's immediate, dev, override, and
/// transitive dependencies.
///
/// For each dependency listed, *that* package's immediate dependencies are
/// shown.
void _outputList() {
var root = entrypoint.root;
_outputListSection('dependencies', root.dependencies.keys);
if (_includeDev) {
_outputListSection('dev dependencies', root.devDependencies.keys);
}
_outputListSection('dependency overrides', root.dependencyOverrides.keys);
var transitive = _getTransitiveDependencies();
if (transitive.isEmpty) return;
_outputListSection('transitive dependencies', ordered(transitive));
}
/// Outputs one section of packages in the list output.
void _outputListSection(String name, Iterable<String> deps) {
if (deps.isEmpty) return;
_buffer.writeln();
_buffer.writeln('$name:');
for (var name in deps) {
var package = _getPackage(name);
_buffer.writeln('- ${_labelPackage(package)}');
for (var dep in package.dependencies.values) {
_buffer
.writeln(' - ${log.bold(dep.name)} ${log.gray(dep.constraint)}');
}
}
}
/// Generates a dependency tree for the root package.
///
/// If a package is encountered more than once (i.e. a shared or circular
/// dependency), later ones are not traversed. This is done in breadth-first
/// fashion so that a package will always be expanded at the shallowest
/// depth that it appears at.
void _outputTree() {
// The work list for the breadth-first traversal. It contains the package
// being added to the tree, and the parent map that will receive that
// package.
var toWalk = Queue<Pair<Package, Map<String, Map>>>();
var visited = <String>{entrypoint.root.name};
// Start with the root dependencies.
var packageTree = <String, Map>{};
var immediateDependencies =
entrypoint.root.immediateDependencies.keys.toSet();
if (!_includeDev) {
immediateDependencies.removeAll(entrypoint.root.devDependencies.keys);
}
for (var name in immediateDependencies) {
toWalk.add(Pair(_getPackage(name), packageTree));
}
// Do a breadth-first walk to the dependency graph.
while (toWalk.isNotEmpty) {
var pair = toWalk.removeFirst();
var package = pair.first;
var map = pair.last;
if (visited.contains(package.name)) {
map[log.gray('${package.name}...')] = <String, Map>{};
continue;
}
visited.add(package.name);
// Populate the map with this package's dependencies.
var childMap = <String, Map>{};
map[_labelPackage(package)] = childMap;
for (var dep in package.dependencies.values) {
toWalk.add(Pair(_getPackage(dep.name), childMap));
}
}
_buffer.write(tree.fromMap(packageTree, showAllChildren: true));
}
String _labelPackage(Package package) =>
'${log.bold(package.name)} ${package.version}';
/// Gets the names of the non-immediate dependencies of the root package.
Set<String> _getTransitiveDependencies() {
var transitive = _getAllDependencies();
var root = entrypoint.root;
transitive.remove(root.name);
transitive.removeAll(root.dependencies.keys);
if (_includeDev) {
transitive.removeAll(root.devDependencies.keys);
}
transitive.removeAll(root.dependencyOverrides.keys);
return transitive;
}
Set<String> _getAllDependencies() {
if (_includeDev) return entrypoint.packageGraph.packages.keys.toSet();
var nonDevDependencies = entrypoint.root.dependencies.keys.toList()
..addAll(entrypoint.root.dependencyOverrides.keys);
return nonDevDependencies
.expand((name) => entrypoint.packageGraph.transitiveDependencies(name))
.map((package) => package.name)
.toSet();
}
/// Get the package named [name], or throw a [DataError] if it's not
/// available.
///
/// It's very unlikely that the lockfile won't be up-to-date with the pubspec,
/// but it's possible, since [Entrypoint.assertUpToDate]'s modification time
/// check can return a false negative. This fails gracefully if that happens.
Package _getPackage(String name) {
var package = entrypoint.packageGraph.packages[name];
if (package != null) return package;
dataError('The pubspec.yaml file has changed since the pubspec.lock file '
'was generated, please run "$topLevelProgram pub get" again.');
}
/// Outputs all executables reachable from [entrypoint].
void _outputExecutables() {
var packages = [
entrypoint.root,
...(_includeDev
? entrypoint.root.immediateDependencies
: entrypoint.root.dependencies)
.keys
.map((name) => entrypoint.packageGraph.packages[name]!)
];
for (var package in packages) {
var executables = package.executableNames;
if (executables.isNotEmpty) {
_buffer.writeln(_formatExecutables(package.name, executables.toList()));
}
}
}
/// Returns formatted string that lists [executables] for the [packageName].
/// Examples:
///
/// _formatExecutables('foo', ['foo']) // -> 'foo'
/// _formatExecutables('foo', ['bar']) // -> 'foo:bar'
/// _formatExecutables('foo', ['bar', 'foo']) // -> 'foo: foo, bar'
///
/// Note the leading space before first executable and sorting order in the
/// last example.
String _formatExecutables(String packageName, List<String> executables) {
if (executables.length == 1) {
// If executable matches the package name omit the name of executable in
// the output.
return executables.first != packageName
? '$packageName:${log.bold(executables.first)}'
: log.bold(executables.first);
}
// Sort executables to make executable that matches the package name to be
// the first in the list.
executables.sort((e1, e2) {
if (e1 == packageName) {
return -1;
} else if (e2 == packageName) {
return 1;
} else {
return e1.compareTo(e2);
}
});
return '$packageName: ${executables.map(log.bold).join(', ')}';
}
}