blob: 262beaa31eb9782f655d53c1e454babf536ce58d [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 'package:analyzer/analyzer.dart' as analyzer;
import 'package:path/path.dart' as p;
import '../ascii_tree.dart' as tree;
import '../command.dart';
import '../dart.dart';
import '../log.dart' as log;
import '../package.dart';
import '../sdk.dart';
import '../utils.dart';
/// Returns `true` if [path] looks like a Dart entrypoint.
bool _isDartExecutable(String path) {
try {
var unit = analyzer.parseDartFile(path, parseFunctionBodies: false);
return isEntrypoint(unit);
} on analyzer.AnalyzerErrorGroup {
return false;
}
}
/// Handles the `deps` pub command.
class DepsCommand extends PubCommand {
String get name => "deps";
String get description => "Print package dependencies.";
List<String> get aliases => const ["dependencies", "tab"];
String get invocation => "pub deps";
String get docUrl => "http://dartlang.org/tools/pub/cmd/pub-deps.html";
bool get takesArguments => false;
/// The [StringBuffer] used to accumulate the output.
StringBuffer _buffer;
/// 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",
negatable: true,
help: "Whether to include dev dependencies.",
defaultsTo: true);
argParser.addFlag("executables",
negatable: false, help: "List all available executables.");
}
void run() {
// Explicitly run this in case we don't access `entrypoint.packageGraph`.
entrypoint.assertUpToDate();
_buffer = new StringBuffer();
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.
_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.
_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 = new Queue<Pair<Package, Map<String, Map>>>();
var visited = new Set<String>.from([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(new 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(new 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 "pub get" again.');
return null;
}
/// Outputs all executables reachable from [entrypoint].
void _outputExecutables() {
var packages = []
..add(entrypoint.root)
..addAll((_includeDev
? entrypoint.root.immediateDependencies
: entrypoint.root.dependencies)
.keys
.map((name) => entrypoint.packageGraph.packages[name]));
for (var package in packages) {
var executables = _getExecutablesFor(package);
if (executables.isNotEmpty) {
_buffer.writeln(_formatExecutables(package.name, executables.toList()));
}
}
}
/// Lists all Dart files in the `bin` directory of the [package].
///
/// Returns file names without extensions.
Iterable<String> _getExecutablesFor(Package package) =>
package.executablePaths
.where((e) => _isDartExecutable(p.absolute(package.dir, e)))
.map((e) => p.basenameWithoutExtension(e));
/// 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(', ')}';
}
}