8a0aabe483
If the pubspec has a new dependency which causes an incompatibility with the locked packages' version constraints, `pub install` will now unlock packages one by one until a compatible version graph can be found. `pub update <package>` will now always update the given package to its latest version, even if that requires updating locked packages that depend on it transitively. If no amount of updating will make the latest version valid, an exception will be thrown. This was originally submitted as r10779 Review URL: https://chromiumcodereview.appspot.com//10837274 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@11119 260f80e4-7a28-3924-810f-c04153c831b5
192 lines
6.5 KiB
Dart
192 lines
6.5 KiB
Dart
// Copyright (c) 2012, 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('entrypoint');
|
|
|
|
#import('io.dart');
|
|
#import('lock_file.dart');
|
|
#import('package.dart');
|
|
#import('root_source.dart');
|
|
#import('system_cache.dart');
|
|
#import('version.dart');
|
|
#import('version_solver.dart');
|
|
#import('utils.dart');
|
|
|
|
/**
|
|
* Pub operates over a directed graph of dependencies that starts at a root
|
|
* "entrypoint" package. This is typically the package where the current
|
|
* working directory is located. An entrypoint knows the [root] package it is
|
|
* associated with and is responsible for managing the "packages" directory
|
|
* for it.
|
|
*
|
|
* That directory contains symlinks to all packages used by an app. These links
|
|
* point either to the [SystemCache] or to some other location on the local
|
|
* filesystem.
|
|
*
|
|
* While entrypoints are typically applications, a pure library package may end
|
|
* up being used as an entrypoint. Also, a single package may be used as an
|
|
* entrypoint in one context but not in another. For example, a package that
|
|
* contains a reusable library may not be the entrypoint when used by an app,
|
|
* but may be the entrypoint when you're running its tests.
|
|
*/
|
|
class Entrypoint {
|
|
/**
|
|
* The root package this entrypoint is associated with.
|
|
*/
|
|
final Package root;
|
|
|
|
/**
|
|
* The system-wide cache which caches packages that need to be fetched over
|
|
* the network.
|
|
*/
|
|
final SystemCache cache;
|
|
|
|
/**
|
|
* Packages which are either currently being asynchronously installed to the
|
|
* directory, or have already been installed.
|
|
*/
|
|
final Map<PackageId, Future<PackageId>> _installs;
|
|
|
|
Entrypoint(this.root, this.cache)
|
|
: _installs = new Map<PackageId, Future<PackageId>>();
|
|
|
|
/**
|
|
* The path to this "packages" directory.
|
|
*/
|
|
// TODO(rnystrom): Make this path configurable.
|
|
String get path() => join(root.dir, 'packages');
|
|
|
|
/**
|
|
* Ensures that the package identified by [id] is installed to the directory.
|
|
* Returns the resolved [PackageId].
|
|
*
|
|
* If this completes successfully, the package is guaranteed to be importable
|
|
* using the `package:` scheme.
|
|
*
|
|
* This will automatically install the package to the system-wide cache as
|
|
* well if it requires network access to retrieve (specifically, if
|
|
* `id.source.shouldCache` is true).
|
|
*
|
|
* See also [installDependencies].
|
|
*/
|
|
Future<PackageId> install(PackageId id) {
|
|
var pendingOrCompleted = _installs[id];
|
|
if (pendingOrCompleted != null) return pendingOrCompleted;
|
|
|
|
var packageDir = join(path, id.name);
|
|
var future = ensureDir(dirname(packageDir)).chain((_) {
|
|
return exists(packageDir);
|
|
}).chain((exists) {
|
|
if (!exists) return new Future.immediate(null);
|
|
// TODO(nweiz): figure out when to actually delete the directory, and when
|
|
// we can just re-use the existing symlink.
|
|
return deleteDir(packageDir);
|
|
}).chain((_) {
|
|
if (id.source.shouldCache) {
|
|
return cache.install(id).chain(
|
|
(pkg) => createSymlink(pkg.dir, packageDir));
|
|
} else {
|
|
return id.source.install(id, packageDir).transform((found) {
|
|
if (found) return null;
|
|
// TODO(nweiz): More robust error-handling.
|
|
throw 'Package ${id.name} not found in source "${id.source.name}".';
|
|
});
|
|
}
|
|
}).chain((_) => id.resolved);
|
|
|
|
_installs[id] = future;
|
|
|
|
return future;
|
|
}
|
|
|
|
/**
|
|
* Installs all dependencies of the [root] package to its "packages"
|
|
* directory, respecting the [LockFile] if present. Returns a [Future] that
|
|
* completes when all dependencies are installed.
|
|
*/
|
|
Future installDependencies() {
|
|
return _loadLockFile()
|
|
.chain((lockFile) => resolveVersions(cache.sources, root, lockFile))
|
|
.chain(_installDependencies);
|
|
}
|
|
|
|
/**
|
|
* Installs the latest available versions of all dependencies of the [root]
|
|
* package to its "package" directory, writing a new [LockFile]. Returns a
|
|
* [Future] that completes when all dependencies are installed.
|
|
*/
|
|
Future updateAllDependencies() {
|
|
return resolveVersions(cache.sources, root, new LockFile.empty()).
|
|
chain(_installDependencies);
|
|
}
|
|
|
|
/**
|
|
* Installs the latest available versions of [dependencies], while leaving
|
|
* other dependencies as specified by the [LockFile] if possible. Returns a
|
|
* [Future] that completes when all dependencies are installed.
|
|
*/
|
|
Future updateDependencies(List<String> dependencies) {
|
|
return _loadLockFile().chain((lockFile) {
|
|
var versionSolver = new VersionSolver(cache.sources, root, lockFile);
|
|
for (var dependency in dependencies) {
|
|
versionSolver.useLatestVersion(dependency);
|
|
}
|
|
return versionSolver.solve();
|
|
}).chain(_installDependencies);
|
|
}
|
|
|
|
/**
|
|
* Installs all dependencies listed in [packageVersions] and writes a
|
|
* [LockFile].
|
|
*/
|
|
Future _installDependencies(List<PackageId> packageVersions) {
|
|
return Futures.wait(packageVersions.map((id) {
|
|
if (id.source is RootSource) return new Future.immediate(id);
|
|
return install(id);
|
|
})).chain(_saveLockFile);
|
|
}
|
|
|
|
/**
|
|
* Loads the list of concrete package versions from the `pubspec.lock`, if it
|
|
* exists. If it doesn't, this completes to an empty [LockFile].
|
|
*
|
|
* If there's an error reading the `pubspec.lock` file, this will print a
|
|
* warning message and act as though the file doesn't exist.
|
|
*/
|
|
Future<LockFile> _loadLockFile() {
|
|
var completer = new Completer<LockFile>();
|
|
var lockFilePath = join(root.dir, 'pubspec.lock');
|
|
var future = readTextFile(lockFilePath);
|
|
|
|
future.handleException((_) {
|
|
completer.complete(new LockFile.empty());
|
|
|
|
// If we failed to load the lockfile but it does exist, something's
|
|
// probably wrong and we should notify the user.
|
|
fileExists(lockFilePath).then((exists) {
|
|
if (!exists) return;
|
|
printError("Error reading pubspec.lock: ${future.exception}");
|
|
});
|
|
|
|
return true;
|
|
});
|
|
|
|
future.then((text) =>
|
|
completer.complete(new LockFile.parse(text, cache.sources)));
|
|
return completer.future;
|
|
}
|
|
|
|
/**
|
|
* Saves a list of concrete package versions to the `pubspec.lock` file.
|
|
*/
|
|
Future _saveLockFile(List<PackageId> packageIds) {
|
|
var lockFile = new LockFile.empty();
|
|
for (var id in packageIds) {
|
|
if (id.source is! RootSource) lockFile.packages[id.name] = id;
|
|
}
|
|
|
|
return writeTextFile(join(root.dir, 'pubspec.lock'), lockFile.serialize());
|
|
}
|
|
}
|