Skip to content

Commit

Permalink
Merge pull request #72 from alexarchambault/lazy-constraint
Browse files Browse the repository at this point in the history
Keep parsed string in VersionConstraint, parse it lazily
  • Loading branch information
alexarchambault authored Jan 20, 2025
2 parents e9e65bd + a69a46a commit cefb42d
Show file tree
Hide file tree
Showing 9 changed files with 258 additions and 110 deletions.
15 changes: 14 additions & 1 deletion build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,29 @@ trait VersionsMima extends Mima {
val current = os.proc("git", "describe", "--tags", "--match", "v*")
.call(cwd = T.workspace)
.out.trim()
val cutOff = coursier.core.Version("0.3.3")
os.proc("git", "tag", "-l")
.call(cwd = T.workspace)
.out.lines()
.filter(_ != current)
.filter(_.startsWith("v"))
.map(_.stripPrefix("v"))
.map(coursier.core.Version(_))
.filter(_ > cutOff)
.sorted
.map(_.repr)
}
// required if mimaPreviousVersions is empty
def mimaPreviousArtifacts = T {
val versions = mimaPreviousVersions().distinct
mill.api.Result.Success(
Agg.from(
versions.map(version =>
ivy"${pomSettings().organization}:${artifactId()}:$version"
)
)
)
}
}

trait VersionsPublishModule extends PublishModule with VersionsMima {
Expand Down Expand Up @@ -106,7 +119,7 @@ trait Versions extends Cross.Module[String] with ScalaModule with VersionsPublis
}

def compileIvyDeps = Agg(
ivy"io.github.alexarchambault::data-class:0.2.6"
ivy"io.github.alexarchambault::data-class:0.2.7"
)

def mimaBinaryIssueFilters = super.mimaBinaryIssueFilters() ++ Seq(
Expand Down
88 changes: 56 additions & 32 deletions versions/shared/src/coursier/version/ConstraintReconciliation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,31 @@ package coursier.version
* To be used mainly during resolution.
*/
sealed abstract class ConstraintReconciliation extends Product with Serializable {
def reconcile(versions: Seq[String]): Option[String]
def reconcile(versions: Seq[VersionConstraint]): Option[VersionConstraint]
}

object ConstraintReconciliation {

private final val LatestIntegration = "latest.integration"
private final val LatestRelease = "latest.release"
private final val LatestStable = "latest.stable"
private final val LatestIntegration = VersionConstraint("latest.integration")
private final val LatestRelease = VersionConstraint("latest.release")
private final val LatestStable = VersionConstraint("latest.stable")

private def splitStandard(versions: Seq[String]): (Seq[String], Seq[String]) =
private def splitStandard(versions: Seq[VersionConstraint]): (Seq[VersionConstraint], Seq[VersionConstraint]) =
versions.distinct.partition {
case LatestIntegration => false
case LatestRelease => false
case LatestStable => false
case _ => true
}

private def retainLatestOpt(latests: Seq[String]): Option[String] =
private def retainLatestOpt(latests: Seq[VersionConstraint]): Option[VersionConstraint] =
if (latests.isEmpty) None
else if (latests.lengthCompare(1) == 0) latests.headOption
else {
val set = latests.toSet
val retained =
if (set(LatestIntegration))
LatestIntegration
else if (set(LatestRelease))
LatestRelease
if (set(LatestIntegration)) LatestIntegration
else if (set(LatestRelease)) LatestRelease
else {
// at least two distinct latest.* means we shouldn't even reach this else block anyway
assert(set(LatestStable))
Expand All @@ -48,7 +46,7 @@ object ConstraintReconciliation {
* Fails when passed version intervals that don't overlap.
*/
case object Default extends ConstraintReconciliation {
def reconcile(versions: Seq[String]): Option[String] =
def reconcile(versions: Seq[VersionConstraint]): Option[VersionConstraint] =
if (versions.isEmpty)
None
else if (versions.lengthCompare(1) == 0)
Expand All @@ -58,28 +56,23 @@ object ConstraintReconciliation {
val retainedStandard =
if (standard.isEmpty) None
else if (standard.lengthCompare(1) == 0) standard.headOption
else {
val parsedConstraints = standard.map(VersionParse.versionConstraint)
VersionConstraint.merge(parsedConstraints: _*)
.flatMap(_.repr)
}
else
VersionConstraint.merge(standard: _*)
.map(_.uniquePreferred.removeUnusedPreferred)
val retainedLatestOpt = retainLatestOpt(latests)

if (standard.isEmpty)
retainedLatestOpt
else if (latests.isEmpty)
retainedStandard
if (standard.isEmpty) retainedLatestOpt
else if (latests.isEmpty) retainedStandard
else {
val parsedIntervals = standard.map(VersionParse.versionConstraint)
val parsedIntervals = standard
.filter(_.preferred.isEmpty) // only keep intervals
.filter(_.interval != VersionInterval.zero) // not interval matching any version

if (parsedIntervals.isEmpty)
retainedLatestOpt
else
VersionConstraint.merge(parsedIntervals: _*)
.flatMap(_.repr)
.map(itv => (itv +: retainedLatestOpt.toSeq).mkString("&"))
.map(_.uniquePreferred.removeUnusedPreferred) // FIXME Add retainedLatestOpt too
}
}
}
Expand All @@ -90,7 +83,7 @@ object ConstraintReconciliation {
* When passed version intervals that don't overlap, the lowest intervals are discarded until the remaining intervals do overlap.
*/
case object Relaxed extends ConstraintReconciliation {
def reconcile(versions: Seq[String]): Option[String] =
def reconcile(versions: Seq[VersionConstraint]): Option[VersionConstraint] =
if (versions.isEmpty)
None
else if (versions.lengthCompare(1) == 0)
Expand All @@ -101,16 +94,15 @@ object ConstraintReconciliation {
if (standard.isEmpty) None
else if (standard.lengthCompare(1) == 0) standard.headOption
else {
val parsedConstraints = standard.map(VersionParse.versionConstraint)
VersionConstraint.merge(parsedConstraints: _*)
.getOrElse(VersionConstraint.relaxedMerge(parsedConstraints: _*))
.repr
val repr = VersionConstraint.merge(standard: _*)
.getOrElse(VersionConstraint.relaxedMerge(standard: _*))
.uniquePreferred
.removeUnusedPreferred
Some(repr)
}
val retainedLatestOpt = retainLatestOpt(latests)
if (latests.isEmpty)
retainedStandard
else
retainedLatestOpt
if (latests.isEmpty) retainedStandard
else retainedLatestOpt
}
}

Expand All @@ -129,4 +121,36 @@ object ConstraintReconciliation {
case _ => Default
}

/** Strict version reconciliation.
*
* This particular instance behaves the same as [[Default]] when used by
* [[coursier.core.Resolution]]. Actual strict conflict manager is handled by
* `coursier.params.rule.Strict`, which is set up by `coursier.Resolve` when a strict
* reconciliation is added to it.
*/
case object Strict extends ConstraintReconciliation {
def reconcile(versions: Seq[VersionConstraint]): Option[VersionConstraint] =
Default.reconcile(versions)
}

/** Semantic versioning version reconciliation.
*
* This particular instance behaves the same as [[Default]] when used by
* [[coursier.core.Resolution]]. Actual semantic versioning checks are handled by
* `coursier.params.rule.Strict` with field `semVer = true`, which is set up by
* `coursier.Resolve` when a SemVer reconciliation is added to it.
*/
case object SemVer extends ConstraintReconciliation {
def reconcile(versions: Seq[VersionConstraint]): Option[VersionConstraint] =
Default.reconcile(versions)
}

def apply(input: String): Option[ConstraintReconciliation] =
input match {
case "default" => Some(Default)
case "relaxed" => Some(Relaxed)
case "strict" => Some(Strict)
case "semver" => Some(SemVer)
case _ => None
}
}
6 changes: 5 additions & 1 deletion versions/shared/src/coursier/version/ModuleMatcher.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ import scala.util.matching.Regex
lazy val orgPattern = blobToPattern(organizationMatcher)
lazy val namePattern = blobToPattern(nameMatcher)
lazy val attributesPattern = attributeMatchers
.mapValues(blobToPattern(_))
.iterator
.map {
case (k, v) =>
(k, blobToPattern(v))
}
.toMap

def matches(organization: String, name: String): Boolean =
Expand Down
15 changes: 13 additions & 2 deletions versions/shared/src/coursier/version/Version.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ import scala.annotation.tailrec
* Same kind of ordering as aether-util/src/main/java/org/eclipse/aether/util/version/GenericVersion.java
*/
@data class Version(repr: String) extends Ordered[Version] {
lazy val items: Vector[Version.Item] = Version.items(repr)
def asString: String = repr
private var items0: Vector[Version.Item] = null
def items: Vector[Version.Item] = {
// no need to guard against concurrent computations, this is not too expensive to compute
if (items0 == null)
items0 = Version.items(repr)
items0
}
def compare(other: Version) = Version.listCompare(items, other.items)
def isEmpty = items.forall(_.isEmpty)

Expand All @@ -21,11 +28,15 @@ import scala.annotation.tailrec
repr
.split(Array('.', '-'))
.forall(_.lengthCompare(5) <= 0)

override lazy val hashCode = repr.hashCode()
}

object Version {

private[version] val zero = Version("0")
private val zero0 = Version("0")

def zero: Version = Version("0")

sealed abstract class Item extends Ordered[Item] {
def compare(other: Item): Int =
Expand Down
Loading

0 comments on commit cefb42d

Please sign in to comment.