More details from getExecutableForCommand (#3186)
diff --git a/lib/pub.dart b/lib/pub.dart
index b2fed29..5b3054f 100644
--- a/lib/pub.dart
+++ b/lib/pub.dart
@@ -6,7 +6,11 @@
import 'src/command_runner.dart';
import 'src/pub_embeddable_command.dart';
export 'src/executable.dart'
- show getExecutableForCommand, CommandResolutionFailedException;
+ show
+ getExecutableForCommand,
+ CommandResolutionFailedException,
+ CommandResolutionIssue,
+ DartExecutableWithPackageConfig;
export 'src/pub_embeddable_command.dart' show PubAnalytics;
/// Returns a [Command] for pub functionality that can be used by an embedding
diff --git a/lib/src/executable.dart b/lib/src/executable.dart
index 8c7be58..2112acc 100644
--- a/lib/src/executable.dart
+++ b/lib/src/executable.dart
@@ -7,6 +7,7 @@
import 'dart:isolate';
import 'package:args/args.dart';
+import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;
import 'entrypoint.dart';
@@ -202,7 +203,23 @@
}
}
-/// Returns the path to dart program/snapshot to invoke for running [descriptor]
+/// The result of a `getExecutableForCommand` command resolution.
+@sealed
+class DartExecutableWithPackageConfig {
+ /// Can be a .dart file or a incremental snapshot.
+ final String executable;
+
+ /// The package_config.json to run [executable] with. Or <null> if the VM
+ /// should find it according to the standard rules.
+ final String? packageConfig;
+
+ DartExecutableWithPackageConfig({
+ required this.executable,
+ required this.packageConfig,
+ });
+}
+
+/// Returns the dart program/snapshot to invoke for running [descriptor]
/// resolved according to the package configuration of the package at [root]
/// (defaulting to the current working directory). Using the pub-cache at
/// [pubCacheDir] (defaulting to the default pub cache).
@@ -237,7 +254,7 @@
/// * `` and `:` both resolves to `<current>:bin/<current>.dart` or
/// `bin/<current>:main.dart`.
///
-/// If that doesn't resolve as an existing file throw an exception.
+/// If that doesn't resolve as an existing file, throw an exception.
///
/// ## Snapshotting
///
@@ -250,7 +267,7 @@
///
/// Throws an [CommandResolutionFailedException] if the command is not found or
/// if the entrypoint is not up to date (requires `pub get`) and a `pub get`.
-Future<String> getExecutableForCommand(
+Future<DartExecutableWithPackageConfig> getExecutableForCommand(
String descriptor, {
bool allowSnapshot = true,
String? root,
@@ -270,71 +287,127 @@
}
final asDirectFile = p.join(root, asPath);
- if (fileExists(asDirectFile)) return p.relative(asDirectFile, from: root);
- if (!fileExists(p.join(root, 'pubspec.yaml'))) {
- throw CommandResolutionFailedException('Could not find file `$descriptor`');
+ if (fileExists(asDirectFile)) {
+ return DartExecutableWithPackageConfig(
+ executable: p.relative(asDirectFile, from: root),
+ packageConfig: null,
+ );
}
+ if (!fileExists(p.join(root, 'pubspec.yaml'))) {
+ throw CommandResolutionFailedException._(
+ 'Could not find file `$descriptor`',
+ CommandResolutionIssue.fileNotFound);
+ }
+ final entrypoint = Entrypoint(root, SystemCache(rootDir: pubCacheDir));
try {
- final entrypoint = Entrypoint(root, SystemCache(rootDir: pubCacheDir));
+ // TODO(sigurdm): it would be nicer with a 'isUpToDate' function.
+ entrypoint.assertUpToDate();
+ } on DataException {
try {
- // TODO(sigurdm): it would be nicer with a 'isUpToDate' function.
- entrypoint.assertUpToDate();
- } on DataException {
await warningsOnlyUnlessTerminal(
() => entrypoint.acquireDependencies(
SolveType.GET,
analytics: analytics,
),
);
+ } on ApplicationException catch (e) {
+ throw CommandResolutionFailedException._(
+ e.toString(), CommandResolutionIssue.pubGetFailed);
}
+ }
- String command;
- String package;
- if (descriptor.contains(':')) {
- final parts = descriptor.split(':');
- if (parts.length > 2) {
- throw CommandResolutionFailedException(
- '[<package>[:command]] cannot contain multiple ":"');
- }
- package = parts[0];
- if (package.isEmpty) package = entrypoint.root.name;
- command = parts[1];
- } else {
- package = descriptor;
- if (package.isEmpty) package = entrypoint.root.name;
- command = package;
+ late final String command;
+ String package;
+ if (descriptor.contains(':')) {
+ final parts = descriptor.split(':');
+ if (parts.length > 2) {
+ throw CommandResolutionFailedException._(
+ '[<package>[:command]] cannot contain multiple ":"',
+ CommandResolutionIssue.parseError,
+ );
}
+ package = parts[0];
+ if (package.isEmpty) package = entrypoint.root.name;
+ command = parts[1];
+ } else {
+ package = descriptor;
+ if (package.isEmpty) package = entrypoint.root.name;
+ command = package;
+ }
- final executable = Executable(package, p.join('bin', '$command.dart'));
- if (!entrypoint.packageGraph.packages.containsKey(package)) {
- throw CommandResolutionFailedException(
- 'Could not find package `$package` or file `$descriptor`');
- }
- final path = entrypoint.resolveExecutable(executable);
- if (!fileExists(path)) {
- throw CommandResolutionFailedException(
- 'Could not find `bin${p.separator}$command.dart` in package `$package`.');
- }
- if (!allowSnapshot) {
- return p.relative(path, from: root);
- } else {
- final snapshotPath = entrypoint.pathOfExecutable(executable);
- if (!fileExists(snapshotPath) ||
- entrypoint.packageGraph.isPackageMutable(package)) {
+ if (!entrypoint.packageGraph.packages.containsKey(package)) {
+ throw CommandResolutionFailedException._(
+ 'Could not find package `$package` or file `$descriptor`',
+ CommandResolutionIssue.packageNotFound,
+ );
+ }
+ final executable = Executable(package, p.join('bin', '$command.dart'));
+ final packageConfig = p.join('.dart_tool', 'package_config.json');
+
+ final path = entrypoint.resolveExecutable(executable);
+ if (!fileExists(path)) {
+ throw CommandResolutionFailedException._(
+ 'Could not find `bin${p.separator}$command.dart` in package `$package`.',
+ CommandResolutionIssue.noBinaryFound,
+ );
+ }
+ if (!allowSnapshot) {
+ return DartExecutableWithPackageConfig(
+ executable: p.relative(path, from: root),
+ packageConfig: packageConfig,
+ );
+ } else {
+ final snapshotPath = entrypoint.pathOfExecutable(executable);
+ if (!fileExists(snapshotPath) ||
+ entrypoint.packageGraph.isPackageMutable(package)) {
+ try {
await warningsOnlyUnlessTerminal(
() => entrypoint.precompileExecutable(executable),
);
+ } on ApplicationException catch (e) {
+ throw CommandResolutionFailedException._(
+ e.toString(),
+ CommandResolutionIssue.compilationFailed,
+ );
}
- return p.relative(snapshotPath, from: root);
}
- } on ApplicationException catch (e) {
- throw CommandResolutionFailedException(e.toString());
+ return DartExecutableWithPackageConfig(
+ executable: p.relative(snapshotPath, from: root),
+ packageConfig: packageConfig,
+ );
}
}
+/// Information on why no executable is returned.
+enum CommandResolutionIssue {
+ /// The command string looked like a file (contained '.' '/' or '\\'), but no
+ /// such file exists.
+ fileNotFound,
+
+ /// The command-string was '<package>:<binary>' or '<package>', and <package>
+ /// was not in dependencies.
+ packageNotFound,
+
+ /// The command string was '<package>:<binary>' or ':<binary>' and <binary>
+ /// was not found.
+ noBinaryFound,
+
+ /// Failed retrieving dependencies (pub get).
+ pubGetFailed,
+
+ /// Pre-compilation of the binary failed.
+ compilationFailed,
+
+ /// The command string did not have a valid form (eg. more than one ':').
+ parseError,
+}
+
+/// Indicates that a command string did not resolve to an executable.
+@sealed
class CommandResolutionFailedException implements Exception {
final String message;
- CommandResolutionFailedException(this.message);
+ final CommandResolutionIssue issue;
+ CommandResolutionFailedException._(this.message, this.issue);
@override
String toString() {
diff --git a/test/embedding/get_executable_for_command_test.dart b/test/embedding/get_executable_for_command_test.dart
index e174f3a..7667e12 100644
--- a/test/embedding/get_executable_for_command_test.dart
+++ b/test/embedding/get_executable_for_command_test.dart
@@ -9,15 +9,26 @@
import 'package:path/path.dart' show separator;
import 'package:path/path.dart' as p;
import 'package:pub/pub.dart';
+import 'package:pub/src/log.dart' as log;
+
import 'package:test/test.dart';
import '../descriptor.dart' as d;
import '../test_pub.dart';
-Future<void> testGetExecutable(String command, String root,
- {allowSnapshot = true, result, errorMessage}) async {
+Future<void> testGetExecutable(
+ String command,
+ String root, {
+ allowSnapshot = true,
+ executable,
+ packageConfig,
+ errorMessage,
+ CommandResolutionIssue issue,
+}) async {
final _cachePath = getPubTestEnvironment()['PUB_CACHE'];
- if (result == null) {
+ final oldVerbosity = log.verbosity;
+ log.verbosity = log.Verbosity.NONE;
+ if (executable == null) {
expect(
() => getExecutableForCommand(
command,
@@ -27,18 +38,25 @@
),
throwsA(
isA<CommandResolutionFailedException>()
- .having((e) => e.message, 'message', errorMessage),
+ .having((e) => e.message, 'message', errorMessage)
+ .having((e) => e.issue, 'issue', issue),
),
);
} else {
- final path = await getExecutableForCommand(
+ final e = await getExecutableForCommand(
command,
root: root,
pubCacheDir: _cachePath,
allowSnapshot: allowSnapshot,
);
- expect(path, result);
- expect(File(p.join(root, path)).existsSync(), true);
+ expect(
+ e,
+ isA<DartExecutableWithPackageConfig>()
+ .having((e) => e.executable, 'executable', executable)
+ .having((e) => e.packageConfig, 'packageConfig', packageConfig),
+ );
+ expect(File(p.join(root, e.executable)).existsSync(), true);
+ log.verbosity = oldVerbosity;
}
}
@@ -50,13 +68,13 @@
final dir = d.path('foo');
await testGetExecutable('bar/bar.dart', dir,
- result: p.join('bar', 'bar.dart'));
+ executable: p.join('bar', 'bar.dart'));
await testGetExecutable(p.join('bar', 'bar.dart'), dir,
- result: p.join('bar', 'bar.dart'));
+ executable: p.join('bar', 'bar.dart'));
await testGetExecutable('${p.toUri(dir)}/bar/bar.dart', dir,
- result: p.join('bar', 'bar.dart'));
+ executable: p.join('bar', 'bar.dart'));
});
test('Looks for file when no pubspec.yaml', () async {
@@ -66,9 +84,11 @@
final dir = d.path('foo');
await testGetExecutable('bar/m.dart', dir,
- errorMessage: contains('Could not find file `bar/m.dart`'));
+ errorMessage: contains('Could not find file `bar/m.dart`'),
+ issue: CommandResolutionIssue.fileNotFound);
await testGetExecutable(p.join('bar', 'm.dart'), dir,
- errorMessage: contains('Could not find file `bar${separator}m.dart`'));
+ errorMessage: contains('Could not find file `bar${separator}m.dart`'),
+ issue: CommandResolutionIssue.fileNotFound);
});
test('Error message when pubspec is broken', () async {
@@ -95,13 +115,15 @@
contains(
'Error on line 1, column 9 of ${d.sandbox}${p.separator}foo${p.separator}pubspec.yaml: "name" field must be a valid Dart identifier.'),
contains(
- '{"name":"broken name","environment":{"sdk":">=0.1.2 <1.0.0"}}')));
+ '{"name":"broken name","environment":{"sdk":">=0.1.2 <1.0.0"}}')),
+ issue: CommandResolutionIssue.pubGetFailed);
});
test('Does `pub get` if there is a pubspec.yaml', () async {
await d.dir(appPath, [
d.pubspec({
'name': 'myapp',
+ 'environment': {'sdk': '>=$_currentVersion <3.0.0'},
'dependencies': {'foo': '^1.0.0'}
}),
d.dir('bin', [
@@ -112,8 +134,49 @@
await serveNoPackages();
// The solver uses word-wrapping in its error message, so we use \s to
// accomodate.
- await testGetExecutable('bar/m.dart', d.path(appPath),
- errorMessage: matches(r'version\s+solving\s+failed'));
+ await testGetExecutable(
+ 'bar/m.dart',
+ d.path(appPath),
+ errorMessage: matches(r'version\s+solving\s+failed'),
+ issue: CommandResolutionIssue.pubGetFailed,
+ );
+ });
+
+ test('Reports parse failure', () async {
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'environment': {'sdk': '>=$_currentVersion <3.0.0'},
+ }),
+ ]).create();
+ await testGetExecutable(
+ '::',
+ d.path(appPath),
+ errorMessage: contains(r'cannot contain multiple ":"'),
+ issue: CommandResolutionIssue.parseError,
+ );
+ });
+
+ test('Reports compilation failure', () async {
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'environment': {'sdk': '>=$_currentVersion <3.0.0'},
+ }),
+ d.dir('bin', [
+ d.file('foo.dart', 'main() {'),
+ ])
+ ]).create();
+
+ await serveNoPackages();
+ // The solver uses word-wrapping in its error message, so we use \s to
+ // accomodate.
+ await testGetExecutable(
+ ':foo',
+ d.path(appPath),
+ errorMessage: matches(r'foo.dart:1:8:'),
+ issue: CommandResolutionIssue.compilationFailed,
+ );
});
test('Finds files', () async {
@@ -148,46 +211,81 @@
]).create();
final dir = d.path(appPath);
- await testGetExecutable('myapp', dir,
- result: p.join('.dart_tool', 'pub', 'bin', 'myapp',
- 'myapp.dart-$_currentVersion.snapshot'));
- await testGetExecutable('myapp:myapp', dir,
- result: p.join('.dart_tool', 'pub', 'bin', 'myapp',
- 'myapp.dart-$_currentVersion.snapshot'));
- await testGetExecutable(':myapp', dir,
- result: p.join('.dart_tool', 'pub', 'bin', 'myapp',
- 'myapp.dart-$_currentVersion.snapshot'));
- await testGetExecutable(':tool', dir,
- result: p.join('.dart_tool', 'pub', 'bin', 'myapp',
- 'tool.dart-$_currentVersion.snapshot'));
- await testGetExecutable('foo', dir,
- allowSnapshot: false,
- result: endsWith('foo-1.0.0${separator}bin${separator}foo.dart'));
- await testGetExecutable('foo', dir,
- result:
- '.dart_tool${separator}pub${separator}bin${separator}foo${separator}foo.dart-$_currentVersion.snapshot');
- await testGetExecutable('foo:tool', dir,
- allowSnapshot: false,
- result: endsWith('foo-1.0.0${separator}bin${separator}tool.dart'));
- await testGetExecutable('foo:tool', dir,
- result:
- '.dart_tool${separator}pub${separator}bin${separator}foo${separator}tool.dart-$_currentVersion.snapshot');
+ await testGetExecutable(
+ 'myapp',
+ dir,
+ executable: p.join('.dart_tool', 'pub', 'bin', 'myapp',
+ 'myapp.dart-$_currentVersion.snapshot'),
+ packageConfig: p.join('.dart_tool', 'package_config.json'),
+ );
+ await testGetExecutable(
+ 'myapp:myapp',
+ dir,
+ executable: p.join('.dart_tool', 'pub', 'bin', 'myapp',
+ 'myapp.dart-$_currentVersion.snapshot'),
+ packageConfig: p.join('.dart_tool', 'package_config.json'),
+ );
+ await testGetExecutable(
+ ':myapp',
+ dir,
+ executable: p.join('.dart_tool', 'pub', 'bin', 'myapp',
+ 'myapp.dart-$_currentVersion.snapshot'),
+ packageConfig: p.join('.dart_tool', 'package_config.json'),
+ );
+ await testGetExecutable(
+ ':tool',
+ dir,
+ executable: p.join('.dart_tool', 'pub', 'bin', 'myapp',
+ 'tool.dart-$_currentVersion.snapshot'),
+ packageConfig: p.join('.dart_tool', 'package_config.json'),
+ );
+ await testGetExecutable(
+ 'foo',
+ dir,
+ allowSnapshot: false,
+ executable: endsWith('foo-1.0.0${separator}bin${separator}foo.dart'),
+ packageConfig: p.join('.dart_tool', 'package_config.json'),
+ );
+ await testGetExecutable(
+ 'foo',
+ dir,
+ executable:
+ '.dart_tool${separator}pub${separator}bin${separator}foo${separator}foo.dart-$_currentVersion.snapshot',
+ packageConfig: p.join('.dart_tool', 'package_config.json'),
+ );
+ await testGetExecutable(
+ 'foo:tool',
+ dir,
+ allowSnapshot: false,
+ executable: endsWith('foo-1.0.0${separator}bin${separator}tool.dart'),
+ packageConfig: p.join('.dart_tool', 'package_config.json'),
+ );
+ await testGetExecutable(
+ 'foo:tool',
+ dir,
+ executable:
+ '.dart_tool${separator}pub${separator}bin${separator}foo${separator}tool.dart-$_currentVersion.snapshot',
+ packageConfig: p.join('.dart_tool', 'package_config.json'),
+ );
await testGetExecutable(
'unknown:tool',
dir,
errorMessage: 'Could not find package `unknown` or file `unknown:tool`',
+ issue: CommandResolutionIssue.packageNotFound,
);
await testGetExecutable(
'foo:unknown',
dir,
errorMessage:
'Could not find `bin${separator}unknown.dart` in package `foo`.',
+ issue: CommandResolutionIssue.noBinaryFound,
);
await testGetExecutable(
'unknownTool',
dir,
errorMessage:
'Could not find package `unknownTool` or file `unknownTool`',
+ issue: CommandResolutionIssue.packageNotFound,
);
});
}