// 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/cached.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. 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"); /// The source that global packages can be activated from. // TODO(rnystrom): Allow activating packages from other sources. CachedSource get _source => cache.sources["hosted"] as CachedSource; /// 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 activate(String name, VersionConstraint constraint) { // See if we already have it activated. var lockFile; var currentVersion; try { lockFile = new LockFile.load(_getLockFilePath(name), cache.sources); currentVersion = lockFile.packages[name].version; // Pull the root package out of the lock file so the solver doesn't see // it. lockFile.packages.remove(name); log.message("Package ${log.bold(name)} is already active at " "version ${log.bold(currentVersion)}."); } on IOException catch (error) { // If we couldn't read the lock file, it's not activated. lockFile = new LockFile.empty(); } var package; var id; return _selectVersion(name, currentVersion, constraint).then((version) { // Make sure it's in the cache. id = new PackageId(name, _source.name, version, name); return _source.downloadToSystemCache(id); }).then((p) { package = p; // 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((id) { var source = cache.sources[id.source]; if (source is! CachedSource) return new Future.value(); return source.downloadToSystemCache(id) .then((_) => source.resolveId(id)); })); }).then((ids) { var lockFile = new LockFile(ids); // Add the root package itself to the lockfile. lockFile.packages[name] = id; ensureDir(_directory); writeTextFile(_getLockFilePath(name), lockFile.serialize(cache.rootDir, cache.sources)); log.message("Activated ${log.bold(package.name)} ${package.version}."); // TODO(rnystrom): Look in "bin" and display list of binaries that // user can run. }); } /// Deactivates a previously-activated package named [name] or fails with /// an error if [name] is not an active package. void deactivate(String name) { // See if we already have it activated. try { var lockFilePath = p.join(_directory, "$name.lock"); var lockFile = new LockFile.load(lockFilePath, cache.sources); var version = lockFile.packages[name].version; deleteEntry(lockFilePath); log.message("Deactivated package ${log.bold(name)} $version."); } on IOException catch (error) { dataError("No active package ${log.bold(name)}."); } } /// Finds the active packge with [name]. /// /// Returns an [Entrypoint] loaded with the active package if found. Future<Entrypoint> find(String name) { var lockFile; var version; return syncFuture(() { try { lockFile = new LockFile.load(_getLockFilePath(name), cache.sources); version = lockFile.packages[name].version; } on IOException catch (error) { // If we couldn't read the lock file, it's not activated. dataError("No active package ${log.bold(name)}."); } }).then((_) { // Load the package from the cache. var id = new PackageId(name, _source.name, version, name); return _source.getDirectory(id); }).then((dir) { return new Package.load(name, dir, cache.sources); }).then((package) { // Pull the root package out of the lock file so the solver doesn't see // it. lockFile.packages.remove(name); return new Entrypoint.inMemory(package, lockFile, cache); }); } /// Picks the best 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. 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 package with [name]. String _getLockFilePath(name) => p.join(_directory, name + ".lock"); }