diff --git a/build.mill b/build.mill index af5a13b..12a811c 100644 --- a/build.mill +++ b/build.mill @@ -7,6 +7,7 @@ import mill.* import mill.api.BuildCtx import mill.scalajslib.* import mill.scalalib.* +import mill.scalanativelib.* import mill.scalalib.publish.* import mill.util.VcsVersion @@ -17,6 +18,7 @@ object DepVersions { def scala213 = "2.13.18" def scala3 = "3.3.8" def scalaJs = "1.22.0" + def scalaNative = "0.5.12" def scala = Seq(scala213, "2.12.21", scala3) } @@ -226,6 +228,38 @@ trait VersionsJs extends Versions with ScalaJSModule { } } +trait VersionsNative extends Versions with ScalaNativeModule { + def scalaNativeVersion = DepVersions.scalaNative + + def mimaPreviousVersions = Task { + import _root_.coursier.version.Version + val cutOff = Version("0.6.0") + super.mimaPreviousVersions() + .map(Version(_)) + .filter(_ >= cutOff) + .map(_.repr) + } + + // no Scala Native artifacts published before 0.6.0, so unlike on the JVM and on Scala.js, + // there are no Scala 2.13 ones for the Scala 3 module to fall back on + def mimaPreviousArtifacts = Task { + mimaPreviousVersions().distinct.map(version => + mvn"${pomSettings().organization}:${artifactId()}:$version" + ) + } + + object test extends ScalaNativeTests { + def mvnDeps = super.mvnDeps() ++ Seq( + Deps.pprint, + Deps.utest + ) + def testFramework = "utest.runner.Framework" + def sources = Task { + super.sources() ++ Seq(versions.shared.testSources()) + } + } +} + object versions extends Module { object shared extends Module { def sources = Task.Source("src") @@ -235,6 +269,7 @@ object versions extends Module { } object jvm extends Cross[VersionsJvm](DepVersions.scala) object js extends Cross[VersionsJs](DepVersions.scala) + object native extends Cross[VersionsNative](DepVersions.scala) } diff --git a/versions/js/src/coursier/version/internal/Compatibility.scala b/versions/js/src/coursier/version/internal/Compatibility.scala index 0db3401..08303b7 100644 --- a/versions/js/src/coursier/version/internal/Compatibility.scala +++ b/versions/js/src/coursier/version/internal/Compatibility.scala @@ -9,6 +9,7 @@ object Compatibility { def letterOrDigit: Boolean = between(c, '0', '9') || letter } + // unused, kept for binary compatibility def regexLookbehind: String = ":" } diff --git a/versions/jvm/src/coursier/version/internal/Compatibility.scala b/versions/jvm/src/coursier/version/internal/Compatibility.scala index 5eaf341..c6339e1 100644 --- a/versions/jvm/src/coursier/version/internal/Compatibility.scala +++ b/versions/jvm/src/coursier/version/internal/Compatibility.scala @@ -7,6 +7,7 @@ object Compatibility { def letterOrDigit = c.isLetterOrDigit } + // unused, kept for binary compatibility def regexLookbehind: String = "<=" } diff --git a/versions/native/src/coursier/version/internal/Compatibility.scala b/versions/native/src/coursier/version/internal/Compatibility.scala new file mode 100644 index 0000000..212738d --- /dev/null +++ b/versions/native/src/coursier/version/internal/Compatibility.scala @@ -0,0 +1,10 @@ +package coursier.version.internal + +object Compatibility { + + implicit class RichChar(private val c: Char) extends AnyVal { + def letter = c.isLetter + def letterOrDigit = c.isLetterOrDigit + } + +} diff --git a/versions/shared/src/coursier/version/VersionParse.scala b/versions/shared/src/coursier/version/VersionParse.scala index 85e8b46..90a71cb 100644 --- a/versions/shared/src/coursier/version/VersionParse.scala +++ b/versions/shared/src/coursier/version/VersionParse.scala @@ -1,7 +1,5 @@ package coursier.version -import java.util.regex.Pattern.quote - import coursier.version.internal.Compatibility._ object VersionParse { @@ -59,17 +57,32 @@ object VersionParse { } yield itv } - private val multiVersionIntervalSplit = ("(?" + regexLookbehind + "[" + quote("])") + "]),(?=[" + quote("([") + "])").r + private def isIntervalStart(c: Char): Boolean = c == '[' || c == '(' + private def isIntervalEnd(c: Char): Boolean = c == ']' || c == ')' + + /** Index of the first char of the last interval of `s`, that is the index right after the last + * ',' preceded by ']' or ')' and followed by '[' or '(' - `0` if there's no such ','. + */ + private def lastIntervalStart(s: String): Int = { + var idx = s.length - 2 + var res = 0 + while (idx >= 1 && res == 0) { + if (s.charAt(idx) == ',' && isIntervalEnd(s.charAt(idx - 1)) && isIntervalStart(s.charAt(idx + 1))) + res = idx + 1 + idx -= 1 + } + res + } def multiVersionInterval(s: String): Option[VersionInterval] = { // TODO Use a full-fledged (fastparsed-based) parser for this and versionInterval above - val openCount = s.count(c => c == '[' || c == '(') - val closeCount = s.count(c => c == ']' || c == ')') + val openCount = s.count(isIntervalStart) + val closeCount = s.count(isIntervalEnd) if (openCount == closeCount && openCount >= 1) - versionInterval(multiVersionIntervalSplit.split(s).last) + versionInterval(s.substring(lastIntervalStart(s))) else None } diff --git a/versions/shared/test/src/coursier/version/ModuleMatcherTests.scala b/versions/shared/test/src/coursier/version/ModuleMatcherTests.scala new file mode 100644 index 0000000..7b668b3 --- /dev/null +++ b/versions/shared/test/src/coursier/version/ModuleMatcherTests.scala @@ -0,0 +1,53 @@ +package coursier.version + +import utest._ + +object ModuleMatcherTests extends TestSuite { + + def check(matcher: ModuleMatcher, organization: String, name: String): Unit = + assert(matcher.matches(organization, name)) + def checkNot(matcher: ModuleMatcher, organization: String, name: String): Unit = + assert(!matcher.matches(organization, name)) + + val tests = Tests { + test("all") { + check(ModuleMatcher.all, "org.scala-lang", "scala-library") + check(ModuleMatcher.all, "io.get-coursier", "versions_2.13") + } + + test("exact") { + val matcher = ModuleMatcher("io.get-coursier", "versions_2.13") + check(matcher, "io.get-coursier", "versions_2.13") + checkNot(matcher, "io.get-coursier", "versions_2.12") + checkNot(matcher, "io.get-coursier.thing", "versions_2.13") + } + + test("specialChars") { + // '.' and '-' are quoted, not interpreted as regex constructs + val matcher = ModuleMatcher("io.get-coursier", "versions_2.13") + checkNot(matcher, "ioaget-coursier", "versions_2.13") + checkNot(matcher, "io.get-coursier", "versions_2a13") + } + + test("blob") { + val matcher = ModuleMatcher("io.get-coursier*", "versions_*") + check(matcher, "io.get-coursier", "versions_2.13") + check(matcher, "io.get-coursier.scala-cli", "versions_native0.5_2.13") + checkNot(matcher, "org.get-coursier", "versions_2.13") + checkNot(matcher, "io.get-coursier", "coursier_2.13") + } + + test("leadingBlob") { + val matcher = ModuleMatcher("*", "*_2.13") + check(matcher, "io.get-coursier", "versions_2.13") + checkNot(matcher, "io.get-coursier", "versions_2.12") + } + + test("attributes") { + val matcher = ModuleMatcher("io.get-coursier", "versions_2.13", Map("scope" -> "*")) + assert(matcher.matches("io.get-coursier", "versions_2.13", Map("scope" -> "test"))) + assert(!matcher.matches("io.get-coursier", "versions_2.13", Map.empty)) + assert(!matcher.matches("io.get-coursier", "versions_2.13", Map("other" -> "test"))) + } + } +}