diff --git a/lib/src/command_runner.dart b/lib/src/command_runner.dart index 8d2b6f83f..47fc7e255 100644 --- a/lib/src/command_runner.dart +++ b/lib/src/command_runner.dart @@ -30,7 +30,6 @@ import 'http.dart'; import 'io.dart'; import 'log.dart' as log; import 'sdk.dart' as sdk; -import 'solver.dart'; import 'utils.dart'; class PubCommandRunner extends CommandRunner { @@ -232,12 +231,13 @@ and include the logs in an issue on https://github.com/dart-lang/pub/issues/new int _chooseExitCode(exception) { while (exception is WrappedException) exception = exception.innerError; + // TODO(nweiz): Emit an UNAVAILABLE exit code for a SolveFailure that was + // (transitively) caused by a package not existing. if (exception is HttpException || exception is http.ClientException || exception is SocketException || exception is TlsException || - exception is PubHttpException || - exception is DependencyNotFoundException) { + exception is PubHttpException) { return exit_codes.UNAVAILABLE; } else if (exception is FormatException || exception is DataException) { return exit_codes.DATA; diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart index 443406213..bb9ea351d 100644 --- a/lib/src/entrypoint.dart +++ b/lib/src/entrypoint.dart @@ -214,8 +214,6 @@ class Entrypoint { } } - if (!result.succeeded) throw result.error; - result.showReport(type); if (dryRun) { diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart index d5baae250..5a8cb4cd5 100644 --- a/lib/src/global_packages.dart +++ b/lib/src/global_packages.dart @@ -172,14 +172,11 @@ class GlobalPackages { dependencies: [dep], sources: cache.sources)); // Resolve it and download its dependencies. + // + // TODO(nweiz): If this produces a SolveFailure that's caused by [dep] not + // being available, report that as a [dataError]. var result = await resolveVersions(SolveType.GET, cache, root); - if (!result.succeeded) { - // If the package specified by the user doesn't exist, we want to - // surface that as a [DataError] with the associated exit code. - if (result.error.package != dep.name) throw result.error; - if (result.error is NoVersionException) dataError(result.error.message); - throw result.error; - } + result.showReport(SolveType.GET); // Make sure all of the dependencies are locally installed. diff --git a/lib/src/package_name.dart b/lib/src/package_name.dart index 265617ab6..dc1c6d2ba 100644 --- a/lib/src/package_name.dart +++ b/lib/src/package_name.dart @@ -102,8 +102,7 @@ class PackageRef extends PackageName { } String toTerseString() { - if (isMagic) return name; - if (isRoot || source.name != 'hosted') return "$name"; + if (isMagic || isRoot || source.name == 'hosted') return name; return "$name from $source"; } @@ -157,8 +156,8 @@ class PackageId extends PackageName { } String toTerseString() { - if (isMagic) return name; - if (isRoot || source.name == 'hosted') return "$name $version"; + if (isMagic || isRoot) return name; + if (source.name == 'hosted') return "$name $version"; return "$name $version from $source"; } } @@ -230,8 +229,8 @@ class PackageRange extends PackageName { } String toTerseString() { - if (isMagic) return name; - if (isRoot || source.name == 'hosted') return "$name $constraint"; + if (isMagic || isRoot) return name; + if (source.name == 'hosted') return "$name $constraint"; return "$name $constraint from $source"; } @@ -242,6 +241,23 @@ class PackageRange extends PackageName { features: new Map.from(this.features)..addAll(features)); } + /// Returns a copy of [this] with the same semantics, but with a `^`-style + /// constraint if possible. + PackageRange withTerseConstraint() { + if (constraint is! VersionRange) return this; + if (constraint.toString().startsWith("^")) return this; + + var range = constraint as VersionRange; + if (range.includeMin && + !range.includeMax && + range.min != null && + range.max == range.min.nextBreaking) { + return withConstraint(new VersionConstraint.compatibleWith(range.min)); + } else { + return this; + } + } + /// Whether [id] satisfies this dependency. /// /// Specifically, whether [id] refers to the same package as [this] *and* diff --git a/lib/src/solver/dependency.dart b/lib/src/solver/dependency.dart deleted file mode 100644 index 290730cd5..000000000 --- a/lib/src/solver/dependency.dart +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2017, 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 '../package_name.dart'; - -/// A reference from a depending package to a package that it depends on. -class Dependency { - /// The package that has this dependency. - final PackageId depender; - - /// The package being depended on. - final PackageRange dep; - - Dependency(this.depender, this.dep); - - String toString() => '$depender -> $dep'; -} diff --git a/lib/src/solver/failure.dart b/lib/src/solver/failure.dart index 186fc7cba..29fb78284 100644 --- a/lib/src/solver/failure.dart +++ b/lib/src/solver/failure.dart @@ -2,201 +2,23 @@ // 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 'dart:convert'; - -import 'package:pub_semver/pub_semver.dart'; -import 'package:stack_trace/stack_trace.dart'; - import '../exceptions.dart'; -import '../log.dart' as log; -import '../package_name.dart'; -import 'dependency.dart'; +import 'incompatibility.dart'; /// Base class for all failures that can occur while trying to resolve versions. -abstract class SolveFailure implements ApplicationException { - /// The name of the package whose version could not be solved. - /// - /// Will be `null` if the failure is not specific to one package. - final String package; - - /// The known dependencies on [package] at the time of the failure. +class SolveFailure implements ApplicationException { + /// The root incompatibility. /// - /// Will be an empty collection if the failure is not specific to one package. - final Iterable dependencies; + /// This will always indicate that the root package is unselectable. That is, + /// it will have one term, which will be the root package. + final Incompatibility incompatibility; String get message => toString(); - /// A message describing the specific kind of solve failure. - String get _message { - throw new UnimplementedError("Must override _message or toString()."); - } - - SolveFailure(this.package, Iterable dependencies) - : dependencies = dependencies != null ? dependencies : []; - - String toString() { - if (dependencies.isEmpty) return _message; - - var buffer = new StringBuffer(); - buffer.write("$_message:"); - - var sorted = dependencies.toList(); - sorted.sort((a, b) => a.depender.name.compareTo(b.depender.name)); - - for (var dep in sorted) { - buffer.writeln(); - buffer.write("- ${log.bold(dep.depender.name)}"); - if (!dep.depender.isMagic && !dep.depender.isRoot) { - buffer.write(" ${dep.depender.version}"); - } - buffer.write(" ${_describeDependency(dep.dep)}"); - } - - return buffer.toString(); - } - - /// Describes a dependency's reference in the output message. - /// - /// Override this to highlight which aspect of [dep] led to the failure. - String _describeDependency(PackageRange dep) { - var description = "depends on version ${dep.constraint}"; - if (dep.features.isNotEmpty) description += " ${dep.featureDescription}"; - return description; + SolveFailure(this.incompatibility) { + assert(incompatibility.terms.single.package.isRoot); } -} - -/// Exception thrown when the current SDK's version does not match a package's -/// constraint on it. -class BadSdkVersionException extends SolveFailure { - final String _message; - - BadSdkVersionException(String package, String message) - : _message = message, - super(package, null); -} - -/// Exception thrown when the [VersionConstraint] used to match a package is -/// valid (i.e. non-empty), but there are no available versions of the package -/// that fit that constraint. -class NoVersionException extends SolveFailure { - final VersionConstraint constraint; - - /// The last selected version of the package that failed to meet the new - /// constraint. - /// - /// This will be `null` when the failure occurred because there are no - /// versions of the package *at all* that match the constraint. It will be - /// non-`null` when a version was selected, but then the solver tightened a - /// constraint such that that version was no longer allowed. - final Version version; - - NoVersionException(String package, this.version, this.constraint, - Iterable dependencies) - : super(package, dependencies); - - String get _message { - if (version == null) { - return "Package $package has no versions that match $constraint derived " - "from"; - } - - return "Package $package $version does not match $constraint derived from"; - } -} - -// TODO(rnystrom): Report the list of depending packages and their constraints. -/// Exception thrown when the most recent version of [package] must be selected, -/// but doesn't match the [VersionConstraint] imposed on the package. -class CouldNotUpgradeException extends SolveFailure { - final VersionConstraint constraint; - final Version best; - - CouldNotUpgradeException(String package, this.constraint, this.best) - : super(package, null); - - String get _message => - "The latest version of $package, $best, does not match $constraint."; -} - -/// Exception thrown when the [VersionConstraint] used to match a package is -/// the empty set: in other words, multiple packages depend on it and have -/// conflicting constraints that have no overlap. -class DisjointConstraintException extends SolveFailure { - DisjointConstraintException(String package, Iterable dependencies) - : super(package, dependencies); - - String get _message => "Incompatible version constraints on $package"; -} - -/// Exception thrown when two packages with the same name but different sources -/// are depended upon. -class SourceMismatchException extends SolveFailure { - String get _message => "Incompatible dependencies on $package"; - - SourceMismatchException(String package, Iterable dependencies) - : super(package, dependencies); - - String _describeDependency(PackageRange dep) => - "depends on it from source ${dep.source}"; -} - -/// Exception thrown when a dependency on an unknown source name is found. -class UnknownSourceException extends SolveFailure { - UnknownSourceException(String package, Iterable dependencies) - : super(package, dependencies); - - String toString() { - var dep = dependencies.single; - return 'Package ${dep.depender.name} depends on ${dep.dep.name} from ' - 'unknown source "${dep.dep.source}".'; - } -} - -/// Exception thrown when two packages with the same name and source but -/// different descriptions are depended upon. -class DescriptionMismatchException extends SolveFailure { - String get _message => "Incompatible dependencies on $package"; - - DescriptionMismatchException( - String package, Iterable dependencies) - : super(package, dependencies); - - String _describeDependency(PackageRange dep) { - // TODO(nweiz): Dump descriptions to YAML when that's supported. - return "depends on it with description ${JSON.encode(dep.description)}"; - } -} - -/// Exception thrown when a dependency could not be found in its source. -/// -/// Unlike [PackageNotFoundException], this includes information about the -/// dependent packages requesting the missing one. -class DependencyNotFoundException extends SolveFailure - implements WrappedException { - final PackageNotFoundException innerError; - Chain get innerChain => innerError.innerChain; - - String get _message => "${innerError.message}\nDepended on by"; - - DependencyNotFoundException( - String package, this.innerError, Iterable dependencies) - : super(package, dependencies); - - /// The failure isn't because of the version of description of the package, - /// it's the package itself that can't be found, so just show the name and no - /// descriptive details. - String _describeDependency(PackageRange dep) => ""; -} - -/// An exception thrown when a dependency requires a feature that doesn't exist. -class MissingFeatureException extends SolveFailure { - final Version version; - final String feature; - - String get _message => - "$package $version doesn't have a feature named $feature"; - MissingFeatureException(String package, this.version, this.feature, - Iterable dependencies) - : super(package, dependencies); + // TODO(nweiz): Produce a useful error message. + String toString() => "Tough luck, Chuck!"; } diff --git a/lib/src/solver/incompatibility.dart b/lib/src/solver/incompatibility.dart index 9c564ebc3..6e721bc49 100644 --- a/lib/src/solver/incompatibility.dart +++ b/lib/src/solver/incompatibility.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import '../package_name.dart'; +import 'incompatibility_cause.dart'; import 'term.dart'; /// A set of mutually-incompatible terms. @@ -12,17 +13,24 @@ class Incompatibility { /// The mutually-incompatibile terms. final List terms; + /// The reason [terms] are incompatible. + final IncompatibilityCause cause; + + /// Whether this incompatibility indicates that version solving as a whole has + /// failed. + bool get isFailure => terms.length == 1 && terms.first.package.isRoot; + /// Creates an incompatibility with [terms]. /// /// This normalizes [terms] so that each package has at most one term /// referring to it. - factory Incompatibility(List terms) { + factory Incompatibility(List terms, IncompatibilityCause cause) { if (terms.length == 1 || // Short-circuit in the common case of a two-term incompatibility with // two different packages (for example, a dependency). (terms.length == 2 && terms.first.package.name != terms.last.package.name)) { - return new Incompatibility._(terms); + return new Incompatibility._(terms, cause); } // Coalesce multiple terms about the same package if possible. @@ -44,39 +52,86 @@ class Incompatibility { } } - return new Incompatibility._(byName.values.expand((byRef) { - // If there are any positive terms for a given package, we can discard - // any negative terms. - var positiveTerms = - byRef.values.where((term) => term.isPositive).toList(); - if (positiveTerms.isNotEmpty) return positiveTerms; + return new Incompatibility._( + byName.values.expand((byRef) { + // If there are any positive terms for a given package, we can discard + // any negative terms. + var positiveTerms = + byRef.values.where((term) => term.isPositive).toList(); + if (positiveTerms.isNotEmpty) return positiveTerms; - return byRef.values; - }).toList()); + return byRef.values; + }).toList(), + cause); } - Incompatibility._(this.terms); + Incompatibility._(this.terms, this.cause); String toString() { + if (cause == IncompatibilityCause.dependency) { + assert(terms.length == 2); + + var depender = terms.first; + var dependee = terms.last; + assert(depender.isPositive); + assert(!dependee.isPositive); + + if (depender.constraint.isAny) { + return "all versions of ${_terseRef(depender)} " + "depend on ${dependee.package.toTerseString()}"; + } else { + return "${depender.package.toTerseString()} depends on " + "${dependee.package.toTerseString()}"; + } + } else if (cause == IncompatibilityCause.sdk) { + assert(terms.length == 1); + assert(terms.first.isPositive); + + // TODO(nweiz): Include more details about the expected and actual SDK + // versions. + if (terms.first.constraint.isAny) { + return "no versions of ${_terseRef(terms.first)} " + "are compatible with the current SDK"; + } else { + return "${terms.first.package.toTerseString()} is incompatible with " + "the current SDK"; + } + } else if (cause == IncompatibilityCause.noVersions) { + assert(terms.length == 1); + assert(terms.first.isPositive); + return "no versions of ${_terseRef(terms.first)} " + "match ${terms.first.constraint}"; + } else if (isFailure) { + return "version solving failed"; + } + if (terms.length == 1) { var term = terms.single; - return "${term.package.toTerseString()} is " - "${term.isPositive ? 'forbidden' : 'required'}"; + if (term.constraint.isAny) { + return "${_terseRef(term)} is " + "${term.isPositive ? 'forbidden' : 'required'}"; + } else { + return "${term.package.toTerseString()} is " + "${term.isPositive ? 'forbidden' : 'required'}"; + } } if (terms.length == 2) { var term1 = terms.first; var term2 = terms.last; - if (term1.isPositive != term2.isPositive) { - var positive = (term1.isPositive ? term1 : term2).package; - var negative = (term1.isPositive ? term2 : term1).package; - return "if ${positive.toTerseString()} then ${negative.toTerseString()}"; - } else if (term1.isPositive) { - return "${term1.package.toTerseString()} is incompatible with " - "${term2.package.toTerseString()}"; - } else { - return "either ${term1.package.toTerseString()} or " - "${term2.package.toTerseString()}"; + if (term1.isPositive == term2.isPositive) { + if (term1.isPositive) { + var package1 = term1.constraint.isAny + ? _terseRef(term1) + : term1.package.toTerseString(); + var package2 = term2.constraint.isAny + ? _terseRef(term2) + : term2.package.toTerseString(); + return "$package1 is incompatible with $package2"; + } else { + return "either ${term1.package.toTerseString()} or " + "${term2.package.toTerseString()}"; + } } } @@ -87,11 +142,24 @@ class Incompatibility { } if (positive.isNotEmpty && negative.isNotEmpty) { - return "if ${positive.join(' and ')} then ${negative.join(' and ')}"; + if (positive.length == 1) { + var positiveTerm = terms.firstWhere((term) => term.isPositive); + if (positiveTerm.constraint.isAny) { + return "all versions of ${_terseRef(positiveTerm)} require " + "${negative.join(' or ')}"; + } else { + return "${positive.first} requires ${negative.join(' or ')}"; + } + } else { + return "if ${positive.join(' and ')} then ${negative.join(' or ')}"; + } } else if (positive.isNotEmpty) { return "one of ${positive.join(' or ')} must be false"; } else { return "one of ${negative.join(' or ')} must be true"; } } + + /// Returns a terse representation of term's package ref. + String _terseRef(Term term) => term.package.toRef().toTerseString(); } diff --git a/lib/src/solver/incompatibility_cause.dart b/lib/src/solver/incompatibility_cause.dart new file mode 100644 index 000000000..60c9fd458 --- /dev/null +++ b/lib/src/solver/incompatibility_cause.dart @@ -0,0 +1,47 @@ +// Copyright (c) 2018, 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 'incompatibility.dart'; + +/// The reason an [Incompatibility]'s terms are incompatible. +abstract class IncompatibilityCause { + /// The incompatibility represents a package's dependency. + static const IncompatibilityCause dependency = const _DependencyCause(); + + /// The incompatibility represents a package's SDK constraint being + /// incompatible with the current SDK. + static const IncompatibilityCause sdk = const _SdkCause(); + + /// The incompatibility represents that the package has no versions that match + /// the given constraint. + static const IncompatibilityCause noVersions = const _NoVersionsCause(); +} + +/// The incompatibility was derived from two existing incompatibilities during +/// conflict resolution. +class ConflictCause implements IncompatibilityCause { + /// The incompatibility that was originally found to be in conflict, from + /// which the target incompatiblity was derived. + final Incompatibility conflict; + + /// The incompatibility that caused the most recent satisfier for [conflict], + /// from which the target incompatibility was derived. + final Incompatibility other; + + ConflictCause(this.conflict, this.other); +} + +class _DependencyCause implements IncompatibilityCause { + const _DependencyCause(); +} + +class _NoVersionsCause implements IncompatibilityCause { + const _NoVersionsCause(); +} + +// TODO(nweiz): Include more information about what SDK versions are allowed +// and/or whether Flutter is required but unavailable. +class _SdkCause implements IncompatibilityCause { + const _SdkCause(); +} diff --git a/lib/src/solver/package_lister.dart b/lib/src/solver/package_lister.dart index 5c6a78ba9..b37655d54 100644 --- a/lib/src/solver/package_lister.dart +++ b/lib/src/solver/package_lister.dart @@ -15,6 +15,7 @@ import '../sdk.dart' as sdk; import '../system_cache.dart'; import '../utils.dart'; import 'incompatibility.dart'; +import 'incompatibility_cause.dart'; import 'term.dart'; /// A cache of all the versions of a single package that provides information @@ -109,7 +110,8 @@ class PackageLister { _knownInvalidSdks = constraint.union(_knownInvalidSdks); return [ - new Incompatibility([new Term(_ref.withConstraint(constraint), true)]) + new Incompatibility([new Term(_ref.withConstraint(constraint), true)], + IncompatibilityCause.sdk) ]; } @@ -138,7 +140,7 @@ class PackageLister { return new Incompatibility([ new Term(_ref.withConstraint(constraint), true), new Term(pubspec.dependencies[package], false) - ]); + ], IncompatibilityCause.dependency); }).toList(); } diff --git a/lib/src/solver/result.dart b/lib/src/solver/result.dart index b18446229..109b106c6 100644 --- a/lib/src/solver/result.dart +++ b/lib/src/solver/result.dart @@ -10,37 +10,28 @@ import '../package.dart'; import '../package_name.dart'; import '../pubspec.dart'; import '../source_registry.dart'; -import 'failure.dart'; import 'report.dart'; import 'type.dart'; -/// The result of a version resolution. +/// The result of a successful version resolution. class SolveResult { - /// Whether the solver found a complete solution or failed. - bool get succeeded => error == null; - /// The list of concrete package versions that were selected for each package - /// reachable from the root, or `null` if the solver failed. + /// reachable from the root. final List packages; /// The dependency overrides that were used in the solution. final List overrides; /// A map from package names to the pubspecs for the versions of those - /// packages that were installed, or `null` if the solver failed. + /// packages that were installed. final Map pubspecs; /// The available versions of all selected packages from their source. /// - /// Will be empty if the solve failed. An entry here may not include the full - /// list of versions available if the given package was locked and did not - /// need to be unlocked during the solve. + /// An entry here may not include the full list of versions available if the + /// given package was locked and did not need to be unlocked during the solve. final Map> availableVersions; - /// The error that prevented the solver from finding a solution or `null` if - /// it was successful. - final SolveFailure error; - /// The number of solutions that were attempted before either finding a /// successful solution or exhausting all options. /// @@ -97,7 +88,7 @@ class SolveResult { .toSet()); } - SolveResult.success( + SolveResult( this._sources, this._root, this._previousLockFile, @@ -105,14 +96,7 @@ class SolveResult { this.overrides, this.pubspecs, this.availableVersions, - this.attemptedSolutions) - : error = null; - - SolveResult.failure(this._sources, this._root, this._previousLockFile, - this.overrides, this.error, this.attemptedSolutions) - : this.packages = null, - this.pubspecs = null, - this.availableVersions = {}; + this.attemptedSolutions); /// Displays a report of what changes were made to the lockfile. /// @@ -130,13 +114,6 @@ class SolveResult { .summarize(dryRun: dryRun); } - String toString() { - if (!succeeded) { - return 'Failed to solve after $attemptedSolutions attempts:\n' - '$error'; - } - - return 'Took $attemptedSolutions tries to resolve to\n' - '- ${packages.join("\n- ")}'; - } + String toString() => 'Took $attemptedSolutions tries to resolve to\n' + '- ${packages.join("\n- ")}'; } diff --git a/lib/src/solver/term.dart b/lib/src/solver/term.dart index 843b7ae03..2c65eeea3 100644 --- a/lib/src/solver/term.dart +++ b/lib/src/solver/term.dart @@ -27,7 +27,9 @@ class Term { /// A copy of this term with the opposite [isPositive] value. Term get inverse => new Term(package, !isPositive); - Term(this.package, this.isPositive); + Term(PackageName package, this.isPositive) + : package = + package is PackageRange ? package.withTerseConstraint() : package; /// The constraint of [package]. VersionConstraint get constraint { diff --git a/lib/src/solver/version_solver.dart b/lib/src/solver/version_solver.dart index 2bcd21fe6..42657fa5d 100644 --- a/lib/src/solver/version_solver.dart +++ b/lib/src/solver/version_solver.dart @@ -15,7 +15,9 @@ import '../pubspec.dart'; import '../system_cache.dart'; import '../utils.dart'; import 'assignment.dart'; +import 'failure.dart'; import 'incompatibility.dart'; +import 'incompatibility_cause.dart'; import 'package_lister.dart'; import 'partial_solution.dart'; import 'result.dart'; @@ -65,7 +67,8 @@ class VersionSolver { _log("selecting ${rootId.toTerseString()}"); for (var dependency in _root.immediateDependencies.values) { _addIncompatibility(new Incompatibility( - [new Term(rootId, true), new Term(dependency, false)])); + [new Term(rootId, true), new Term(dependency, false)], + IncompatibilityCause.dependency)); } var next = _root.name; @@ -180,7 +183,7 @@ class VersionSolver { _log("${log.red(log.bold("conflict"))}: $incompatibility"); var newIncompatibility = false; - while (true) { + while (!incompatibility.isFailure) { // The term in `incompatibility.terms` that was most recently satisfied by // [_solution]. Term mostRecentTerm; @@ -230,12 +233,6 @@ class VersionSolver { } } - // If we have a conflict at the root level, there's no hope of finding a - // solution and version solving has failed. - if (mostRecentSatisfier.decisionLevel == 0) { - throw "Tough luck, chuck!"; - } - // If [mostRecentSatisfier] is the only satisfier left at its decision // level, or if it has no cause (indicating that it's a decision rather // than a derivation), then [incompatibility] is the root cause. We then @@ -273,7 +270,8 @@ class VersionSolver { // [the algorithm documentation]: https://github.com/dart-lang/pub/tree/master/doc/solver.md#conflict-resolution if (difference != null) newTerms.add(difference.inverse); - incompatibility = new Incompatibility(newTerms); + incompatibility = new Incompatibility(newTerms, + new ConflictCause(incompatibility, mostRecentSatisfier.cause)); newIncompatibility = true; var partially = difference == null ? "" : " partially"; @@ -283,6 +281,8 @@ class VersionSolver { _log('$bang which is caused by "${mostRecentSatisfier.cause}"'); _log("$bang thus: $incompatibility"); } + + throw new SolveFailure(incompatibility); } /// Tries to select a version of a required package. @@ -307,7 +307,8 @@ class VersionSolver { if (version == null) { // If there are no versions that satisfy [package.constraint], add an // incompatibility that indicates that. - _addIncompatibility(new Incompatibility([new Term(package, true)])); + _addIncompatibility(new Incompatibility( + [new Term(package, true)], IncompatibilityCause.noVersions)); return package.name; } @@ -367,7 +368,7 @@ class VersionSolver { } } - return new SolveResult.success( + return new SolveResult( _systemCache.sources, _root, new LockFile.empty(), diff --git a/lib/src/utils.dart b/lib/src/utils.dart index ba78a80df..adb5ddfaf 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -68,6 +68,19 @@ const reservedWords = const [ /// An cryptographically secure instance of [math.Random]. final random = new math.Random.secure(); +/// The default line length for output when there isn't a terminal attached to +/// stdout. +const _defaultLineLength = 100; + +/// The maximum line length for output. +final int lineLength = () { + try { + return stdout.terminalColumns; + } on StdoutException { + return _defaultLineLength; + } +}(); + /// A pair of values. class Pair { E first; @@ -786,3 +799,48 @@ String createUuid([List bytes]) { return '${chars.substring(0, 8)}-${chars.substring(8, 12)}-' '${chars.substring(12, 16)}-${chars.substring(16, 20)}-${chars.substring(20, 32)}'; } + +/// Wraps [text] so that it fits within [lineLength]. +/// +/// This preserves existing newlines and doesn't consider terminal color escapes +/// part of a word's length. It only splits words on spaces, not on other sorts +/// of whitespace. +/// +/// If [prefix] is passed, it's added at the beginning of any wrapped lines. +String wordWrap(String text, {String prefix}) { + prefix ??= ""; + return text.split("\n").map((originalLine) { + var buffer = new StringBuffer(); + var lengthSoFar = 0; + var firstLine = true; + for (var word in originalLine.split(" ")) { + var wordLength = withoutColors(word).length; + if (wordLength > lineLength) { + if (lengthSoFar != 0) buffer.writeln(); + if (!firstLine) buffer.write(prefix); + buffer.writeln(word); + firstLine = false; + } else if (lengthSoFar == 0) { + if (!firstLine) buffer.write(prefix); + buffer.write(word); + lengthSoFar = wordLength + prefix.length; + } else if (lengthSoFar + 1 + wordLength > lineLength) { + buffer.writeln(); + buffer.write(prefix); + buffer.write(word); + lengthSoFar = wordLength + prefix.length; + firstLine = false; + } else { + buffer.write(" $word"); + lengthSoFar += 1 + wordLength; + } + } + return buffer.toString(); + }).join("\n"); +} + +/// A regular expression matching terminal color codes. +final _colorCode = new RegExp('\u001b\\[[0-9;]+m'); + +/// Returns [str] without any color codes. +String withoutColors(String str) => str.replaceAll(_colorCode, '');