Validate packages against their SDK constraints.

After running pub install or update, it will look at all of the
selected packages and see if their SDK constraints match the
current SDK. If not, it will show a (hopefully) helpful error
message.
BUG=dartbug.com/6285

Review URL: https://codereview.chromium.org//12092080

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@18014 260f80e4-7a28-3924-810f-c04153c831b5
This commit is contained in:
rnystrom@google.com 2013-02-01 23:45:26 +00:00
parent c651283897
commit 4a9cc0f619
21 changed files with 332 additions and 52 deletions

View file

@ -10,14 +10,14 @@ import 'entrypoint.dart';
import 'log.dart' as log;
import 'pub.dart';
/// Handles the `install` pub command.
/// Handles the `install` pub command.
class InstallCommand extends PubCommand {
String get description => "Install the current package's dependencies.";
String get usage => "pub install";
Future onRun() {
return entrypoint.installDependencies().then((_) {
log.message("Dependencies installed!");
});
return entrypoint.installDependencies()
.then((_) => log.message("Dependencies installed!"))
.then((_) => entrypoint.validateSdkConstraints());
}
}

View file

@ -9,7 +9,7 @@ import 'entrypoint.dart';
import 'log.dart' as log;
import 'pub.dart';
/// Handles the `update` pub command.
/// Handles the `update` pub command.
class UpdateCommand extends PubCommand {
String get description =>
"Update the current package's dependencies to the latest versions.";
@ -23,6 +23,8 @@ class UpdateCommand extends PubCommand {
} else {
future = entrypoint.updateDependencies(commandOptions.rest);
}
return future.then((_) => log.message("Dependencies updated!"));
return future
.then((_) => log.message("Dependencies updated!"))
.then((_) => entrypoint.validateSdkConstraints());
}
}

View file

@ -9,6 +9,7 @@ import 'io.dart';
import 'lock_file.dart';
import 'log.dart' as log;
import 'package.dart';
import 'sdk.dart' as sdk;
import 'system_cache.dart';
import 'utils.dart';
import 'version.dart';
@ -139,6 +140,63 @@ class Entrypoint {
.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<List<Package>> walkDependencies() {
return loadLockFile().then((lockFile) {
var group = new FutureGroup<Package>();
var visited = new Set<String>();
// Include the root package in the results.
group.add(new Future.immediate(root));
visitPackage(Package package) {
for (var ref in package.dependencies) {
if (visited.contains(ref.name)) continue;
// Look up the concrete version.
var id = lockFile.packages[ref.name];
visited.add(ref.name);
var future = cache.describe(id);
group.add(future.then(visitPackage));
}
return package;
}
visitPackage(root);
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((packages) {
var errors = [];
for (var package in packages) {
var sdkConstraint = package.pubspec.environment.sdkVersion;
if (!sdkConstraint.allows(sdk.version)) {
errors.add("- '${package.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].
Future<LockFile> loadLockFile() {

View file

@ -60,6 +60,8 @@ class GitSource extends Source {
});
}
Future<String> systemCacheDirectory(PackageId id) => _revisionCachePath(id);
/// Ensures [description] is a Git URL.
void validateDescription(description, {bool fromLockFile: false}) {
// A single string is assumed to be a Git URL.

View file

@ -93,13 +93,15 @@ class HostedSource extends Source {
/// for each separate repository URL that's used on the system. Each of these
/// subdirectories then contains a subdirectory for each package installed
/// from that site.
String systemCacheDirectory(PackageId id) {
Future<String> systemCacheDirectory(PackageId id) {
var parsed = _parseDescription(id.description);
var url = parsed.last.replaceAll(new RegExp(r"^https?://"), "");
var urlDir = replace(url, new RegExp(r'[<>:"\\/|?*%]'), (match) {
return '%${match[0].charCodeAt(0)}';
});
return join(systemCacheRoot, urlDir, "${parsed.first}-${id.version}");
return new Future.immediate(
join(systemCacheRoot, urlDir, "${parsed.first}-${id.version}"));
}
String packageName(description) => _parseDescription(description).first;

View file

@ -74,6 +74,7 @@ Future<bool> fileExists(file) {
(exists) => "File $path ${exists ? 'exists' : 'does not exist'}.");
}
// TODO(rnystrom): Get rid of this and only use sync.
/// Reads the contents of the text file [file], which can either be a [String]
/// or a [File].
Future<String> readTextFile(file) {
@ -90,6 +91,23 @@ Future<String> readTextFile(file) {
});
}
/// Reads the contents of the text file [file], which can either be a [String]
/// or a [File].
String readTextFileSync(file) {
var path = _getPath(file);
log.io("Reading text file $path.");
var contents = new File(path).readAsStringSync(Encoding.UTF_8);
// Sanity check: don't spew a huge file.
if (contents.length < 1024 * 1024) {
log.fine("Read $path. Contents:\n$contents");
} else {
log.fine("Read ${contents.length} characters from $path.");
}
return contents;
}
/// Reads the contents of the binary file [file], which can either be a [String]
/// or a [File].
List<int> readBinaryFile(file) {
@ -289,6 +307,7 @@ Future<List<String>> listDir(dir,
return doList(_getDirectory(dir), new Set<String>());
}
// TODO(rnystrom): Migrate everything over to the sync one and get rid of this.
/// Asynchronously determines if [dir], which can be a [String] directory path
/// or a [Directory], exists on the file system. Returns a [Future] that
/// completes with the result.
@ -300,6 +319,11 @@ Future<bool> dirExists(dir) {
"${exists ? 'exists' : 'does not exist'}.");
}
/// Determines if [dir], which can be a [String] directory path or a
/// [Directory], exists on the file system. Returns a [Future] that completes
/// with the result.
bool dirExistsSync(dir) => _getDirectory(dir).existsSync();
/// "Cleans" [dir]. If that directory already exists, it will be deleted. Then a
/// new empty directory will be created. Returns a [Future] that completes when
/// the new clean directory is created.

View file

@ -58,7 +58,7 @@ class Package {
/// The ids of the packages that this package depends on. This is what is
/// specified in the pubspec when this package depends on another.
Collection<PackageRef> get dependencies => pubspec.dependencies;
List<PackageRef> get dependencies => pubspec.dependencies;
/// Returns the path to the README file at the root of the entrypoint, or null
/// if no README file is found. If multiple READMEs are found, this uses the
@ -124,6 +124,10 @@ class PackageId implements Comparable {
int get hashCode => name.hashCode ^ source.hashCode ^ version.hashCode;
/// Gets the directory where this package is or would be found in the
/// [SystemCache].
Future<String> get systemCacheDirectory => source.systemCacheDirectory(this);
bool operator ==(other) {
if (other is! PackageId) return false;
// TODO(rnystrom): We're assuming here the name/version/source tuple is

View file

@ -4,12 +4,13 @@
library pubspec;
import '../../pkg/yaml/lib/yaml.dart';
import 'package.dart';
import 'source.dart';
import 'source_registry.dart';
import 'utils.dart';
import 'version.dart';
import '../../pkg/yaml/lib/yaml.dart';
/// The parsed and validated contents of a pubspec file.
class Pubspec {
@ -44,6 +45,7 @@ class Pubspec {
bool get isEmpty =>
name == null && version == Version.none && dependencies.isEmpty;
// TODO(rnystrom): Make this a static method to match corelib.
/// Parses the pubspec whose text is [contents]. If the pubspec doesn't define
/// version for itself, it defaults to [Version.none].
factory Pubspec.parse(String contents, SourceRegistry sources) {

View file

@ -104,13 +104,14 @@ abstract class Source {
///
/// By default, this uses [systemCacheDirectory] and [install].
Future<Package> installToSystemCache(PackageId id) {
var path = systemCacheDirectory(id);
return exists(path).then((exists) {
if (exists) return new Future<bool>.immediate(true);
return ensureDir(dirname(path)).then((_) => install(id, path));
}).then((found) {
if (!found) throw 'Package $id not found.';
return Package.load(id.name, path, systemCache.sources);
return systemCacheDirectory(id).then((path) {
return exists(path).then((exists) {
if (exists) return new Future<bool>.immediate(true);
return ensureDir(dirname(path)).then((_) => install(id, path));
}).then((found) {
if (!found) throw 'Package $id not found.';
return Package.load(id.name, path, systemCache.sources);
});
});
}
@ -118,11 +119,10 @@ abstract class Source {
/// [id] should be installed to. This should return a path to a subdirectory
/// of [systemCacheRoot].
///
/// This doesn't need to be implemented if [shouldCache] is false, or if
/// [installToSystemCache] is implemented.
String systemCacheDirectory(PackageId id) {
throw 'Source.systemCacheDirectory must be implemented if shouldCache is '
'true and installToSystemCache is not implemented.';
/// This doesn't need to be implemented if [shouldCache] is false.
Future<String> systemCacheDirectory(PackageId id) {
return new Future.immediateError(
"systemCacheDirectory() must be implemented if shouldCache is true.");
}
/// When a [Pubspec] or [LockFile] is parsed, it reads in the description for

View file

@ -13,6 +13,7 @@ import 'io.dart';
import 'io.dart' as io show createTempDir;
import 'log.dart' as log;
import 'package.dart';
import 'pubspec.dart';
import 'sdk_source.dart';
import 'source.dart';
import 'source_registry.dart';
@ -59,6 +60,26 @@ class SystemCache {
sources.register(source);
}
/// Gets the package identified by [id]. If the package is already cached,
/// reads it from the cache. Otherwise, requests it from the source.
Future<Package> describe(PackageId id) {
Future<Package> getUncached() {
// Not cached, so get it from the source.
return id.describe().then((pubspec) => new Package.inMemory(pubspec));
}
// Try to get it from the system cache first.
if (id.source.shouldCache) {
return id.systemCacheDirectory.then((packageDir) {
if (!dirExistsSync(packageDir)) return getUncached();
return Package.load(id.name, packageDir, sources);
});
}
// Not cached, so get it from the source.
return getUncached();
}
/// Ensures that the package identified by [id] is installed to the cache,
/// loads it, and returns it.
///

View file

@ -27,6 +27,47 @@ class Pair<E, F> {
int get hashCode => first.hashCode ^ last.hashCode;
}
/// A completer that waits until all added [Future]s complete.
// TODO(rnystrom): Copied from web_components. Remove from here when it gets
// added to dart:core. (See #6626.)
class FutureGroup<T> {
int _pending = 0;
Completer<List<T>> _completer = new Completer<List<T>>();
final List<Future<T>> futures = <Future<T>>[];
bool completed = false;
final List<T> _values = <T>[];
/// Wait for [task] to complete.
Future<T> add(Future<T> task) {
if (completed) {
throw new StateError("The FutureGroup has already completed.");
}
_pending++;
futures.add(task.then((value) {
if (completed) return;
_pending--;
_values.add(value);
if (_pending <= 0) {
completed = true;
_completer.complete(_values);
}
}).catchError((e) {
if (completed) return;
completed = true;
_completer.completeError(e.error, e.stackTrace);
}));
return task;
}
Future<List> get future => _completer.future;
}
// TODO(rnystrom): Move into String?
/// Pads [source] to [length] by adding spaces at the end.
String padRight(String source, int length) {

View file

@ -14,7 +14,7 @@ main() {
git('foo.git', [
libDir('foo'),
libPubspec('foo', '1.0.0', [{"git": "../bar.git"}])
libPubspec('foo', '1.0.0', deps: [{"git": "../bar.git"}])
]).scheduleCreate();
git('bar.git', [

View file

@ -67,7 +67,6 @@ main() {
integration('does not add a package if it does not have a "lib" directory', () {
// Using an SDK source, but this should be true of all sources.
dir(sdkPath, [
file('version', '0.1.2.3'),
dir('pkg', [
dir('foo', [
libPubspec('foo', '0.0.0-not.used')

View file

@ -11,7 +11,6 @@ import '../../test_pub.dart';
main() {
integration('checks out a package from the SDK', () {
dir(sdkPath, [
file('version', '0.1.2.3'),
dir('pkg', [
dir('foo', [
libDir('foo', 'foo 0.1.2+3'),

View file

@ -11,11 +11,10 @@ import '../../test_pub.dart';
main() {
integration('includes transitive dependencies', () {
dir(sdkPath, [
file('version', '0.1.2.3'),
dir('pkg', [
dir('foo', [
libDir('foo', 'foo 0.1.2+3'),
libPubspec('foo', '0.0.0-not.used', [{'sdk': 'bar'}])
libPubspec('foo', '0.0.0-not.used', deps: [{'sdk': 'bar'}])
]),
dir('bar', [
libDir('bar', 'bar 0.1.2+3'),

View file

@ -55,10 +55,6 @@ main() {
});
integration('running pub with just --version displays version', () {
dir(sdkPath, [
file('version', '0.1.2.3'),
]).scheduleCreate();
schedulePub(args: ['--version'], output: VERSION_STRING);
});
@ -125,10 +121,6 @@ main() {
group('version', () {
integration('displays the current version', () {
dir(sdkPath, [
file('version', '0.1.2.3'),
]).scheduleCreate();
schedulePub(args: ['version'], output: VERSION_STRING);
});

View file

@ -0,0 +1,121 @@
// 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 check_sdk_test;
import "test_pub.dart";
import "../../../pkg/unittest/lib/unittest.dart";
main() {
initConfig();
for (var command in ["install", "update"]) {
var success = new RegExp(r"Dependencies installed!$");
if (command == "update") {
success = new RegExp(r"Dependencies updated!$");
}
integration("gives a friendly message if there are no constraints", () {
dir(appPath, [
pubspec({"name": "myapp"}),
]).scheduleCreate();
schedulePub(args: [command], output: success);
});
integration("gives an error if the root package does not match", () {
dir(appPath, [
pubspec({
"name": "myapp",
"environment": {"sdk": ">2.0.0"}
})
]).scheduleCreate();
schedulePub(args: [command],
error:
"""
Some packages are not compatible with your SDK version 0.1.2+3:
- 'myapp' requires >2.0.0
You may be able to resolve this by upgrading to the latest Dart SDK
or adding a version constraint to use an older version of a package.
""");
});
integration("gives an error if some dependencies do not match", () {
// Using an SDK source, but this should be true of all sources.
dir(sdkPath, [
dir("pkg", [
dir("foo", [
libPubspec("foo", "0.0.1", sdk: ">0.1.3"),
libDir("foo")
]),
dir("bar", [
libPubspec("bar", "0.0.1", sdk: ">0.1.1"),
libDir("bar")
])
])
]).scheduleCreate();
dir(appPath, [
pubspec({
"name": "myapp",
"dependencies": {
"foo": { "sdk": "foo" },
"bar": { "sdk": "bar" }
},
"environment": {"sdk": ">2.0.0"}
})
]).scheduleCreate();
schedulePub(args: [command],
error:
"""
Some packages are not compatible with your SDK version 0.1.2+3:
- 'myapp' requires >2.0.0
- 'foo' requires >0.1.3
You may be able to resolve this by upgrading to the latest Dart SDK
or adding a version constraint to use an older version of a package.
""");
});
integration("gives an error if a transitive dependency doesn't match", () {
// Using an SDK source, but this should be true of all sources.
dir(sdkPath, [
dir("pkg", [
dir("foo", [
libPubspec("foo", "0.0.1", deps: [
{"sdk": "bar"}
]),
libDir("foo")
]),
dir("bar", [
libPubspec("bar", "0.0.1", sdk: "<0.1.1"),
libDir("bar")
])
])
]).scheduleCreate();
dir(appPath, [
pubspec({
"name": "myapp",
"dependencies": {
"foo": { "sdk": "foo" }
}
})
]).scheduleCreate();
schedulePub(args: [command],
error:
"""
Some packages are not compatible with your SDK version 0.1.2+3:
- 'bar' requires <0.1.1
You may be able to resolve this by upgrading to the latest Dart SDK
or adding a version constraint to use an older version of a package.
""");
});
}
}

View file

@ -242,9 +242,19 @@ Descriptor appPubspec(List dependencies) {
}
/// Describes a file named `pubspec.yaml` for a library package with the given
/// [name], [version], and [dependencies].
Descriptor libPubspec(String name, String version, [List dependencies]) =>
pubspec(package(name, version, dependencies));
/// [name], [version], and [deps]. If "sdk" is given, then it adds an SDK
/// constraint on that version.
Descriptor libPubspec(String name, String version, {List deps, String sdk}) {
var map = package(name, version, deps);
if (sdk != null) {
map["environment"] = {
"sdk": sdk
};
}
return pubspec(map);
}
/// Describes a directory named `lib` containing a single dart file named
/// `<name>.dart` that contains a line of Dart code.
@ -466,6 +476,11 @@ final _TIMEOUT = 30000;
/// operations which will be run asynchronously.
void integration(String description, void body()) {
test(description, () {
// Ensure the SDK version is always available.
dir(sdkPath, [
file('version', '0.1.2.3')
]).scheduleCreate();
// Schedule the test.
body();

View file

@ -15,7 +15,7 @@ main() {
git('foo.git', [
libDir('foo'),
libPubspec("foo", "1.0.0", [{"git": "../foo-dep.git"}])
libPubspec("foo", "1.0.0", deps: [{"git": "../foo-dep.git"}])
]).scheduleCreate();
git('foo-dep.git', [
@ -39,7 +39,7 @@ main() {
git('foo.git', [
libDir('foo', 'foo 2'),
libPubspec("foo", "1.0.0", [{"git": "../foo-dep.git"}])
libPubspec("foo", "1.0.0", deps: [{"git": "../foo-dep.git"}])
]).scheduleCreate();
git('foo-dep.git', [

View file

@ -69,7 +69,6 @@ main() {
'directory', () {
// Using an SDK source, but this should be true of all sources.
dir(sdkPath, [
file('version', '0.1.2.3'),
dir('pkg', [
dir('foo', [
libPubspec('foo', '0.0.0-not.used')

View file

@ -127,7 +127,7 @@ main() {
integration('has an unconstrained dependency on "unittest"', () {
dir(appPath, [
libPubspec("test_pkg", "1.0.0", [
libPubspec("test_pkg", "1.0.0", deps: [
{'hosted': 'unittest'}
])
]).scheduleCreate();
@ -342,7 +342,7 @@ main() {
}));
dir(appPath, [
libPubspec("test_pkg", "1.0.0", [
libPubspec("test_pkg", "1.0.0", deps: [
{'git': 'git://github.com/dart-lang/foo'}
])
]).scheduleCreate();
@ -366,7 +366,7 @@ main() {
}));
dir(appPath, [
libPubspec("test_pkg", "1.0.0", [
libPubspec("test_pkg", "1.0.0", deps: [
{'git': 'git://github.com/dart-lang/foo'}
])
]).scheduleCreate();
@ -390,7 +390,7 @@ main() {
}));
dir(appPath, [
libPubspec("test_pkg", "1.0.0", [
libPubspec("test_pkg", "1.0.0", deps: [
{'git': 'git://github.com/dart-lang/foo'}
])
]).scheduleCreate();
@ -411,7 +411,7 @@ main() {
}));
dir(appPath, [
libPubspec("test_pkg", "1.0.0", [
libPubspec("test_pkg", "1.0.0", deps: [
{
'git': {'url': 'git://github.com/dart-lang/foo'},
'version': '>=1.0.0 <2.0.0'
@ -434,7 +434,7 @@ main() {
}));
dir(appPath, [
libPubspec("test_pkg", "1.0.0", [
libPubspec("test_pkg", "1.0.0", deps: [
{
'git': {'url': 'git://github.com/dart-lang/foo'},
'version': '0.2.3'
@ -452,7 +452,7 @@ main() {
group('and it should not suggest a version', () {
integration("if there's no lockfile", () {
dir(appPath, [
libPubspec("test_pkg", "1.0.0", [
libPubspec("test_pkg", "1.0.0", deps: [
{'hosted': 'foo'}
])
]).scheduleCreate();
@ -464,7 +464,7 @@ main() {
integration("if the lockfile doesn't have an entry for the "
"dependency", () {
dir(appPath, [
libPubspec("test_pkg", "1.0.0", [
libPubspec("test_pkg", "1.0.0", deps: [
{'hosted': 'foo'}
]),
file("pubspec.lock", json.stringify({
@ -490,7 +490,7 @@ main() {
integration('and it should suggest a constraint based on the locked '
'version', () {
dir(appPath, [
libPubspec("test_pkg", "1.0.0", [
libPubspec("test_pkg", "1.0.0", deps: [
{'hosted': 'foo'}
]),
file("pubspec.lock", json.stringify({
@ -515,7 +515,7 @@ main() {
integration('and it should suggest a concrete constraint if the locked '
'version is pre-1.0.0', () {
dir(appPath, [
libPubspec("test_pkg", "1.0.0", [
libPubspec("test_pkg", "1.0.0", deps: [
{'hosted': 'foo'}
]),
file("pubspec.lock", json.stringify({
@ -541,7 +541,7 @@ main() {
integration('has a hosted dependency on itself', () {
dir(appPath, [
libPubspec("test_pkg", "1.0.0", [
libPubspec("test_pkg", "1.0.0", deps: [
{'hosted': {'name': 'test_pkg', 'version': '>=1.0.0'}}
])
]).scheduleCreate();