Hey!
I think this is ready for a first round of reviews.
@sig...@google.com, I think you're the most knowledgable about (1) how `dart pub global activate` works, and (2) how I may or may not use `package:pub` from `package:dartdev`. So, your review would be greatly appreciated!
Kind r
import 'package:pub/src/source/git.dart';| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
await sourcePackagePubspecFile.copy(appBundleDirectory.pubspec.path);Consider using rename operations here, as they happen atomically.
And perhaps create the entire appBundle first in a temp-dir and then rename.
This requires that the temp-directory is colocated with the final destination, such that we don't have to rename across devices.
throw StateError('Unknown package source: ${lockInfo.source.name}');Consider handling this more gracefully. The user could have a corrupt installation, we should report that nicely, not crash
'Wrong number of arguments, expected "<package>", got "${args.join(' ')}"',This reads a bit funny with 0 arguments,
void writeSync(File file) {Please don't write lock files and package configs if you can avoid it.
That should be the job of pub.
GlobalCan we have these below the project? They seem
// Don't fail on files being in use.Consider retrying after a delay on windows
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
var target = args.option('target')!;My understanding from line 104 is that `target` could potentially be null if the user doesn't specify the option, which would make this `!` throw an ugly exception. We should probably handle that null case nicer? Might be also worthwhile to add a test where it ends up being null.
executables.first.$2,Consider maybe a named tuple so this reads a little nicer?
argParser.addOption('git-path', help: 'Path of git package in repository');(here and below) Maybe add that this is only valid when source is git? (Similar to what you say for hosted-url)
case _SourceKind.hosted:
versionConstraint = args.isEmpty ? 'any' : readArg();
}
if (args.isNotEmpty) {The `invocation` help line kinda implies that you could always optionally supply a version constraint argument. Not sure if we can somehow document that it only applies to hosted?
'A web search for "configure windows path" will show you how.',😂
: '(.bashrc, .bash_profile, .zshrc etc.)';For consistency with line above:
```suggestion
: '(.bashrc, .bash_profile, .zshrc, etc.)';
```
_createHelperPackagePubspec(It wasn't immediately obvious to me what the purpose of the helper package is. Maybe document that in a code comment to help the next reader?
help: 'Also list packages which are currently not active',Where can I as a reader of the help message learn more about what it means for a package to be active or not? Can we add a short description or some pointers here?
help: 'Also list packages which are currently not active',```suggestion
help: 'Also list packages which are currently not active.',
```
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
My understanding from line 104 is that `target` could potentially be null if the user doesn't specify the option, which would make this `!` throw an ugly exception. We should probably handle that null case nicer? Might be also worthwhile to add a test where it ends up being null.
Done
Consider maybe a named tuple so this reads a little nicer?
Done
TODO: land https://github.com/dart-lang/pub/pull/4630 and roll in.
Done
argParser.addOption('git-path', help: 'Path of git package in repository');(here and below) Maybe add that this is only valid when source is git? (Similar to what you say for hosted-url)
Done
case _SourceKind.hosted:
versionConstraint = args.isEmpty ? 'any' : readArg();
}
if (args.isNotEmpty) {The `invocation` help line kinda implies that you could always optionally supply a version constraint argument. Not sure if we can somehow document that it only applies to hosted?
I've added some description to the to the description field and a golden test to see how it looks.
For consistency with line above:
```suggestion
: '(.bashrc, .bash_profile, .zshrc, etc.)';
```
Done
It wasn't immediately obvious to me what the purpose of the helper package is. Maybe document that in a code comment to help the next reader?
Done
await sourcePackagePubspecFile.copy(appBundleDirectory.pubspec.path);Consider using rename operations here, as they happen atomically.
And perhaps create the entire appBundle first in a temp-dir and then rename.
This requires that the temp-directory is colocated with the final destination, such that we don't have to rename across devices.
Done
help: 'Also list packages which are currently not active',```suggestion
help: 'Also list packages which are currently not active.',
```
Done
help: 'Also list packages which are currently not active',Where can I as a reader of the help message learn more about what it means for a package to be active or not? Can we add a short description or some pointers here?
Done
throw StateError('Unknown package source: ${lockInfo.source.name}');Consider handling this more gracefully. The user could have a corrupt installation, we should report that nicely, not crash
Done
'Wrong number of arguments, expected "<package>", got "${args.join(' ')}"',This reads a bit funny with 0 arguments,
Done
Please don't write lock files and package configs if you can avoid it.
That should be the job of pub.
also removed package graphs.
GlobalCan we have these below the project? They seem
I think currently this is not possible, I've filed https://github.com/dart-lang/core/issues/901
Consider retrying after a delay on windows
I think that will make the experience slower than necessary. And might not help with anything.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Code-Review | +1 |
You can specify three different values the <package> argument:This sentence is missing something, maybe:
help: 'Path of git package in repository'Here and below: Doesn't this need a trailing whitespace in the string?
```suggestion
help: 'Path of git package in repository. '
```
help: 'Git branch or commit to be retrieved'Same
--git-path Path of git package in repositoryOnly applies when using a git url for <package>.The test caught the missing white space :)
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Commit-Queue | +1 |
You can specify three different values the <package> argument:This sentence is missing something, maybe:
Done
Here and below: Doesn't this need a trailing whitespace in the string?
```suggestion
help: 'Path of git package in repository. '
```
Done
help: 'Git branch or commit to be retrieved'Daco HarkesSame
Done
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Complete deletes all installed versions of <package> and all executables fromTypo
extension DirectoryExetnsion on Directory {Typo
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Commit-Queue | +1 |
Complete deletes all installed versions of <package> and all executables fromDaco HarkesTypo
Done
Future<int> run() async {From a discussion with @sig...@google.com: We should delete the other versions of this package. This would align the behavior with `brew upgrade`. (`brew install` only applies to not yet installed packages, and `brew upgrade` upgrades the package deleting the previous version. We don't want two commands, so we do both in `dart install`.)
extension DirectoryExetnsion on Directory {Daco HarkesTypo
Done
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Commit-Queue | +1 |
Future<int> run() async {From a discussion with @sig...@google.com: We should delete the other versions of this package. This would align the behavior with `brew upgrade`. (`brew install` only applies to not yet installed packages, and `brew upgrade` upgrades the package deleting the previous version. We don't want two commands, so we do both in `dart install`.)
Done
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
18 is the latest approved patch-set.
The change was submitted with unreviewed changes in the following files:
```
The name of the file: pkg/dartdev/test/native_assets/install_test.dart
Insertions: 51, Deletions: 19.
@@ -82,13 +82,15 @@
(
'install',
'''
-Install a Dart CLI tool for global use.
+Install or upgrade a Dart CLI tool for global use.
Install all executables specified in a package's pubspec.yaml executables
section (https://dart.dev/tools/pub/pubspec#executables) on the PATH. If the
executables section doesn't exist, installs all `bin/*.dart` entry points as
executables.
+If the same package has been previously installed, it will be overwritten.
+
You can specify three different values for the <package> argument:
1. A package name. This will install the package from pub.dev. (hosted)
The [version-constraint] argument can only be passed to 'hosted'.
@@ -125,7 +127,7 @@
'''
Remove a globally installed Dart CLI tool.
-Complete deletes all installed versions of <package> and all executables from
+Completely deletes all installed versions of <package> and all executables from
<package> placed on PATH.
Usage: dart uninstall <package>
@@ -449,7 +451,7 @@
final pubspecFile = File.fromUri(dartAppUri.resolve('pubspec.yaml'));
final pubspecContents = await pubspecFile.readAsString();
final pubspecContentsNew =
- pubspecContents.replaceFirst('dart_app', 'dart_app_renamed');
+ pubspecContents.replaceFirst('dart_app', 'a_different_name');
await pubspecFile.writeAsString(pubspecContentsNew);
// Trying to install an executable with the same name from a different
@@ -471,6 +473,35 @@
null,
environment,
);
+
+ // Using --overwrite leads to inactive versions.
+ // `dart installed --all` should also report the non-active versions.
+ for (final all in [true, false]) {
+ final installedResult = await _runDartdev(
+ fromDartdevSource,
+ 'installed',
+ [if (all) '--all'],
+ null,
+ environment,
+ );
+ final installedLines = installedResult.stdout
+ .split('\n')
+ .where((e) => e.isNotEmpty)
+ .toList();
+ if (all) {
+ expect(installedLines, hasLength(2));
+ expect(
+ installedLines,
+ contains(startsWith('dart_app')),
+ );
+ } else {
+ expect(installedLines, hasLength(1));
+ expect(
+ installedLines,
+ isNot(contains(startsWith('dart_app'))),
+ );
+ }
+ }
});
});
});
@@ -612,7 +643,8 @@
});
});
- skippableTest('dart installed --all', timeout: longTimeout, () async {
+ skippableTest('dart install uninstalls old versions', timeout: longTimeout,
+ () async {
await inTempDir((tempUri) async {
final binDir = Directory.fromUri(tempUri.resolve('install/bin'));
@@ -630,28 +662,36 @@
_packageDir.uri,
environment,
);
- await _runDartdev(
+ final installResult = await _runDartdev(
fromDartdevSource,
'install',
[_packageForTest, _packageVersion],
null,
environment,
);
+ expect(
+ installResult.stdout,
+ stringContainsInOrder(['Uninstalling ', _packageForTest]),
+ );
// `--all` should also report the non-active versions.
- for (final all in [true, false]) {
+ Future<List<String>> runInstalled() async {
final installedResult = await _runDartdev(
fromDartdevSource,
'installed',
- [if (all) '--all'],
+ ['--all'],
null,
environment,
);
- final installedLines = installedResult.stdout.split('\n');
- expect(installedLines.where((e) => e.isNotEmpty).length,
- equals(all ? 2 : 1));
+ final installedLines = installedResult.stdout
+ .split('\n')
+ .where((e) => e.isNotEmpty)
+ .toList();
+ return installedLines;
}
+ expect(await runInstalled(), hasLength(1));
+
// `uninstall` uninstalls all versions.
await _runDartdev(
fromDartdevSource,
@@ -660,15 +700,7 @@
null,
environment,
);
- final installedResult = await _runDartdev(
- fromDartdevSource,
- 'installed',
- ['--all'],
- null,
- environment,
- );
- final installedLines = installedResult.stdout.split('\n');
- expect(installedLines.where((e) => e.isNotEmpty), isEmpty);
+ expect(await runInstalled(), hasLength(0));
});
});
```
```
The name of the file: pkg/dartdev/lib/src/install/file_system.dart
Insertions: 1, Deletions: 1.
@@ -313,6 +313,6 @@
bool equals(ExecutableInBundle other) => file.path == other.file.path;
}
-extension DirectoryExetnsion on Directory {
+extension DirectoryExtension on Directory {
Directory get ensureEndWithSeparator => Directory.fromUri(uri);
}
```
```
The name of the file: pkg/dartdev/lib/src/commands/uninstall.dart
Insertions: 1, Deletions: 1.
@@ -11,7 +11,7 @@
static const cmdName = 'uninstall';
static const cmdDescription = '''Remove a globally installed Dart CLI tool.
-Complete deletes all installed versions of <package> and all executables from
+Completely deletes all installed versions of <package> and all executables from
<package> placed on PATH.''';
@override
```
```
The name of the file: pkg/dartdev/lib/src/commands/install.dart
Insertions: 26, Deletions: 1.
@@ -15,13 +15,16 @@
class InstallCommand extends DartdevCommand {
static const cmdName = 'install';
- static const cmdDescription = '''Install a Dart CLI tool for global use.
+ static const cmdDescription =
+ '''Install or upgrade a Dart CLI tool for global use.
Install all executables specified in a package's pubspec.yaml executables
section (https://dart.dev/tools/pub/pubspec#executables) on the PATH. If the
executables section doesn't exist, installs all `bin/*.dart` entry points as
executables.
+If the same package has been previously installed, it will be overwritten.
+
You can specify three different values for the <package> argument:
1. A package name. This will install the package from pub.dev. (hosted)
The [version-constraint] argument can only be passed to 'hosted'.
@@ -270,6 +273,26 @@
}
}
+ void _uniinstallAllPackageVersions(String packageName) {
+ final bundles =
+ DartInstallDirectory().allAppBundlesSync(packageName: packageName);
+
+ try {
+ for (final bundle in bundles) {
+ print('Uninstalling ${bundle.directory.path}.');
+ final links = bundle.executablesOnPathSync;
+ for (final link in links) {
+ print('Deleting ${link.entity.path}');
+ link.deleteSync();
+ }
+ print('Deleting ${bundle.directory.path}');
+ bundle.directory.deleteSync(recursive: true);
+ }
+ } on PathAccessException {
+ _installException('Deletion failed. The application might be in use.');
+ }
+ }
+
AppBundleDirectory _selectAppBundleDirectory(
_InstallCommandParsedArguments parsedArgs,
String packageName,
@@ -480,6 +503,8 @@
sourcePackagePubspecFile,
);
+ _uniinstallAllPackageVersions(packageName);
+
AppBundleDirectory appBundleDirectory = _selectAppBundleDirectory(
parsedArgs,
packageName,
```
```
The name of the file: pkg/dartdev/test/commands/help_test.dart
Insertions: 1, Deletions: 1.
@@ -80,7 +80,7 @@
Available commands:
Global
- install Install a Dart CLI tool for global use.
+ install Install or upgrade a Dart CLI tool for global use.
installed List globally installed Dart CLI tools.
uninstall Remove a globally installed Dart CLI tool.
```
[dartdev] `dart install`
This CL adds three new commands to `dart`:
```
Global
install Install a Dart CLI tool for global use.
installed List globally installed Dart CLI tools.
uninstall Remove a globally installed Dart CLI tool.
```
These commands are intended to replace `dart pub global` subcommands
while adding support for `hook/build.dart` and building packages in
AOT instead of running them with JIT.
Internal design doc: http://go/dart-install-cli.
Implementation details:
* The source of truth is the state of the file system. These commands
write and read directories, files, and symlinks.
* App bundles and symlinks are placed in `DART_DATA_HOME` as per
http://go/dart-data-home.
* On Unix systems we use symlinks and on Windows batchfiles to place
executables in the bin directory that point to an application
bundle. (These OS differences have been encapsulated in a single
class.)
* On Windows, when an application is running, trying to re-install it
will fail.
Test coverage:
* Installing from hosted, git, and local paths.
* Installing a package with hooks.
* Installing a package with hooks and user-defines.
* Surfacing build hook failures during install.
* Installing packages with conflicting executables names. This tests
`--overwrite` flag behavior.
* Installing a new or the same version, this should simply succeed.
* A warning is shown if the bin directory is not on the `PATH`.
* Running an installed app reports the correct exit code on exit.
* Listing all installed versions, including the versions not on
the`PATH`.
* Uninstalling, which uninstalls all versions.
* Re-installing while it is running.
* Uninstalling while it is running.
Out of scope for initial version:
* Saving the SDK version (to display in `dart installed`).
* Short-circuiting if re-installing an exactly installed version.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |