| // 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); |
| }); |
| } |
| } |