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 'system_cache.dart';
import 'solver/version_solver.dart';
import 'source.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);
/// 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) {
// See if we already have it activated.
var lockFile = _describeActive(name);
if (lockFile != null) {
var id = lockFile.packages[name];
// Try to preserve the current version if we've already activated the
// hosted package.
if (id.source == "hosted") currentVersion = id.version;
// Pull the root package out of the lock file so the solver doesn't see
// it.
lockFile.packages.remove(name);
lockFile = new LockFile.empty();
}
return _selectVersion(name, currentVersion, constraint).then((version) {
// Make sure it's in the cache.
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
var id = new PackageId(name, "hosted", version, name);
return _installInCache(id, lockFile);
});
}
/// 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(id, new LockFile.empty());
});
}
/// Installs the package [id] with [lockFile] into the system cache along
/// with its dependencies.
Future _installInCache(PackageId id, LockFile lockFile) {
var source = cache.sources[id.source];
// Put the main package in the cache.
return source.downloadToSystemCache(id).then((package) {
// If we didn't know the version for the ID (which is true for Git
// packages), look it up now that we have it.
if (id.version == Version.none) {
id = id.atVersion(package.version);
}
return source.resolveId(id).then((id_) {
id = id_;
// Resolve it and download its dependencies.
return resolveVersions(SolveType.GET, cache.sources, package,
lockFile: lockFile);
});
}).then((result) {
if (!result.succeeded) throw result.error;
// Make sure all of the dependencies are locally installed.
return Future.wait(result.packages.map(_cacheDependency));
_writeLockFile(id, 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));
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
/// Finishes activating package [id] by saving [lockFile] in the cache.
void _writeLockFile(PackageId id, LockFile lockFile) {
// Add the root package to the lockfile.
lockFile.packages[id.name] = id;
ensureDir(_directory);
writeTextFile(_getLockFilePath(id.name),
lockFile.serialize(cache.rootDir, cache.sources));
if (id.source == "path") {
var path = PathSource.pathFromDescription(id.description);
log.message('Activated ${log.bold(id.name)} ${id.version} at path '
'"$path".');
} else {
log.message("Activated ${log.bold(id.name)} ${id.version}.");
}
// TODO(rnystrom): Look in "bin" and display list of binaries that
// user can run.
}
/// Gets the lock file for the currently active package with [name].
///
/// Displays a message to the user about the current package, if any. Returns
/// the [LockFile] for the active package or `null` otherwise.
LockFile _describeActive(String package) {
var lockFile = new LockFile.load(_getLockFilePath(package),
cache.sources);
var id = lockFile.packages[package];
if (id.source == "path") {
var path = PathSource.pathFromDescription(id.description);
log.message('Package ${log.bold(package)} is currently active at '
'path "$path".');
} else {
log.message("Package ${log.bold(package)} is currently active at "
"version ${log.bold(id.version)}.");
}
return lockFile;
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
// If we couldn't read the lock file, it's not activated.
return null;
}
}
/// Deactivates a previously-activated package named [name].
///
/// If [logDeletion] 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) {
if (id.source == "path") {
var path = PathSource.pathFromDescription(id.description);
log.message('Deactivated package ${log.bold(name)} at path "$path".');
} else {
log.message("Deactivated package ${log.bold(name)} ${id.version}.");
}
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);
/// Picks the best hosted version of [package] to activate that meets
/// [constraint].
///
/// If [version] is not `null`, this tries to maintain that version if
/// possible.
Future<Version> _selectVersion(String package, Version version,
VersionConstraint constraint) {
// If we already have a valid active version, just use it.
if (version != null && constraint.allows(version)) {
return new Future.value(version);
}
// Otherwise, select the best version the matches the constraint.
var source = cache.sources["hosted"];
return source.getVersions(package, package).then((versions) {
versions = versions.where(constraint.allows).toList();
if (versions.isEmpty) {
// TODO(rnystrom): Show most recent unmatching version?
dataError("Package ${log.bold(package)} has no versions that match "
"$constraint.");
}
// Pick the best matching version.
versions.sort(Version.prioritize);
return versions.last;
});
}
/// Gets the path to the lock file for an activated cached package with
/// [name].
String _getLockFilePath(name) => p.join(_directory, name + ".lock");