blob: 313d8a52fec89f5eb2a62277dc10cf269ea50550 [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 '../entrypoint.dart';
import '../log.dart' as log;
import '../package.dart';
import '../pubspec.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;
/// Whether to include dev dependencies.
bool get _includeDev => argResults.flag('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 {
final buffer = StringBuffer();
if (argResults.flag('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 workspacePackageNames = [
...entrypoint.workspaceRoot.transitiveWorkspace.map((p) => p.name),
];
final toVisit = [...workspacePackageNames];
final packagesJson = <dynamic>[];
final graph = await entrypoint.packageGraph;
while (toVisit.isNotEmpty) {
final current = toVisit.removeLast();
if (visited.contains(current)) continue;
visited.add(current);
final currentPackage =
(await entrypoint.packageGraph).packages[current]!;
final isRoot = workspacePackageNames.contains(currentPackage.name);
final next = (isRoot
? currentPackage.immediateDependencies
: currentPackage.dependencies)
.keys
.toList();
final dependencyType =
entrypoint.workspaceRoot.pubspec.dependencyType(current);
final kind = isRoot
? 'root'
: (dependencyType == DependencyType.direct
? 'direct'
: (dependencyType == DependencyType.dev
? 'dev'
: 'transitive'));
final source =
entrypoint.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.workspaceRoot,
...entrypoint.workspaceRoot.immediateDependencies.keys
.map((name) => graph.packages[name]),
])
...package!.executableNames.map(
(name) => package == entrypoint.workspaceRoot
? ':$name'
: (package.name == name ? name : '${package.name}:$name'),
),
];
buffer.writeln(
JsonEncoder.withIndent(' ').convert(
{
'root': entrypoint.workspaceRoot.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.flag('executables')) {
await _outputExecutables(buffer);
} else {
for (var sdk in sdks.values) {
if (!sdk.isAvailable) continue;
buffer.writeln("${log.bold('${sdk.name} SDK')} ${sdk.version}");
}
switch (argResults.optionWithDefault('style')) {
case 'compact':
await _outputCompact(buffer);
break;
case 'list':
await _outputList(buffer);
break;
case 'tree':
await _outputTree(buffer);
break;
}
}
}
log.message(buffer.toString());
}
/// 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.
Future<void> _outputCompact(
StringBuffer buffer,
) async {
var first = true;
for (final root in entrypoint.workspaceRoot.transitiveWorkspace) {
if (!first) {
buffer.write('\n');
}
first = false;
buffer.writeln(_labelPackage(root));
await _outputCompactPackages(
'dependencies',
root.dependencies.keys,
buffer,
);
if (_includeDev) {
await _outputCompactPackages(
'dev dependencies',
root.devDependencies.keys,
buffer,
);
}
await _outputCompactPackages(
'dependency overrides',
root.dependencyOverrides.keys,
buffer,
);
}
var transitive = await _getTransitiveDependencies();
await _outputCompactPackages('transitive dependencies', transitive, buffer);
}
/// Outputs one section of packages in the compact output.
Future<void> _outputCompactPackages(
String section,
Iterable<String> names,
StringBuffer buffer,
) async {
if (names.isEmpty) return;
buffer.writeln();
buffer.writeln('$section:');
for (var name in ordered(names)) {
var package = await _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.
Future<void> _outputList(StringBuffer buffer) async {
var first = true;
for (final root in entrypoint.workspaceRoot.transitiveWorkspace) {
if (!first) {
buffer.write('\n');
}
first = false;
buffer.writeln(_labelPackage(root));
await _outputListSection('dependencies', root.dependencies.keys, buffer);
if (_includeDev) {
await _outputListSection(
'dev dependencies',
root.devDependencies.keys,
buffer,
);
}
await _outputListSection(
'dependency overrides',
root.dependencyOverrides.keys,
buffer,
);
}
var transitive = await _getTransitiveDependencies();
if (transitive.isEmpty) return;
await _outputListSection(
'transitive dependencies',
ordered(transitive),
buffer,
);
}
/// Outputs one section of packages in the list output.
Future<void> _outputListSection(
String name,
Iterable<String> deps,
StringBuffer buffer,
) async {
if (deps.isEmpty) return;
buffer.writeln();
buffer.writeln('$name:');
for (var name in deps) {
var package = await _getPackage(name);
buffer.writeln('- ${_labelPackage(package)}');
for (var dep in package.dependencies.values) {
buffer.writeln(
' - ${log.bold(dep.name)} ${log.gray(dep.constraint.toString())}',
);
}
}
}
/// 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.
Future<void> _outputTree(
StringBuffer buffer,
) async {
// 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<(Package, Map<String, Map>)>();
var visited = <String>{};
// Start with the root dependencies.
var packageTree = <String, Map>{};
final workspacePackageNames = [
...entrypoint.workspaceRoot.transitiveWorkspace.map((p) => p.name),
];
var immediateDependencies =
entrypoint.workspaceRoot.immediateDependencies.keys.toSet();
if (!_includeDev) {
immediateDependencies
.removeAll(entrypoint.workspaceRoot.devDependencies.keys);
}
for (var name in workspacePackageNames) {
toWalk.add((await _getPackage(name), packageTree));
}
// Do a breadth-first walk to the dependency graph.
while (toWalk.isNotEmpty) {
final (package, map) = toWalk.removeFirst();
if (!visited.add(package.name)) {
map[log.gray('${package.name}...')] = <String, Map>{};
continue;
}
// Populate the map with this package's dependencies.
var childMap = <String, Map>{};
map[_labelPackage(package)] = childMap;
final isRoot = workspacePackageNames.contains(package.name);
final children = [
...isRoot
? package.immediateDependencies.keys
: package.dependencies.keys,
];
if (!_includeDev) {
children.removeWhere(package.devDependencies.keys.contains);
}
for (var dep in children) {
toWalk.add((await _getPackage(dep), childMap));
}
}
buffer.write(tree.fromMap(packageTree));
}
String _labelPackage(Package package) =>
'${log.bold(package.name)} ${package.version}';
/// Gets the names of the non-immediate dependencies of the workspace packages.
Future<Set<String>> _getTransitiveDependencies() async {
var transitive = await _getAllDependencies();
for (final root in entrypoint.workspaceRoot.transitiveWorkspace) {
transitive.remove(root.name);
transitive.removeAll(root.dependencies.keys);
if (_includeDev) {
transitive.removeAll(root.devDependencies.keys);
}
transitive.removeAll(root.dependencyOverrides.keys);
}
return transitive;
}
Future<Set<String>> _getAllDependencies() async {
final graph = await entrypoint.packageGraph;
if (_includeDev) {
return graph.packages.keys.toSet();
}
var nonDevDependencies = [
for (final package in entrypoint.workspaceRoot.transitiveWorkspace) ...[
...package.dependencies.keys,
...package.dependencyOverrides.keys,
],
];
return nonDevDependencies
.expand(graph.transitiveDependencies)
.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.
Future<Package> _getPackage(String name) async {
var package = (await 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].
Future<void> _outputExecutables(StringBuffer buffer) async {
final graph = await entrypoint.packageGraph;
final packages = {
for (final p in entrypoint.workspaceRoot.transitiveWorkspace) ...[
graph.packages[p.name]!,
...(_includeDev ? p.immediateDependencies : p.dependencies)
.keys
.map((name) => graph.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(', ')}';
}
}