// 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 'entrypoint.dart'; 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); var currentVersion; 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); } else { lockFile = new LockFile.empty(); } return _selectVersion(name, currentVersion, constraint).then((version) { // Make sure it's in the cache. 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; result.showReport(SolveType.GET); // Make sure all of the dependencies are locally installed. return Future.wait(result.packages.map(_cacheDependency)); }).then((ids) { _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)); } /// 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) { try { 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; } on IOException catch (error) { // 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"); }