From 8bf75818e5ae718553c01e31528c1e8b69652279 Mon Sep 17 00:00:00 2001 From: "rnystrom@google.com" <rnystrom@google.com> Date: Mon, 18 Nov 2013 21:51:24 +0000 Subject: [PATCH] Hook up dependency overrides to the rest of pub. - Parse them in pubspec. - Pass them to solver. - Warn when they are used. - Error on publish. BUG=https://code.google.com/p/dart/issues/detail?id=8566 R=nweiz@google.com Review URL: https://codereview.chromium.org//74013007 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge@30376 260f80e4-7a28-3924-810f-c04153c831b5 --- lib/src/entrypoint.dart | 12 +- lib/src/package.dart | 3 + lib/src/pubspec.dart | 15 ++- lib/src/solver/backtracking_solver.dart | 12 +- lib/src/solver/version_solver.dart | 12 +- lib/src/validator.dart | 2 + lib/src/validator/dependency_override.dart | 28 +++++ test/dependency_override_test.dart | 113 +++++++++++++++++++ test/pubspec_test.dart | 32 ++++++ test/validator/dependency_override_test.dart | 30 +++++ test/version_solver_test.dart | 36 +++--- 11 files changed, 266 insertions(+), 29 deletions(-) create mode 100644 lib/src/validator/dependency_override.dart create mode 100644 test/dependency_override_test.dart create mode 100644 test/validator/dependency_override_test.dart diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart index e9e513fa..b4ef32b8 100644 --- a/lib/src/entrypoint.dart +++ b/lib/src/entrypoint.dart @@ -143,6 +143,16 @@ class Entrypoint { return new Future.sync(() { if (!result.succeeded) throw result.error; + // Warn the user if any overrides were in effect. + if (result.overrides.isNotEmpty) { + var buffer = new StringBuffer(); + buffer.write("Warning: You are overriding these dependencies:"); + for (var override in result.overrides) { + buffer.write("\n- $override"); + } + log.warning(buffer); + } + cleanDir(packagesDir); return Future.wait(result.packages.map((id) { if (id.isRoot) return new Future.value(id); @@ -196,7 +206,7 @@ class Entrypoint { /// pubspec. Future ensureLockFileIsUpToDate() { return new Future.sync(() { - if (isLockFileUpToDate()) return; + if (isLockFileUpToDate()) return null; if (lockFileExists) { log.message( diff --git a/lib/src/package.dart b/lib/src/package.dart index e6aa20cd..6b31e566 100644 --- a/lib/src/package.dart +++ b/lib/src/package.dart @@ -37,6 +37,9 @@ class Package { /// The immediate dev dependencies this package specifies in its pubspec. List<PackageDep> get devDependencies => pubspec.devDependencies; + /// The dependency overrides this package specifies in its pubspec. + List<PackageDep> get dependencyOverrides => pubspec.dependencyOverrides; + /// 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 /// same conventions as pub.dartlang.org for choosing the primary one: the diff --git a/lib/src/pubspec.dart b/lib/src/pubspec.dart index bcef655b..c5c5c1af 100644 --- a/lib/src/pubspec.dart +++ b/lib/src/pubspec.dart @@ -102,6 +102,18 @@ class Pubspec { } List<PackageDep> _devDependencies; + /// The dependency constraints that this package overrides when it is the + /// root package. + /// + /// Dependencies here will replace any dependency on a package with the same + /// name anywhere in the dependency graph. + List<PackageDep> get dependencyOverrides { + if (_dependencyOverrides != null) return _dependencyOverrides; + _dependencyOverrides = _parseDependencies('dependency_overrides'); + return _dependencyOverrides; + } + List<PackageDep> _dependencyOverrides; + /// The ids of the transformers to use for this package. List<Set<TransformerId>> get transformers { if (_transformers != null) return _transformers; @@ -220,7 +232,8 @@ class Pubspec { } Pubspec(this._name, this._version, this._dependencies, this._devDependencies, - this._environment, this._transformers, [Map fields]) + this._dependencyOverrides, this._environment, this._transformers, + [Map fields]) : this.fields = fields == null ? {} : fields, _sources = null, _location = null; diff --git a/lib/src/solver/backtracking_solver.dart b/lib/src/solver/backtracking_solver.dart index 6a0a5397..43009095 100644 --- a/lib/src/solver/backtracking_solver.dart +++ b/lib/src/solver/backtracking_solver.dart @@ -90,7 +90,7 @@ class BacktrackingSolver { var _attemptedSolutions = 1; BacktrackingSolver(SourceRegistry sources, this.root, this.lockFile, - Iterable<PackageDep> overrides, List<String> useLatest) + List<String> useLatest) : sources = sources, cache = new PubspecCache(sources) { for (var package in useLatest) { @@ -98,7 +98,7 @@ class BacktrackingSolver { lockFile.packages.remove(package); } - for (var override in overrides) { + for (var override in root.dependencyOverrides) { _overrides[override.name] = override; } } @@ -110,6 +110,10 @@ class BacktrackingSolver { _logParameters(); + // Sort the overrides by package name to make sure they're deterministic. + var overrides = _overrides.values.toList(); + overrides.sort((a, b) => a.name.compareTo(b.name)); + return newFuture(() { stopwatch.start(); @@ -119,12 +123,12 @@ class BacktrackingSolver { _validateSdkConstraint(root.pubspec); return _traverseSolution(); }).then((packages) { - return new SolveResult(packages, null, attemptedSolutions); + return new SolveResult(packages, overrides, null, attemptedSolutions); }).catchError((error) { if (error is! SolveFailure) throw error; // Wrap a failure in a result so we can attach some other data. - return new SolveResult(null, error, attemptedSolutions); + return new SolveResult(null, overrides, error, attemptedSolutions); }).whenComplete(() { // Gather some solving metrics. var buffer = new StringBuffer(); diff --git a/lib/src/solver/version_solver.dart b/lib/src/solver/version_solver.dart index 30bd2d09..54c5ab46 100644 --- a/lib/src/solver/version_solver.dart +++ b/lib/src/solver/version_solver.dart @@ -25,14 +25,12 @@ import 'backtracking_solver.dart'; /// packages will be used. This is for forcing an upgrade to one or more /// packages. Future<SolveResult> resolveVersions(SourceRegistry sources, Package root, - {LockFile lockFile, List<PackageRef> overrides, List<String> useLatest}) { + {LockFile lockFile, List<String> useLatest}) { if (lockFile == null) lockFile = new LockFile.empty(); - if (overrides == null) overrides = []; if (useLatest == null) useLatest = []; return log.progress('Resolving dependencies', () { - return new BacktrackingSolver(sources, root, lockFile, overrides, - useLatest).solve(); + return new BacktrackingSolver(sources, root, lockFile, useLatest).solve(); }); } @@ -45,6 +43,9 @@ class SolveResult { /// reachable from the root, or `null` if the solver failed. final List<PackageId> packages; + /// The dependency overrides that were used in the solution. + final List<PackageDep> overrides; + /// The error that prevented the solver from finding a solution or `null` if /// it was successful. final SolveFailure error; @@ -55,7 +56,8 @@ class SolveResult { /// solution. final int attemptedSolutions; - SolveResult(this.packages, this.error, this.attemptedSolutions); + SolveResult(this.packages, this.overrides, this.error, + this.attemptedSolutions); String toString() { if (!succeeded) { diff --git a/lib/src/validator.dart b/lib/src/validator.dart index 98ef43e6..640c54ff 100644 --- a/lib/src/validator.dart +++ b/lib/src/validator.dart @@ -11,6 +11,7 @@ import 'log.dart' as log; import 'utils.dart'; import 'validator/compiled_dartdoc.dart'; import 'validator/dependency.dart'; +import 'validator/dependency_override.dart'; import 'validator/directory.dart'; import 'validator/lib.dart'; import 'validator/license.dart'; @@ -55,6 +56,7 @@ abstract class Validator { new NameValidator(entrypoint), new PubspecFieldValidator(entrypoint), new DependencyValidator(entrypoint), + new DependencyOverrideValidator(entrypoint), new DirectoryValidator(entrypoint), new CompiledDartdocValidator(entrypoint), new Utf8ReadmeValidator(entrypoint) diff --git a/lib/src/validator/dependency_override.dart b/lib/src/validator/dependency_override.dart new file mode 100644 index 00000000..a5ce2998 --- /dev/null +++ b/lib/src/validator/dependency_override.dart @@ -0,0 +1,28 @@ +// 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 pub.validator.dependency_override; + +import 'dart:async'; + +import '../entrypoint.dart'; +import '../validator.dart'; + +/// A validator that validates a package's dependencies overrides (or the +/// absence thereof). +class DependencyOverrideValidator extends Validator { + DependencyOverrideValidator(Entrypoint entrypoint) + : super(entrypoint); + + Future validate() { + if (entrypoint.root.dependencyOverrides.isNotEmpty) { + errors.add( + 'Your pubspec.yaml must not have a "dependency_overrides" field.\n' + 'This ensures you test your package against the same versions of ' + 'its dependencies\n' + 'that users will have when they use it.'); + } + return new Future.value(); + } +} diff --git a/test/dependency_override_test.dart b/test/dependency_override_test.dart new file mode 100644 index 00000000..c930dd5b --- /dev/null +++ b/test/dependency_override_test.dart @@ -0,0 +1,113 @@ +// Copyright (c) 2013, 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. + +import 'descriptor.dart' as d; +import 'test_pub.dart'; + +main() { + initConfig(); + forBothPubGetAndUpgrade((command) { + integration("chooses best version matching override constraint", () { + servePackages([ + packageMap("foo", "1.0.0"), + packageMap("foo", "2.0.0"), + packageMap("foo", "3.0.0") + ]); + + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependencies": { + "foo": ">2.0.0" + }, + "dependency_overrides": { + "foo": "<3.0.0" + } + }) + ]).create(); + + pubCommand(command); + + d.packagesDir({ + "foo": "2.0.0" + }).validate(); + }); + + integration("treats override as implicit dependency", () { + servePackages([ + packageMap("foo", "1.0.0") + ]); + + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependency_overrides": { + "foo": "any" + } + }) + ]).create(); + + pubCommand(command); + + d.packagesDir({ + "foo": "1.0.0" + }).validate(); + }); + + integration("ignores other constraints on overridden package", () { + servePackages([ + packageMap("foo", "1.0.0"), + packageMap("foo", "2.0.0"), + packageMap("foo", "3.0.0"), + packageMap("bar", "1.0.0", {"foo": "5.0.0-nonexistent"}) + ]); + + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependencies": { + "bar": "any" + }, + "dependency_overrides": { + "foo": "<3.0.0" + } + }) + ]).create(); + + pubCommand(command); + + d.packagesDir({ + "foo": "2.0.0", + "bar": "1.0.0" + }).validate(); + }); + + integration("warns about overridden dependencies", () { + servePackages([ + packageMap("foo", "1.0.0"), + packageMap("bar", "1.0.0"), + packageMap("baz", "1.0.0") + ]); + + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependency_overrides": { + "foo": "any", + "bar": "any", + "baz": "any" + } + }) + ]).create(); + + schedulePub(args: [command.name], output: command.success, error: + """ + Warning: You are overriding these dependencies: + - bar any from hosted (bar) + - baz any from hosted (baz) + - foo any from hosted (foo) + """); + }); + }); +} \ No newline at end of file diff --git a/test/pubspec_test.dart b/test/pubspec_test.dart index afc04684..70eba930 100644 --- a/test/pubspec_test.dart +++ b/test/pubspec_test.dart @@ -100,6 +100,29 @@ dev_dependencies: expect(pubspec.devDependencies, isEmpty); }); + test("allows a version constraint for dependency overrides", () { + var pubspec = new Pubspec.parse(''' +dependency_overrides: + foo: + mock: ok + version: ">=1.2.3 <3.4.5" +''', sources); + + var foo = pubspec.dependencyOverrides[0]; + expect(foo.name, equals('foo')); + expect(foo.constraint.allows(new Version(1, 2, 3)), isTrue); + expect(foo.constraint.allows(new Version(1, 2, 5)), isTrue); + expect(foo.constraint.allows(new Version(3, 4, 5)), isFalse); + }); + + test("allows an empty dependency overrides map", () { + var pubspec = new Pubspec.parse(''' +dependency_overrides: +''', sources); + + expect(pubspec.dependencyOverrides, isEmpty); + }); + test("allows an unknown source", () { var pubspec = new Pubspec.parse(''' dependencies: @@ -143,6 +166,15 @@ dev_dependencies: ''', (pubspec) => pubspec.devDependencies); }); + test("throws if it has an override on itself", () { + expectPubspecException(''' +name: myapp +dependency_overrides: + myapp: + mock: ok +''', (pubspec) => pubspec.dependencyOverrides); + }); + test("throws if the description isn't valid", () { expectPubspecException(''' dependencies: diff --git a/test/validator/dependency_override_test.dart b/test/validator/dependency_override_test.dart new file mode 100644 index 00000000..6f336a27 --- /dev/null +++ b/test/validator/dependency_override_test.dart @@ -0,0 +1,30 @@ +// Copyright (c) 2013, 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. + +import '../../lib/src/entrypoint.dart'; +import '../../lib/src/validator.dart'; +import '../../lib/src/validator/dependency_override.dart'; +import '../descriptor.dart' as d; +import '../test_pub.dart'; +import 'utils.dart'; + +Validator dependencyOverride(Entrypoint entrypoint) => + new DependencyOverrideValidator(entrypoint); + +main() { + initConfig(); + + integration('invalidates a package if it has dependency overrides', () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependency_overrides": { + "foo": "<3.0.0" + } + }) + ]).create(); + + expectValidationError(dependencyOverride); + }); +} diff --git a/test/version_solver_test.dart b/test/version_solver_test.dart index 00b3e1bb..83551b1e 100644 --- a/test/version_solver_test.dart +++ b/test/version_solver_test.dart @@ -1023,7 +1023,8 @@ _testResolve(void testFn(String description, Function body), var root; packages.forEach((description, dependencies) { var id = parseSpec(description); - var package = mockPackage(id, dependencies); + var package = mockPackage(id, dependencies, + id.name == 'myapp' ? overrides : null); if (id.name == 'myapp') { // Don't add the root package to the server, so we can verify that Pub // doesn't try to look up information about the local package on the @@ -1055,15 +1056,6 @@ _testResolve(void testFn(String description, Function body), }); } - // Parse the overrides. - var realOverrides = []; - if (overrides != null) { - overrides.forEach((spec, constraint) { - realOverrides.add(parseSpec(spec).withConstraint( - new VersionConstraint.parse(constraint))); - }); - } - // Make a version number like the continuous build's version. var previousVersion = sdk.version; if (useBleedingEdgeSdkVersion) { @@ -1071,8 +1063,7 @@ _testResolve(void testFn(String description, Function body), } // Resolve the versions. - var future = resolveVersions(cache.sources, root, - lockFile: realLockFile, overrides: realOverrides); + var future = resolveVersions(cache.sources, root, lockFile: realLockFile); var matcher; if (result != null) { @@ -1331,20 +1322,20 @@ class MockSource extends Source { } } -Package mockPackage(PackageId id, Map dependencyStrings) { +Package mockPackage(PackageId id, Map dependencyStrings, Map overrides) { var sdkConstraint = null; // Build the pubspec dependencies. var dependencies = <PackageDep>[]; var devDependencies = <PackageDep>[]; - dependencyStrings.forEach((description, constraint) { - var isDev = description.startsWith("(dev) "); + dependencyStrings.forEach((spec, constraint) { + var isDev = spec.startsWith("(dev) "); if (isDev) { - description = description.substring("(dev) ".length); + spec = spec.substring("(dev) ".length); } - var dep = parseSpec(description).withConstraint( + var dep = parseSpec(spec).withConstraint( new VersionConstraint.parse(constraint)); if (dep.name == 'sdk') { @@ -1359,8 +1350,17 @@ Package mockPackage(PackageId id, Map dependencyStrings) { } }); + var dependencyOverrides = <PackageDep>[]; + if (overrides != null) { + overrides.forEach((spec, constraint) { + dependencyOverrides.add(parseSpec(spec).withConstraint( + new VersionConstraint.parse(constraint))); + }); + } + var pubspec = new Pubspec(id.name, id.version, dependencies, - devDependencies, new PubspecEnvironment(sdkConstraint), []); + devDependencies, dependencyOverrides, + new PubspecEnvironment(sdkConstraint), []); return new Package.inMemory(pubspec); } -- GitLab