blob: 521f2614c4bdae4b88cf55fbc88be08a0a56c703 [file] [log] [blame]
// 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) => createPackageSymlink(id.name, 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);
}
/**
* Removes the old packages directory, installs all dependencies listed in
* [packageVersions], and writes a [LockFile].
*/
Future _installDependencies(List<PackageId> packageVersions) {
return cleanDir(path).chain((_) {
return Futures.wait(packageVersions.map((id) {
if (id.source is RootSource) return new Future.immediate(id);
return install(id);
}));
}).chain(_saveLockFile)
.chain(_installSelfReference)
.chain(_linkSecondaryPackageDirs);
}
/**
* 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((_) {
// If we failed to load the lockfile but it does exist, something's
// probably wrong and we should notify the user.
fileExists(lockFilePath).transform((exists) {
if (!exists) return;
printError("Error reading pubspec.lock: ${future.exception}");
}).then((_) {
completer.complete(new LockFile.empty());
});
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());
}
/**
* Installs a self-referential symlink in the `packages` directory that will
* allow a package to import its own files using `package:`.
*/
Future _installSelfReference(_) {
var linkPath = join(path, root.name);
return exists(linkPath).chain((exists) {
// Create the symlink if it doesn't exist.
if (exists) return new Future.immediate(null);
return ensureDir(path).chain(
(_) => createPackageSymlink(root.name, root.dir, linkPath,
isSelfLink: true));
});
}
/**
* If `bin/`, `test/`, or `example/` directories exist, symlink `packages/`
* into them so that their entrypoints can be run. Do the same for any
* subdirectories of `test/` and `example/`.
*/
Future _linkSecondaryPackageDirs(_) {
var binDir = join(root.dir, 'bin');
var exampleDir = join(root.dir, 'example');
var testDir = join(root.dir, 'test');
var toolDir = join(root.dir, 'tool');
var webDir = join(root.dir, 'web');
return dirExists(binDir).chain((exists) {
if (!exists) return new Future.immediate(null);
return _linkSecondaryPackageDir(binDir);
}).chain((_) => _linkSecondaryPackageDirsRecursively(exampleDir))
.chain((_) => _linkSecondaryPackageDirsRecursively(testDir))
.chain((_) => _linkSecondaryPackageDirsRecursively(toolDir))
.chain((_) => _linkSecondaryPackageDirsRecursively(webDir));
}
/**
* Creates a symlink to the `packages` directory in [dir] and all its
* subdirectories.
*/
Future _linkSecondaryPackageDirsRecursively(String dir) {
return dirExists(dir).chain((exists) {
if (!exists) return new Future.immediate(null);
return _linkSecondaryPackageDir(dir)
.chain((_) => _listDirWithoutPackages(dir))
.chain((files) {
return Futures.wait(files.map((file) {
return dirExists(file).chain((isDir) {
if (!isDir) return new Future.immediate(null);
return _linkSecondaryPackageDir(file);
});
}));
});
});
}
// TODO(nweiz): roll this into [listDir] in io.dart once issue 4775 is fixed.
/**
* Recursively lists the contents of [dir], excluding hidden `.DS_Store` files
* and `package` files.
*/
Future<List<String>> _listDirWithoutPackages(dir) {
return listDir(dir).chain((files) {
return Futures.wait(files.map((file) {
if (basename(file) == 'packages') return new Future.immediate([]);
return dirExists(file).chain((isDir) {
if (!isDir) return new Future.immediate([]);
return _listDirWithoutPackages(file);
}).transform((subfiles) {
var fileAndSubfiles = [file];
fileAndSubfiles.addAll(subfiles);
return fileAndSubfiles;
});
}));
}).transform(flatten);
}
/**
* Creates a symlink to the `packages` directory in [dir] if none exists.
*/
Future _linkSecondaryPackageDir(String dir) {
var to = join(dir, 'packages');
return exists(to).chain((exists) {
if (exists) return new Future.immediate(null);
return createSymlink(path, to);
});
}
}