Skip to content

Commit

Permalink
Initial import from coursier
Browse files Browse the repository at this point in the history
  • Loading branch information
alexarchambault committed Jun 3, 2020
0 parents commit 555af0a
Show file tree
Hide file tree
Showing 15 changed files with 984 additions and 0 deletions.
64 changes: 64 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: CI
on:
push:
branches:
- master
tags:
- "v*"
pull_request:

jobs:
test:
runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v1

- name: coursier cache
uses: actions/cache@v1
if: runner.OS == 'Linux'
with:
path: ~/.cache/coursier
key: ${{ runner.OS }}-coursier-cache-${{ hashFiles('**/*.sbt') }} # -${{ hashFiles('project/**.scala') }} (fails for now)
restore-keys: |
${{ runner.OS }}-coursier-cache-${{ hashFiles('**/*.sbt') }}-
${{ runner.OS }}-coursier-cache-
- uses: olafurpg/setup-scala@v7
with:
java-version: adopt@1.8.0-232

- name: Compile
run: csbt test compatibilityCheck

publish:
needs: test
if: github.event_name == 'push'
runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v1

- name: coursier cache (Linux)
uses: actions/cache@v1
if: runner.OS == 'Linux'
with:
path: ~/.cache/coursier
key: ${{ runner.OS }}-coursier-cache-${{ hashFiles('**/*.sbt') }} # -${{ hashFiles('project/**.scala') }} (fails for now)
restore-keys: |
${{ runner.OS }}-coursier-cache-${{ hashFiles('**/*.sbt') }}-
${{ runner.OS }}-coursier-cache-
- uses: olafurpg/setup-scala@v7

- uses: olafurpg/setup-gpg@v2

- name: Release
run: csbt ci-release
env:
PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }}
PGP_SECRET: ${{ secrets.PGP_SECRET }}
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target/
47 changes: 47 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@

inThisBuild(List(
organization := "io.get-coursier",
homepage := Some(url("https://github.com/coursier/versions")),
licenses := List("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")),
developers := List(
Developer(
"alexarchambault",
"Alexandre Archambault",
"",
url("https://github.com/alexarchambault")
)
)
))

lazy val shared = Def.settings(
scalaVersion := "2.13.2",
crossScalaVersions := Seq("2.13.2", "2.12.11"),
libraryDependencies ++= {
if (isAtLeastScala213.value) Nil
else Seq(compilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full))
},
scalacOptions ++= {
if (isAtLeastScala213.value) Seq("-Ymacro-annotations")
else Nil
},
scalacOptions += "-deprecation"
)

lazy val isAtLeastScala213 = Def.setting {
import Ordering.Implicits._
CrossVersion.partialVersion(scalaVersion.value).exists(_ >= (2, 13))
}


lazy val versions = crossProject(JVMPlatform, JSPlatform)
.settings(
shared,
libraryDependencies += "io.github.alexarchambault" %% "data-class" % "0.2.3" % Provided
)

lazy val versionsJVM = versions.jvm
lazy val versionsJS = versions.js

crossScalaVersions := Nil
skip.in(publish) := true
disablePlugins(MimaPlugin)
1 change: 1 addition & 0 deletions project/build.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=1.3.10
8 changes: 8 additions & 0 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.5.3")
addSbtPlugin(("io.github.alexarchambault.sbt" % "sbt-compatibility" % "0.0.4").exclude("com.typesafe", "sbt-mima-plugin"))
addSbtPlugin("com.github.alexarchambault.tmp" % "sbt-mima-plugin" % "0.7.1-SNAPSHOT")
addSbtPlugin("org.xerial.sbt" % "sbt-pack" % "0.12")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.0.1")
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.0.0")

resolvers += Resolver.sonatypeRepo("snapshots")
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package coursier.version.internal

object Compatibility {

private def between(c: Char, lower: Char, upper: Char) = lower <= c && c <= upper

implicit class RichChar(private val c: Char) extends AnyVal {
def letter: Boolean = between(c, 'a', 'z') || between(c, 'A', 'Z')
def letterOrDigit: Boolean = between(c, '0', '9') || letter
}

def regexLookbehind: String = ":"

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package coursier.version.internal

object Compatibility {

implicit class RichChar(private val c: Char) extends AnyVal {
def letter = c.isLetter
def letterOrDigit = c.isLetterOrDigit
}

def regexLookbehind: String = "<="

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package coursier.version

/**
* Reconciles a set of version constraints (version intervals, specific versions, …).
*
* To be used mainly during resolution.
*/
sealed abstract class ConstraintReconciliation extends Product with Serializable {
def reconcile(versions: Seq[String]): Option[String]
}

object ConstraintReconciliation {

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

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

private def retainLatestOpt(latests: Seq[String]): Option[String] =
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
else {
// at least two distinct latest.* means we shouldn't even reach this else block anyway
assert(set(LatestStable))
LatestStable
}
Some(retained)
}


/**
* Keeps the intersection of intervals, retains the latest version, etc. as described in the coursier documentation
*
* Fails when passed version intervals that don't overlap.
*/
case object Default extends ConstraintReconciliation {
def reconcile(versions: Seq[String]): Option[String] =
if (versions.isEmpty)
None
else if (versions.lengthCompare(1) == 0)
Some(versions.head)
else {
val (standard, latests) = splitStandard(versions)
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)
}
val retainedLatestOpt = retainLatestOpt(latests)

if (standard.isEmpty)
retainedLatestOpt
else if (latests.isEmpty)
retainedStandard
else {
val parsedIntervals = standard.map(VersionParse.versionConstraint)
.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("&"))
}
}
}

/**
* Always succeeds
*
* 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] =
if (versions.isEmpty)
None
else if (versions.lengthCompare(1) == 0)
Some(versions.head)
else {
val (standard, latests) = splitStandard(versions)
val retainedStandard =
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 retainedLatestOpt = retainLatestOpt(latests)
if (latests.isEmpty)
retainedStandard
else
retainedLatestOpt
}
}

/**
* The [[ConstraintReconciliation]] to be used for this [[VersionCompatibility]]
*
* The `Always` version compatibility corresponds to `Relaxed` constraint reconciliation (never fail to reconcile
* versions during resolution).
*
* The other version compatibilities use `Default` as constraint reconciliation (may fail to reconcile versions during
* resolution).
*/
def apply(compatibility: VersionCompatibility): ConstraintReconciliation =
compatibility match {
case VersionCompatibility.Always => Relaxed
case _ => Default
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package coursier.version

import java.util.regex.Pattern

import dataclass.data

import scala.annotation.tailrec
import scala.util.matching.Regex

// Adapted from https://github.com/coursier/coursier/blob/876a6604d0cd0c3783ed729f5399549f52a3a385/modules/coursier/shared/src/main/scala/coursier/util/ModuleMatcher.scala

@data class ModuleMatcher(
organizationMatcher: String,
nameMatcher: String,
attributeMatchers: Map[String, String] = Map.empty
) {

import ModuleMatcher.blobToPattern

lazy val orgPattern = blobToPattern(organizationMatcher)
lazy val namePattern = blobToPattern(nameMatcher)
lazy val attributesPattern = attributeMatchers
.mapValues(blobToPattern(_))
.toMap

def matches(organization: String, name: String): Boolean =
matches(organization, name, Map.empty)

def matches(organization: String, name: String, attributes: Map[String, String]): Boolean =
orgPattern.pattern.matcher(organization).matches() &&
namePattern.pattern.matcher(name).matches() &&
attributes.keySet == attributesPattern.keySet &&
attributesPattern.forall {
case (k, p) =>
attributes.get(k).exists(p.pattern.matcher(_).matches())
}

}

object ModuleMatcher {

def all: ModuleMatcher =
ModuleMatcher("*", "*")

@tailrec
private def blobToPattern(s: String, b: StringBuilder = new StringBuilder): Regex =
if (s.isEmpty)
b.result().r
else {
val idx = s.indexOf('*')
if (idx < 0) {
b ++= Pattern.quote(s)
b.result().r
} else {
if (idx > 0)
b ++= Pattern.quote(s.substring(0, idx))
b ++= ".*"
blobToPattern(s.substring(idx + 1), b)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package coursier.version

import dataclass._

// Adapted from https://github.com/coursier/coursier/blob/f0b10fb1744e5bdf94bf17857dfb3cb19fda2e5b/modules/coursier/shared/src/main/scala/coursier/util/ModuleMatchers.scala

@data class ModuleMatchers(
exclude: Set[ModuleMatcher],
include: Set[ModuleMatcher] = Set(),
@since
includeByDefault: Boolean = true
) {

// If modules are included by default:
// Those matched by anything in exclude are excluded, but for those also matched by something in include.
// If modules are excluded by default:
// Those matched by anything in include are included, but for those also matched by something in exclude.

def matches(organization: String, name: String): Boolean =
matches(organization, name, Map.empty)

def matches(organization: String, name: String, attributes: Map[String, String]): Boolean =
if (includeByDefault)
!exclude.exists(_.matches(organization, name, attributes)) ||
include.exists(_.matches(organization, name, attributes))
else
include.exists(_.matches(organization, name, attributes)) &&
!exclude.exists(_.matches(organization, name, attributes))

}

object ModuleMatchers {
def all: ModuleMatchers =
ModuleMatchers(Set.empty, Set.empty)
def only(organization: String, name: String): ModuleMatchers =
only(organization, name, Map.empty)
def only(organization: String, name: String, attributes: Map[String, String]): ModuleMatchers =
ModuleMatchers(Set.empty, Set(ModuleMatcher(organization, name, attributes)), includeByDefault = false)
}
Loading

0 comments on commit 555af0a

Please sign in to comment.