From f83167dbf62dd4077fa1a70e73f00ba3d6303065 Mon Sep 17 00:00:00 2001 From: Alex Archambault Date: Fri, 4 Sep 2026 01:16:57 +0200 Subject: [PATCH 1/4] Add Scala Native support --- build.mill | 35 +++++++++++++++++++ .../version/internal/Compatibility.scala | 12 +++++++ .../src/coursier/version/VersionParse.scala | 2 +- 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 versions/native/src/coursier/version/internal/Compatibility.scala diff --git a/build.mill b/build.mill index af5a13b..463b327 100644 --- a/build.mill +++ b/build.mill @@ -17,6 +17,7 @@ object DepVersions { def scala213 = "2.13.18" def scala3 = "3.3.8" def scalaJs = "1.22.0" + def scalaNative = "0.5.6" def scala = Seq(scala213, "2.12.21", scala3) } @@ -226,6 +227,39 @@ trait VersionsJs extends Versions with ScalaJSModule { } } +trait VersionsNative extends Versions with ScalaNativeModule { + def scalaNativeVersion = DepVersions.scalaNative + + def mimaPreviousVersions = T { + val cutOff = coursier.core.Version("0.3.3") + super.mimaPreviousVersions().filter { v => + coursier.core.Version(v) > cutOff + } + } + + // 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" + ) + ) + ) + } + + object test extends ScalaNativeTests { + def ivyDeps = super.ivyDeps() ++ Agg( + Deps.utest + ) + def testFramework = "utest.runner.Framework" + def sources = T.sources { + super.sources() ++ 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/native/src/coursier/version/internal/Compatibility.scala b/versions/native/src/coursier/version/internal/Compatibility.scala new file mode 100644 index 0000000..5eaf341 --- /dev/null +++ b/versions/native/src/coursier/version/internal/Compatibility.scala @@ -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 = "<=" + +} diff --git a/versions/shared/src/coursier/version/VersionParse.scala b/versions/shared/src/coursier/version/VersionParse.scala index 85e8b46..c0e6324 100644 --- a/versions/shared/src/coursier/version/VersionParse.scala +++ b/versions/shared/src/coursier/version/VersionParse.scala @@ -59,7 +59,7 @@ object VersionParse { } yield itv } - private val multiVersionIntervalSplit = ("(?" + regexLookbehind + "[" + quote("])") + "]),(?=[" + quote("([") + "])").r + private lazy val multiVersionIntervalSplit = ("(?" + regexLookbehind + "[" + quote("])") + "]),(?=[" + quote("([") + "])").r def multiVersionInterval(s: String): Option[VersionInterval] = { From 5cd96304b668efd20119f3ac9004f5a63a6f895e Mon Sep 17 00:00:00 2001 From: Alex Archambault Date: Mon, 20 Jan 2025 21:01:59 +0100 Subject: [PATCH 2/4] ? --- versions/shared/src/coursier/version/VersionParse.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versions/shared/src/coursier/version/VersionParse.scala b/versions/shared/src/coursier/version/VersionParse.scala index c0e6324..06825be 100644 --- a/versions/shared/src/coursier/version/VersionParse.scala +++ b/versions/shared/src/coursier/version/VersionParse.scala @@ -59,7 +59,7 @@ object VersionParse { } yield itv } - private lazy val multiVersionIntervalSplit = ("(?" + regexLookbehind + "[" + quote("])") + "]),(?=[" + quote("([") + "])").r + private lazy val multiVersionIntervalSplit = ("(?" + regexLookbehind + "[" + "\\]\\)" + "]),(?=[" + "\\(\\[" + "])").r def multiVersionInterval(s: String): Option[VersionInterval] = { From 1588411215852621c1ce6926f6aeb1922eab6dee Mon Sep 17 00:00:00 2001 From: Alex Archambault Date: Fri, 4 Sep 2026 01:35:04 +0200 Subject: [PATCH 3/4] Fixes Co-authored-by: Claude Opus 5 --- build.mill | 30 +++-------- .../version/internal/Compatibility.scala | 1 + .../version/internal/Compatibility.scala | 1 + .../version/internal/Compatibility.scala | 2 - .../src/coursier/version/VersionParse.scala | 25 ++++++--- .../coursier/version/ModuleMatcherTests.scala | 53 +++++++++++++++++++ 6 files changed, 82 insertions(+), 30 deletions(-) create mode 100644 versions/shared/test/src/coursier/version/ModuleMatcherTests.scala diff --git a/build.mill b/build.mill index 463b327..b49b6a5 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,7 +18,7 @@ object DepVersions { def scala213 = "2.13.18" def scala3 = "3.3.8" def scalaJs = "1.22.0" - def scalaNative = "0.5.6" + def scalaNative = "0.5.12" def scala = Seq(scala213, "2.12.21", scala3) } @@ -230,32 +231,17 @@ trait VersionsJs extends Versions with ScalaJSModule { trait VersionsNative extends Versions with ScalaNativeModule { def scalaNativeVersion = DepVersions.scalaNative - def mimaPreviousVersions = T { - val cutOff = coursier.core.Version("0.3.3") - super.mimaPreviousVersions().filter { v => - coursier.core.Version(v) > cutOff - } - } - - // 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" - ) - ) - ) - } + // no Scala Native artifacts published before, nothing to check binary compatibility against + def mimaPreviousVersions = Task { Seq.empty[String] } object test extends ScalaNativeTests { - def ivyDeps = super.ivyDeps() ++ Agg( + def mvnDeps = super.mvnDeps() ++ Seq( + Deps.pprint, Deps.utest ) def testFramework = "utest.runner.Framework" - def sources = T.sources { - super.sources() ++ testSources() + def sources = Task { + super.sources() ++ Seq(versions.shared.testSources()) } } } 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 index 5eaf341..212738d 100644 --- a/versions/native/src/coursier/version/internal/Compatibility.scala +++ b/versions/native/src/coursier/version/internal/Compatibility.scala @@ -7,6 +7,4 @@ object Compatibility { def letterOrDigit = c.isLetterOrDigit } - def regexLookbehind: String = "<=" - } diff --git a/versions/shared/src/coursier/version/VersionParse.scala b/versions/shared/src/coursier/version/VersionParse.scala index 06825be..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 lazy val multiVersionIntervalSplit = ("(?" + regexLookbehind + "[" + "\\]\\)" + "]),(?=[" + "\\(\\[" + "])").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"))) + } + } +} From a5c6e81f35705bf9fe26b8fc013b04c50632554f Mon Sep 17 00:00:00 2001 From: Alex Archambault Date: Fri, 4 Sep 2026 01:47:47 +0200 Subject: [PATCH 4/4] Fix MiMA Co-authored-by: Claude Opus 5 --- build.mill | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/build.mill b/build.mill index b49b6a5..12a811c 100644 --- a/build.mill +++ b/build.mill @@ -231,8 +231,22 @@ trait VersionsJs extends Versions with ScalaJSModule { trait VersionsNative extends Versions with ScalaNativeModule { def scalaNativeVersion = DepVersions.scalaNative - // no Scala Native artifacts published before, nothing to check binary compatibility against - def mimaPreviousVersions = Task { Seq.empty[String] } + 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(