diff --git a/lib/src/command/deps.dart b/lib/src/command/deps.dart index f931c5985..d8a86a794 100644 --- a/lib/src/command/deps.dart +++ b/lib/src/command/deps.dart @@ -4,12 +4,26 @@ 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 '../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"; @@ -36,6 +50,9 @@ class DepsCommand extends PubCommand { negatable: true, help: "Whether to include dev dependencies.", defaultsTo: true); + + argParser.addFlag("executables", + negatable: false, help: "List all available executables."); } void run() { @@ -44,18 +61,22 @@ class DepsCommand extends PubCommand { _buffer = new StringBuffer(); - _buffer.writeln(_labelPackage(entrypoint.root)); - - switch (argResults["style"]) { - case "compact": - _outputCompact(); - break; - case "list": - _outputList(); - break; - case "tree": - _outputTree(); - break; + if (argResults['executables']) { + _outputExecutables(); + } else { + _buffer.writeln(_labelPackage(entrypoint.root)); + + switch (argResults["style"]) { + case "compact": + _outputCompact(); + break; + case "list": + _outputList(); + break; + case "tree": + _outputTree(); + break; + } } log.message(_buffer); @@ -231,4 +252,60 @@ class DepsCommand extends PubCommand { '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) + .map((dep) => entrypoint.packageGraph.packages[dep.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. + List _getExecutablesFor(Package package) => package.executableIds + .where((e) => _isDartExecutable(p.absolute(package.dir, e.path))) + .map((e) => p.basenameWithoutExtension(e.path)); + + /// 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 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(', ')}'; + } } diff --git a/test/deps/executables_test.dart b/test/deps/executables_test.dart new file mode 100644 index 000000000..317ab4970 --- /dev/null +++ b/test/deps/executables_test.dart @@ -0,0 +1,225 @@ +// Copyright (c) 2016, 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 'package:test/test.dart'; + +import '../descriptor.dart' as d; +import '../test_pub.dart'; + +const _validMain = 'main() {}'; + +main() { + _testExecutablesOutput(output, {bool dev: true}) => () async { + await pubGet(); + await runPub( + args: ['deps', '--executables'] + ..addAll(dev ? ['--dev'] : ['--no-dev']), + output: output); + }; + + _testAllDepsOutput(output) => _testExecutablesOutput(output); + _testNonDevDepsOutput(output) => _testExecutablesOutput(output, dev: false); + + group("lists nothing when no executables found", () { + setUp(() async { + await d.dir(appPath, [d.appPubspec()]).create(); + }); + + test("all dependencies", _testAllDepsOutput('\n')); + test("non-dev dependencies", _testNonDevDepsOutput('\n')); + }); + + group("skips non-Dart executables", () { + setUp(() async { + await d.dir(appPath, [ + d.appPubspec(), + d.dir('bin', [d.file('foo.py'), d.file('bar.sh')]) + ]).create(); + }); + + test("all dependencies", _testAllDepsOutput('\n')); + test("non-dev dependencies", _testNonDevDepsOutput('\n')); + }); + + group("skips Dart executables which are not parsable", () { + setUp(() async { + await d.dir(appPath, [ + d.appPubspec(), + d.dir('bin', [d.file('foo.dart', 'main() {')]) + ]).create(); + }); + + test("all dependencies", _testAllDepsOutput('\n')); + test("non-dev dependencies", _testNonDevDepsOutput('\n')); + }); + + group("skips Dart executables without entrypoints", () { + setUp(() async { + await d.dir(appPath, [ + d.appPubspec(), + d.dir( + 'bin', [d.file('foo.dart'), d.file('bar.dart', 'main(x, y, z) {}')]) + ]).create(); + }); + + test("all dependencies", _testAllDepsOutput('\n')); + test("non-dev dependencies", _testNonDevDepsOutput('\n')); + }); + + group("lists valid Dart executables with entrypoints", () { + setUp(() async { + await d.dir(appPath, [ + d.appPubspec(), + d.dir('bin', + [d.file('foo.dart', _validMain), d.file('bar.dart', _validMain)]) + ]).create(); + }); + + test("all dependencies", _testAllDepsOutput('myapp: bar, foo')); + test("non-dev dependencies", _testNonDevDepsOutput('myapp: bar, foo')); + }); + + group("skips executables in sub directories", () { + setUp(() async { + await d.dir(appPath, [ + d.appPubspec(), + d.dir('bin', [ + d.file('foo.dart', _validMain), + d.dir('sub', [d.file('bar.dart', _validMain)]) + ]) + ]).create(); + }); + + test("all dependencies", _testAllDepsOutput('myapp:foo')); + test("non-dev dependencies", _testNonDevDepsOutput('myapp:foo')); + }); + + group("lists executables from a dependency", () { + setUp(() async { + await d.dir('foo', [ + d.libPubspec('foo', '1.0.0'), + d.dir('bin', [d.file('bar.dart', _validMain)]) + ]).create(); + + await d.dir(appPath, [ + d.appPubspec({ + 'foo': {'path': '../foo'} + }) + ]).create(); + }); + + test("all dependencies", _testAllDepsOutput('foo:bar')); + test("non-dev dependencies", _testNonDevDepsOutput('foo:bar')); + }); + + group("lists executables only from immediate dependencies", () { + setUp(() async { + await d.dir(appPath, [ + d.appPubspec({ + 'foo': {'path': '../foo'} + }) + ]).create(); + + await d.dir('foo', [ + d.libPubspec('foo', '1.0.0', deps: { + 'baz': {'path': '../baz'} + }), + d.dir('bin', [d.file('bar.dart', _validMain)]) + ]).create(); + + await d.dir('baz', [ + d.libPubspec('baz', '1.0.0'), + d.dir('bin', [d.file('qux.dart', _validMain)]) + ]).create(); + }); + + test("all dependencies", _testAllDepsOutput('foo:bar')); + test("non-dev dependencies", _testNonDevDepsOutput('foo:bar')); + }); + + group("applies formatting before printing executables", () { + setUp(() async { + await d.dir(appPath, [ + d.appPubspec({ + 'foo': {'path': '../foo'}, + 'bar': {'path': '../bar'} + }), + d.dir('bin', [d.file('myapp.dart', _validMain)]) + ]).create(); + + await d.dir('foo', [ + d.libPubspec('foo', '1.0.0'), + d.dir('bin', + [d.file('baz.dart', _validMain), d.file('foo.dart', _validMain)]) + ]).create(); + + await d.dir('bar', [ + d.libPubspec('bar', '1.0.0'), + d.dir('bin', [d.file('qux.dart', _validMain)]) + ]).create(); + }); + + test("all dependencies", _testAllDepsOutput(''' + myapp + bar:qux + foo: foo, baz''')); + test("non-dev dependencies", _testNonDevDepsOutput(''' + myapp + bar:qux + foo: foo, baz''')); + }); + + group("dev dependencies", () { + setUp(() async { + await d.dir('foo', [ + d.libPubspec('foo', '1.0.0'), + d.dir('bin', [d.file('bar.dart', _validMain)]) + ]).create(); + + await d.dir(appPath, [ + d.pubspec({ + 'name': 'myapp', + 'dev_dependencies': { + 'foo': {'path': '../foo'} + } + }) + ]).create(); + }); + + test("are listed if --dev flag is set", _testAllDepsOutput('foo:bar')); + test("are skipped if --no-dev flag is set", _testNonDevDepsOutput('\n')); + }); + + group("overriden dependencies executables", () { + setUp(() async { + await d.dir('foo-1.0', [ + d.libPubspec('foo', '1.0.0'), + d.dir('bin', [d.file('bar.dart', _validMain)]) + ]).create(); + + await d.dir('foo-2.0', [ + d.libPubspec('foo', '2.0.0'), + d.dir('bin', + [d.file('bar.dart', _validMain), d.file('baz.dart', _validMain)]) + ]).create(); + + await d.dir(appPath, [ + d.pubspec({ + 'name': 'myapp', + 'dependencies': { + 'foo': {'path': '../foo-1.0'} + }, + 'dependency_overrides': { + 'foo': {'path': '../foo-2.0'} + } + }) + ]).create(); + }); + + test( + 'are listed if --dev flag is set', _testAllDepsOutput('foo: bar, baz')); + test('are listed if --no-dev flag is set', + _testNonDevDepsOutput('foo: bar, baz')); + }); +}