// 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 'dart:async'; import '../../pkg/path/lib/path.dart' as path; import 'io.dart'; import 'lock_file.dart'; import 'log.dart' as log; import 'package.dart'; import 'pubspec.dart'; import 'sdk.dart' as sdk; import 'system_cache.dart'; import 'utils.dart'; import 'version.dart'; import 'version_solver.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 _installs = new Map>(); /// Loads the entrypoint from a package at [rootDir]. Entrypoint(String rootDir, SystemCache cache) : root = new Package.load(null, rootDir, cache.sources), cache = cache; // TODO(rnystrom): Make this path configurable. /// The path to the entrypoint's "packages" directory. String get packagesDir => 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 install(PackageId id) { var pendingOrCompleted = _installs[id]; if (pendingOrCompleted != null) return pendingOrCompleted; var packageDir = path.join(packagesDir, id.name); var future = defer(() { ensureDir(path.dirname(packageDir)); if (!dirExists(packageDir)) return; // TODO(nweiz): figure out when to actually delete the directory, and when // we can just re-use the existing symlink. log.fine("Deleting package directory for ${id.name} before install."); return deleteDir(packageDir); }).then((_) { if (id.source.shouldCache) { return cache.install(id).then( (pkg) => createPackageSymlink(id.name, pkg.dir, packageDir)); } else { return id.source.install(id, packageDir).then((found) { if (found) return null; // TODO(nweiz): More robust error-handling. throw 'Package ${id.name} not found in source "${id.source.name}".'; }); } }).then((_) => 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 defer(() { return resolveVersions(cache.sources, root, loadLockFile()); }).then(_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()) .then(_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 dependencies) { return defer(() { var solver = new VersionSolver(cache.sources, root, loadLockFile()); for (var dependency in dependencies) { solver.useLatestVersion(dependency); } return solver.solve(); }).then(_installDependencies); } /// Removes the old packages directory, installs all dependencies listed in /// [packageVersions], and writes a [LockFile]. Future _installDependencies(List packageVersions) { return cleanDir(packagesDir).then((_) { return Future.wait(packageVersions.map((id) { if (id.isRoot) return new Future.immediate(id); return install(id); })); }).then(_saveLockFile) .then(_installSelfReference) .then(_linkSecondaryPackageDirs); } /// Traverses the root's package dependency graph and loads each of the /// reached packages. This should only be called after the lockfile has been /// successfully generated. Future> walkDependencies() { return defer(() { var lockFile = loadLockFile(); var group = new FutureGroup(); var visited = new Set(); // Include the root package in the results. group.add(new Future.immediate(root.pubspec)); visitPackage(Pubspec pubspec) { for (var ref in pubspec.dependencies) { if (visited.contains(ref.name)) continue; // Look up the concrete version. var id = lockFile.packages[ref.name]; visited.add(ref.name); var future; if (ref.name == root.name) { future = new Future.immediate(root.pubspec); } else { future = cache.describe(id); } group.add(future.then(visitPackage)); } return pubspec; } visited.add(root.name); visitPackage(root.pubspec); return group.future; }); } /// Validates that the current Dart SDK version matches the SDK constraints /// of every package in the dependency graph. If a package's constraint does /// not match, prints an error. Future validateSdkConstraints() { return walkDependencies().then((pubspecs) { var errors = []; for (var pubspec in pubspecs) { var sdkConstraint = pubspec.environment.sdkVersion; if (!sdkConstraint.allows(sdk.version)) { errors.add("- '${pubspec.name}' requires ${sdkConstraint}"); } } if (errors.length > 0) { log.error("Some packages are not compatible with your SDK version " "${sdk.version}:\n" "${errors.join('\n')}\n\n" "You may be able to resolve this by upgrading to the latest Dart " "SDK\n" "or adding a version constraint to use an older version of a " "package."); } }); } /// Loads the list of concrete package versions from the `pubspec.lock`, if it /// exists. If it doesn't, this completes to an empty [LockFile]. LockFile loadLockFile() { var lockFilePath = path.join(root.dir, 'pubspec.lock'); if (!fileExists(lockFilePath)) return new LockFile.empty(); return new LockFile.parse(readTextFile(lockFilePath), cache.sources); } /// Saves a list of concrete package versions to the `pubspec.lock` file. void _saveLockFile(List packageIds) { var lockFile = new LockFile.empty(); for (var id in packageIds) { if (!id.isRoot) lockFile.packages[id.name] = id; } var lockFilePath = path.join(root.dir, 'pubspec.lock'); writeTextFile(lockFilePath, 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(_) { return defer(() { var linkPath = path.join(packagesDir, root.name); // Create the symlink if it doesn't exist. if (entryExists(linkPath)) return; ensureDir(packagesDir); return 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 = path.join(root.dir, 'bin'); var exampleDir = path.join(root.dir, 'example'); var testDir = path.join(root.dir, 'test'); var toolDir = path.join(root.dir, 'tool'); var webDir = path.join(root.dir, 'web'); return defer(() { if (!dirExists(binDir)) return; return _linkSecondaryPackageDir(binDir); }).then((_) => _linkSecondaryPackageDirsRecursively(exampleDir)) .then((_) => _linkSecondaryPackageDirsRecursively(testDir)) .then((_) => _linkSecondaryPackageDirsRecursively(toolDir)) .then((_) => _linkSecondaryPackageDirsRecursively(webDir)); } /// Creates a symlink to the `packages` directory in [dir] and all its /// subdirectories. Future _linkSecondaryPackageDirsRecursively(String dir) { return defer(() { if (!dirExists(dir)) return; return _linkSecondaryPackageDir(dir) .then((_) => _listDirWithoutPackages(dir)) .then((files) { return Future.wait(files.map((file) { return defer(() { if (!dirExists(file)) return; 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> _listDirWithoutPackages(dir) { return listDir(dir).then((files) { return Future.wait(files.map((file) { if (path.basename(file) == 'packages') return new Future.immediate([]); return defer(() { if (!dirExists(file)) return []; return _listDirWithoutPackages(file); }).then((subfiles) { var fileAndSubfiles = [file]; fileAndSubfiles.addAll(subfiles); return fileAndSubfiles; }); })); }).then(flatten); } /// Creates a symlink to the `packages` directory in [dir] if none exists. Future _linkSecondaryPackageDir(String dir) { return defer(() { var to = path.join(dir, 'packages'); if (entryExists(to)) return; return createSymlink(packagesDir, to); }); } }