Chromium Code Reviews

Unified Diff: sdk/lib/_internal/pub/lib/src/global_packages.dart

Issue 566093003: Create binstubs for executables when activating a package. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Revise! Created 6 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View side-by-side diff with in-line comments
Index: sdk/lib/_internal/pub/lib/src/global_packages.dart
diff --git a/sdk/lib/_internal/pub/lib/src/global_packages.dart b/sdk/lib/_internal/pub/lib/src/global_packages.dart
index f5a7d6587e7689077815a807d19996a8ba08feeb..22803f8e472559990ab06a2a718025a3d155ea7d 100644
--- a/sdk/lib/_internal/pub/lib/src/global_packages.dart
+++ b/sdk/lib/_internal/pub/lib/src/global_packages.dart
@@ -18,14 +18,19 @@ import 'lock_file.dart';
import 'log.dart' as log;
import 'package.dart';
import 'pubspec.dart';
-import 'system_cache.dart';
+import 'sdk.dart' as sdk;
import 'solver/version_solver.dart';
import 'source/cached.dart';
import 'source/git.dart';
import 'source/path.dart';
+import 'system_cache.dart';
import 'utils.dart';
import 'version.dart';
+/// Matches the package name that a binstub was created for inside the contents
+/// of the shell script.
+final _binStubPackagePattern = new RegExp(r"Package: ([a-zA-Z0-9_-]+)");
+
/// Maintains the set of packages that have been globally activated.
///
/// These have been hand-chosen by the user to make their executables in bin/
@@ -57,6 +62,9 @@ class GlobalPackages {
/// The directory where the lockfiles for activated packages are stored.
String get _directory => p.join(cache.rootDir, "global_packages");
+ /// The directory where binstubs for global package executables are stored.
+ String get _binStubDir => p.join(cache.rootDir, "bin");
+
/// Creates a new global package registry backed by the given directory on
/// the user's file system.
///
@@ -66,7 +74,16 @@ class GlobalPackages {
/// Caches the package located in the Git repository [repo] and makes it the
/// active global version.
- Future activateGit(String repo) async {
+ ///
+ /// [executables] is the names of the executables that should have binstubs.
+ /// If `null`, all executables in the package will get binstubs. If empty, no
+ /// binstubs will be created.
+ ///
+ /// if [overwriteBinStubs] is `true`, any binstubs that collide with
+ /// existing binstubs in other packages will be overwritten by this one's.
+ /// Otherwise, the previous ones will be preserved.
+ Future activateGit(String repo, List<String> executables,
+ {bool overwriteBinStubs}) async {
var source = cache.sources["git"] as GitSource;
var name = await source.getPackageNameFromRepo(repo);
// Call this just to log what the current active package is, if any.
@@ -76,19 +93,42 @@ class GlobalPackages {
// dependencies. Their executables shouldn't be cached, and there should
// be a mechanism for redoing dependency resolution if a path pubspec has
// changed (see also issue 20499).
- await _installInCache(
+ var package = await _installInCache(
new PackageDep(name, "git", VersionConstraint.any, repo));
+ _updateBinStubs(package, executables,
+ overwriteBinStubs: overwriteBinStubs);
}
/// Finds the latest version of the hosted package with [name] that matches
/// [constraint] and makes it the active global version.
- Future activateHosted(String name, VersionConstraint constraint) {
+ ///
+ /// [executables] is the names of the executables that should have binstubs.
+ /// If `null`, all executables in the package will get binstubs. If empty, no
+ /// binstubs will be created.
+ ///
+ /// if [overwriteBinStubs] is `true`, any binstubs that collide with
+ /// existing binstubs in other packages will be overwritten by this one's.
+ /// Otherwise, the previous ones will be preserved.
+ Future activateHosted(String name, VersionConstraint constraint,
+ List<String> executables, {bool overwriteBinStubs}) async {
_describeActive(name);
- return _installInCache(new PackageDep(name, "hosted", constraint, name));
+ var package = await _installInCache(
+ new PackageDep(name, "hosted", constraint, name));
+ _updateBinStubs(package, executables,
+ overwriteBinStubs: overwriteBinStubs);
}
/// Makes the local package at [path] globally active.
- Future activatePath(String path) async {
+ ///
+ /// [executables] is the names of the executables that should have binstubs.
+ /// If `null`, all executables in the package will get binstubs. If empty, no
+ /// binstubs will be created.
+ ///
+ /// if [overwriteBinStubs] is `true`, any binstubs that collide with
+ /// existing binstubs in other packages will be overwritten by this one's.
+ /// Otherwise, the previous ones will be preserved.
+ Future activatePath(String path, List<String> executables,
+ {bool overwriteBinStubs}) async {
var entrypoint = new Entrypoint(path, cache);
// Get the package's dependencies.
@@ -109,10 +149,15 @@ class GlobalPackages {
var binDir = p.join(_directory, name, 'bin');
if (dirExists(binDir)) deleteEntry(binDir);
+
+ _updateBinStubs(entrypoint.root, executables,
+ overwriteBinStubs: overwriteBinStubs);
}
/// Installs the package [dep] and its dependencies into the system cache.
- Future _installInCache(PackageDep dep) async {
+ ///
+ /// Returns the cached root [Package].
+ Future<Package> _installInCache(PackageDep dep) async {
var source = cache.sources[dep.source];
// Create a dummy package with just [dep] so we can do resolution on it.
@@ -140,6 +185,8 @@ class GlobalPackages {
.loadPackageGraph(result);
await _precompileExecutables(graph.entrypoint, dep.name);
_writeLockFile(dep.name, lockFile);
+
+ return graph.packages[dep.name];
}
/// Precompiles the executables for [package] and saves them in the global
@@ -188,7 +235,6 @@ class GlobalPackages {
var id = lockFile.packages[package];
log.message('Activated ${_formatPackage(id)}.');
-
}
/// Shows the user the currently active package with [name], if any.
@@ -217,19 +263,16 @@ class GlobalPackages {
/// Deactivates a previously-activated package named [name].
///
- /// If [logDeactivate] is true, displays to the user when a package is
- /// deactivated. Otherwise, deactivates silently.
- ///
/// Returns `false` if no package with [name] was currently active.
- bool deactivate(String name, {bool logDeactivate: false}) {
+ bool deactivate(String name) {
var dir = p.join(_directory, name);
if (!dirExists(dir)) return false;
- if (logDeactivate) {
- var lockFile = new LockFile.load(_getLockFilePath(name), cache.sources);
- var id = lockFile.packages[name];
- log.message('Deactivated package ${_formatPackage(id)}.');
- }
+ _deleteBinStubs(name);
+
+ var lockFile = new LockFile.load(_getLockFilePath(name), cache.sources);
+ var id = lockFile.packages[name];
+ log.message('Deactivated package ${_formatPackage(id)}.');
deleteEntry(dir);
@@ -366,4 +409,189 @@ class GlobalPackages {
return '${log.bold(id.name)} ${id.version}';
}
}
+
+ /// Updates the binstubs for [package].
+ ///
+ /// A binstub is a little shell script in `PUB_CACHE/bin` that runs an
+ /// executable from a globally activated package. This removes any old
+ /// binstubs from the previously activated version of the package and
+ /// (optionally) creates new ones for the executables listed in the package's
+ /// pubspec.
+ ///
+ /// [executables] is the names of the executables that should have binstubs.
+ /// If `null`, all executables in the package will get binstubs. If empty, no
+ /// binstubs will be created.
+ ///
+ /// if [overwriteBinStubs] is `true`, any binstubs that collide with
+ /// existing binstubs in other packages will be overwritten by this one's.
+ /// Otherwise, the previous ones will be preserved.
+ void _updateBinStubs(Package package, List<String> executables,
+ {bool overwriteBinStubs}) {
+ // Remove any previously activated binstubs for this package, in case the
+ // list of executables has changed.
+ _deleteBinStubs(package.name);
+
+ if ((executables != null && executables.isEmpty) ||
+ package.pubspec.executables.isEmpty) {
+ return;
+ }
+
+ ensureDir(_binStubDir);
+
+ var installed = [];
+ var collided = {};
+ var allExecutables = ordered(package.pubspec.executables.keys);
+ for (var executable in allExecutables) {
+ if (executables != null && !executables.contains(executable)) continue;
+
+ var script = package.pubspec.executables[executable];
+
+ var previousPackage = _createBinStub(package, executable, script,
+ overwrite: overwriteBinStubs);
+ if (previousPackage != null) {
+ collided[executable] = previousPackage;
+
+ if (!overwriteBinStubs) continue;
+ }
+
+ installed.add(executable);
+ }
+
+ if (installed.isNotEmpty) {
+ var names = namedSequence("executable", installed.map(log.bold));
+ log.message("Installed $names.");
+ // TODO(rnystrom): Show the user how to add the binstub directory to
+ // their PATH if not already on it.
+ }
+
+ // Show errors for any collisions.
+ if (collided.isNotEmpty) {
+ for (var command in ordered(collided.keys)) {
+ if (overwriteBinStubs) {
+ log.warning("Replaced ${log.bold(command)} previously installed from "
+ "${log.bold(collided[command])}.");
+ } else {
+ log.warning("Executable ${log.bold(command)} was already installed "
+ "from ${log.bold(collided[command])}.");
+ }
+ }
+
+ if (!overwriteBinStubs) {
+ log.warning("Deactivate the other package(s) or activate "
+ "${log.bold(package.name)} using --overwrite-executables.");
+ }
+ }
+
+ // Show errors for any unknown executables.
+ if (executables != null) {
+ var unknown = ordered(executables.where(
+ (exe) => !package.pubspec.executables.keys.contains(exe)));
+ if (unknown.isNotEmpty) {
+ dataError("Unknown ${namedSequence('executable', unknown)}.");
+ }
+ }
+
+ // Show errors for any missing scripts.
+ // TODO(rnystrom): This can print false positives since a script may be
+ // produced by a transformer. Do something better.
+ var binFiles = package.listFiles(beneath: "bin", recursive: false)
+ .map((path) => p.relative(path, from: package.dir))
+ .toList();
+ for (var executable in installed) {
+ var script = package.pubspec.executables[executable];
+ var scriptPath = p.join("bin", "$script.dart");
+ if (!binFiles.contains(scriptPath)) {
+ log.warning('Warning: Executable "$executable" runs "$scriptPath", '
+ 'which was not found in ${log.bold(package.name)}.');
+ }
+ }
+ }
+
+ /// Creates a binstub named [executable] that runs [script] from [package].
+ ///
+ /// If [overwrite] is `true`, this will replace an existing binstub with that
+ /// name for another package.
+ ///
+ /// If a collision occurs, returns the name of the package that owns the
+ /// existing binstub. Otherwise returns `null`.
+ String _createBinStub(Package package, String executable, String script,
+ {bool overwrite}) {
+ var binStubPath = p.join(_binStubDir, executable);
+
+ // See if the binstub already exists. If so, it's for another package
+ // since we already deleted all of this package's binstubs.
+ var previousPackage;
+ if (fileExists(binStubPath)) {
+ var contents = readTextFile(binStubPath);
+ var match = _binStubPackagePattern.firstMatch(contents);
+ if (match != null) {
+ previousPackage = match[1];
+ if (!overwrite) return previousPackage;
+ } else {
+ log.fine("Could not parse binstub $binStubPath:\n$contents");
+ }
+ }
+
+ // TODO(rnystrom): If the script was precompiled to a snapshot, make the
+ // script just invoke that directly and skip pub global run entirely.
+
+ if (Platform.operatingSystem == "windows") {
+ var batch = """
+@echo off
+rem This file was created by pub v${sdk.version}.
+rem Package: ${package.name}
+rem Version: ${package.version}
+rem Executable: ${executable}
+rem Script: ${script}
+pub global run ${package.name}:$script "%*"
+""";
+ writeTextFile(binStubPath, batch);
+ } else {
+ var bash = """
+# This file was created by pub v${sdk.version}.
+# Package: ${package.name}
+# Version: ${package.version}
+# Executable: ${executable}
+# Script: ${script}
+pub global run ${package.name}:$script "\$@"
+""";
+ writeTextFile(binStubPath, bash);
+
+ // Make it executable.
+ var result = Process.runSync('chmod', ['+x', binStubPath]);
+ if (result.exitCode != 0) {
+ // Couldn't make it executable so don't leave it laying around.
+ try {
+ deleteEntry(binStubPath);
+ } on IOException catch (err) {
+ // Do nothing. We're going to fail below anyway.
+ log.fine("Could not delete binstub:\n$err");
+ }
+
+ fail('Could not make "$binStubPath" executable (exit code '
+ '${result.exitCode}):\n${result.stderr}');
+ }
+ }
+
+ return previousPackage;
+ }
+
+ /// Deletes all existing binstubs for [package].
+ void _deleteBinStubs(String package) {
+ if (!dirExists(_binStubDir)) return;
+
+ for (var file in listDir(_binStubDir, includeDirs: false)) {
+ var contents = readTextFile(file);
+ var match = _binStubPackagePattern.firstMatch(contents);
+ if (match == null) {
+ log.fine("Could not parse binstub $file:\n$contents");
+ continue;
+ }
+
+ if (match[1] == package) {
+ log.fine("Deleting old binstub $file");
+ deleteEntry(file);
+ }
+ }
+ }
}

Powered by Google App Engine