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 'package:barback/barback.dart';
import 'barback/asset_environment.dart';
import 'executable.dart' as exe;
import 'io.dart';
import 'lock_file.dart';
import 'log.dart' as log;
import 'package.dart';
import 'pubspec.dart';
import 'package_graph.dart';
import 'system_cache.dart';
import 'sdk.dart' as sdk;
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);
// TODO(nweiz): Add some special handling for git repos that contain path
// 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).
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));
// TODO(rnystrom): Look in "bin" and display list of binaries that
// user can run.
_writeLockFile(name, new LockFile([id]));
var binDir = p.join(_directory, name, 'bin');
if (dirExists(binDir)) deleteEntry(binDir);
});
}
/// 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.
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
return Future.wait(result.packages.map(_cacheDependency)).then((ids) {
var lockFile = new LockFile(ids);
// Load the package graph from [result] so we don't need to re-parse all
// the pubspecs.
return new Entrypoint.inMemory(root, lockFile, cache)
.loadPackageGraph(result)
.then((graph) => _precompileExecutables(graph.entrypoint, dep.name))
.then((_) => _writeLockFile(dep.name, lockFile));
});
});
}
/// Precompiles the executables for [package] and saves them in the global
/// cache.
Future _precompileExecutables(Entrypoint entrypoint, String package) {
return log.progress("Precompiling executables", () {
var binDir = p.join(_directory, package, 'bin');
var sdkVersionPath = p.join(binDir, 'sdk-version');
cleanDir(binDir);
writeTextFile(sdkVersionPath, "${sdk.version}\n");
return AssetEnvironment.create(entrypoint, BarbackMode.RELEASE,
useDart2JS: false).then((environment) {
environment.barback.errors.listen((error) {
log.error(log.red("Build error:\n$error"));
});
return environment.precompileExecutables(package, binDir);
});
/// 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(p.join(_directory, package));
// TODO(nweiz): This cleans up Dart 1.6's old lockfile location. Remove it
// when Dart 1.6 is old enough that we don't think anyone will have these
// lockfiles anymore (issue 20703).
var oldPath = p.join(_directory, "$package.lock");
if (fileExists(oldPath)) deleteEntry(oldPath);
writeTextFile(_getLockFilePath(package),
lockFile.serialize(cache.rootDir, cache.sources));
var id = lockFile.packages[package];
rnystrom@google.com
committed
log.message('Activated ${_formatPackage(id)}.');
/// 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 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];
rnystrom@google.com
committed
log.message('Deactivated package ${_formatPackage(id)}.');
deleteEntry(dir);
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 lockFilePath = _getLockFilePath(name);
var lockFile;
lockFile = new LockFile.load(lockFilePath, cache.sources);
var oldLockFilePath = p.join(_directory, '$name.lock');
try {
// TODO(nweiz): This looks for Dart 1.6's old lockfile location.
// Remove it when Dart 1.6 is old enough that we don't think anyone
// will have these lockfiles anymore (issue 20703).
lockFile = new LockFile.load(oldLockFilePath, 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)}.");
}
// Move the old lockfile to its new location.
ensureDir(p.dirname(lockFilePath));
new File(oldLockFilePath).renameSync(lockFilePath);
// 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);
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
/// Runs [package]'s [executable] with [args].
///
/// If [executable] is available in its precompiled form, that will be
/// recompiled if the SDK has been upgraded since it was first compiled and
/// then run. Otherwise, it will be run from source.
///
/// Returns the exit code from the executable.
Future<int> runExecutable(String package, String executable,
Iterable<String> args) {
var binDir = p.join(_directory, package, 'bin');
if (!fileExists(p.join(binDir, '$executable.dart.snapshot'))) {
return find(package).then((entrypoint) {
return exe.runExecutable(entrypoint, package, executable, args,
isGlobal: true);
});
}
// Unless the user overrides the verbosity, we want to filter out the
// normal pub output shown while loading the environment.
if (log.verbosity == log.Verbosity.NORMAL) {
log.verbosity = log.Verbosity.WARNING;
}
return syncFuture(() {
var sdkVersionPath = p.join(binDir, 'sdk-version');
var snapshotVersion = readTextFile(sdkVersionPath);
if (snapshotVersion == "${sdk.version}\n") return null;
log.fine("$package:$executable was compiled with Dart "
"${snapshotVersion.trim()} and needs to be recompiled.");
return find(package)
.then((entrypoint) => entrypoint.loadPackageGraph())
.then((graph) => _precompileExecutables(graph.entrypoint, package));
}).then((_) =>
exe.runSnapshot(p.join(binDir, '$executable.dart.snapshot'), args));
}
/// Gets the path to the lock file for an activated cached package with
/// [name].
String _getLockFilePath(String name) =>
p.join(_directory, name, "pubspec.lock");
rnystrom@google.com
committed
/// 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, name) {
rnystrom@google.com
committed
var lockFile = new LockFile.load(p.join(_directory, file), cache.sources);
return lockFile.packages[name];
}
var packages = listDir(_directory).map((entry) {
if (fileExists(entry)) {
return loadPackageId(entry, p.basenameWithoutExtension(entry));
} else {
return loadPackageId(p.join(entry, 'pubspec.lock'), p.basename(entry));
}
}).toList();
rnystrom@google.com
committed
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}';
}
}