Newer
Older
// 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.
library pub.global_packages;
import 'dart:async';
import 'dart:io';
import 'package:path/path.dart' as p;
import 'io.dart';
import 'lock_file.dart';
import 'log.dart' as log;
import 'package.dart';
import 'pubspec.dart';
import 'system_cache.dart';
import 'solver/version_solver.dart';
import 'source/cached.dart';
import 'source/path.dart';
import 'utils.dart';
import 'version.dart';
/// Maintains the set of packages that have been globally activated.
///
/// These have been hand-chosen by the user to make their executables in bin/
/// available to the entire system. This lets them access them even when the
/// current working directory is not inside another entrypoint package.
///
/// Only one version of a given package name can be globally activated at a
/// time. Activating a different version of a package will deactivate the
/// previous one.
///
/// This handles packages from uncached and cached sources a little differently.
/// For a cached source, the package is physically in the user's pub cache and
/// we don't want to mess with it by putting a lockfile in there. Instead, when
/// we activate the package, we create a full lockfile and put it in the
/// "global_packages" directory. It's named "<package>.lock". Unlike a normal
/// lockfile, it also contains an entry for the root package itself, so that we
/// know the version and description that was activated.
///
/// Uncached packages (i.e. "path" packages) are somewhere else on the user's
/// local file system and can have a lockfile directly in place. (And, in fact,
/// we want to ensure we honor the user's lockfile there.) To activate it, we
/// just need to know where that package directory is. For that, we create a
/// lockfile that *only* contains the root package's [PackageId] -- basically
/// just the path to the directory where the real lockfile lives.
class GlobalPackages {
/// The [SystemCache] containing the global packages.
final SystemCache cache;
/// The directory where the lockfiles for activated packages are stored.
String get _directory => p.join(cache.rootDir, "global_packages");
/// Creates a new global package registry backed by the given directory on
/// the user's file system.
///
/// The directory may not physically exist yet. If not, this will create it
/// when needed.
GlobalPackages(this.cache);
/// Caches the package located in the Git repository [repo] and makes it the
/// active global version.
Future activateGit(String repo) {
var source = cache.sources["git"] as GitSource;
return source.getPackageNameFromRepo(repo).then((name) {
// Call this just to log what the current active package is, if any.
_describeActive(name);
return _installInCache(
new PackageDep(name, "git", VersionConstraint.any, repo));
/// 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) {
_describeActive(name);
return _installInCache(new PackageDep(name, "hosted", constraint, name));
}
/// Makes the local package at [path] globally active.
Future activatePath(String path) {
var entrypoint = new Entrypoint(path, cache);
// Get the package's dependencies.
return entrypoint.ensureLockFileIsUpToDate().then((_) {
var name = entrypoint.root.name;
// Call this just to log what the current active package is, if any.
_describeActive(name);
// Write a lockfile that points to the local package.
var fullPath = canonicalize(entrypoint.root.dir);
var id = new PackageId(name, "path", entrypoint.root.version,
PathSource.describePath(fullPath));
_writeLockFile(name, new LockFile([id]));
});
}
/// Installs the package [dep] and its dependencies into the system cache.
Future _installInCache(PackageDep dep) {
var source = cache.sources[dep.source];
// Create a dummy package with just [dep] so we can do resolution on it.
var root = new Package.inMemory(new Pubspec("pub global activate",
dependencies: [dep], sources: cache.sources));
// Resolve it and download its dependencies.
return resolveVersions(SolveType.GET, cache.sources, root).then((result) {
if (!result.succeeded) {
// If the package specified by the user doesn't exist, we want to
// surface that as a [DataError] with the associated exit code.
if (result.error.package != dep.name) throw result.error;
if (result.error is NoVersionException) dataError(result.error.message);
throw result.error;
}
// Make sure all of the dependencies are locally installed.
return Future.wait(result.packages.map(_cacheDependency));
_writeLockFile(dep.name, new LockFile(ids));
/// Downloads [id] into the system cache if it's a cached package.
///
/// Returns the resolved [PackageId] for [id].
Future<PackageId> _cacheDependency(PackageId id) {
var source = cache.sources[id.source];
return syncFuture(() {
if (id.isRoot) return null;
if (source is! CachedSource) return null;
return source.downloadToSystemCache(id);
}).then((_) => source.resolveId(id));
/// Finishes activating package [package] by saving [lockFile] in the cache.
void _writeLockFile(String package, LockFile lockFile) {
ensureDir(_directory);
writeTextFile(_getLockFilePath(package),
lockFile.serialize(cache.rootDir, cache.sources));
var id = lockFile.packages[package];
rnystrom@google.com
committed
log.message('Activated ${_formatPackage(id)}.');
// TODO(rnystrom): Look in "bin" and display list of binaries that
// user can run.
}
/// Shows the user the currently active package with [name], if any.
rnystrom@google.com
committed
void _describeActive(String name) {
rnystrom@google.com
committed
var lockFile = new LockFile.load(_getLockFilePath(name), cache.sources);
var id = lockFile.packages[name];
rnystrom@google.com
committed
if (id.source == 'git') {
var url = GitSource.urlFromDescription(id.description);
rnystrom@google.com
committed
log.message('Package ${log.bold(name)} is currently active from Git '
'repository "${url}".');
} else if (id.source == 'path') {
var path = PathSource.pathFromDescription(id.description);
rnystrom@google.com
committed
log.message('Package ${log.bold(name)} is currently active at path '
'"$path".');
rnystrom@google.com
committed
log.message('Package ${log.bold(name)} is currently active at version '
'${log.bold(id.version)}.');
// If we couldn't read the lock file, it's not activated.
return null;
}
}
/// Deactivates a previously-activated package named [name].
///
rnystrom@google.com
committed
/// 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}) {
var lockFilePath = _getLockFilePath(name);
if (!fileExists(lockFilePath)) return false;
var lockFile = new LockFile.load(lockFilePath, cache.sources);
var id = lockFile.packages[name];
deleteEntry(lockFilePath);
if (logDeactivate) {
rnystrom@google.com
committed
log.message('Deactivated package ${_formatPackage(id)}.');
return true;
/// Finds the active package with [name].
///
/// Returns an [Entrypoint] loaded with the active package if found.
Future<Entrypoint> find(String name) {
return syncFuture(() {
var lockFile;
try {
lockFile = new LockFile.load(_getLockFilePath(name), cache.sources);
} on IOException catch (error) {
// If we couldn't read the lock file, it's not activated.
dataError("No active package ${log.bold(name)}.");
}
// Load the package from the cache.
var id = lockFile.packages[name];
lockFile.packages.remove(name);
var source = cache.sources[id.source];
if (source is CachedSource) {
// For cached sources, the package itself is in the cache and the
// lockfile is the one we just loaded.
return cache.sources[id.source].getDirectory(id)
.then((dir) => new Package.load(name, dir, cache.sources))
.then((package) {
return new Entrypoint.inMemory(package, lockFile, cache);
});
}
// For uncached sources (i.e. path), the ID just points to the real
// directory for the package.
assert(id.source == "path");
return new Entrypoint(PathSource.pathFromDescription(id.description),
cache);
/// Gets the path to the lock file for an activated cached package with
/// [name].
String _getLockFilePath(name) => p.join(_directory, name + ".lock");
rnystrom@google.com
committed
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
/// Shows to the user formatted list of globally activated packages.
void listActivePackages() {
if (!dirExists(_directory)) return;
// Loads lock [file] and returns [PackageId] of the activated package.
loadPackageId(file) {
var name = p.basenameWithoutExtension(file);
var lockFile = new LockFile.load(p.join(_directory, file), cache.sources);
return lockFile.packages[name];
}
var packages = listDir(_directory, includeDirs: false)
.where((file) => p.extension(file) == '.lock')
.map(loadPackageId)
.toList();
packages
..sort((id1, id2) => id1.name.compareTo(id2.name))
..forEach((id) => log.message(_formatPackage(id)));
}
/// Returns formatted string representing the package [id].
String _formatPackage(PackageId id) {
if (id.source == 'git') {
var url = GitSource.urlFromDescription(id.description);
return '${log.bold(id.name)} ${id.version} from Git repository "$url"';
} else if (id.source == 'path') {
var path = PathSource.pathFromDescription(id.description);
return '${log.bold(id.name)} ${id.version} at path "$path"';
} else {
return '${log.bold(id.name)} ${id.version}';
}
}