From cd915845dbbd3893e2ec81de305f6b19cc4be2e2 Mon Sep 17 00:00:00 2001 From: Arnout Engelen Date: Mon, 9 May 2022 16:41:26 +0200 Subject: [PATCH] Add support for wildcards in Scala version switch Picking from the `crossScalaVersions` As discussed in https://github.com/sbt/sbt/discussions/6893 --- main/src/main/scala/sbt/Cross.scala | 96 +++++++++++++------ main/src/test/scala/sbt/CrossSpec.scala | 20 ++++ .../sbt-test/actions/cross-multiproject/test | 21 ++++ 3 files changed, 109 insertions(+), 28 deletions(-) create mode 100644 main/src/test/scala/sbt/CrossSpec.scala diff --git a/main/src/main/scala/sbt/Cross.scala b/main/src/main/scala/sbt/Cross.scala index 8bb001d04..9966c5eb9 100644 --- a/main/src/main/scala/sbt/Cross.scala +++ b/main/src/main/scala/sbt/Cross.scala @@ -8,7 +8,7 @@ package sbt import java.io.File - +import java.util.regex.Pattern import sbt.Def.{ ScopedKey, Setting } import sbt.Keys._ import sbt.SlashSyntax0._ @@ -284,8 +284,8 @@ object Cross { } def logSwitchInfo( - included: Seq[(ProjectRef, Seq[ScalaVersion])], - excluded: Seq[(ProjectRef, Seq[ScalaVersion])] + included: Seq[(ResolvedReference, ScalaVersion, Seq[ScalaVersion])], + excluded: Seq[(ResolvedReference, Seq[ScalaVersion])] ) = { instance.foreach { @@ -304,56 +304,96 @@ object Cross { def detailedLog(msg: => String) = if (switch.verbose) state.log.info(msg) else state.log.debug(msg) - def logProject: (ProjectRef, Seq[ScalaVersion]) => Unit = (proj, scalaVersions) => { - val current = if (proj == currentRef) "*" else " " - detailedLog(s" $current ${proj.project} ${scalaVersions.mkString("(", ", ", ")")}") + def logProject: (ResolvedReference, Seq[ScalaVersion]) => Unit = (ref, scalaVersions) => { + val current = if (ref == currentRef) "*" else " " + ref match { + case proj: ProjectRef => + detailedLog(s" $current ${proj.project} ${scalaVersions.mkString("(", ", ", ")")}") + case _ => // don't log BuildRefs + } } detailedLog("Switching Scala version on:") - included.foreach(logProject.tupled) + included.foreach { case (project, _, versions) => logProject(project, versions) } detailedLog("Excluding projects:") excluded.foreach(logProject.tupled) } - val projects: Seq[(ResolvedReference, Seq[ScalaVersion])] = { + val projects: Seq[(ResolvedReference, Option[ScalaVersion], Seq[ScalaVersion])] = { val projectScalaVersions = structure.allProjectRefs.map(proj => proj -> crossVersions(extracted, proj)) if (switch.version.force) { - logSwitchInfo(projectScalaVersions, Nil) - projectScalaVersions ++ structure.units.keys + projectScalaVersions.map { + case (ref, options) => (ref, Some(version), options) + } ++ structure.units.keys .map(BuildRef.apply) - .map(proj => proj -> crossVersions(extracted, proj)) + .map(proj => (proj, Some(version), crossVersions(extracted, proj))) + } else if (version.contains('*')) { + projectScalaVersions.map { + case (project, scalaVersions) => + globFilter(version, scalaVersions) match { + case Nil => (project, None, scalaVersions) + case Seq(version) => (project, Some(version), scalaVersions) + case multiple => + sys.error( + s"Multiple crossScalaVersions matched query '$version': ${multiple.mkString(", ")}" + ) + } + } } else { val binaryVersion = CrossVersion.binaryScalaVersion(version) - - val (included, excluded) = projectScalaVersions.partition { - case (_, scalaVersions) => - scalaVersions.exists(v => CrossVersion.binaryScalaVersion(v) == binaryVersion) + projectScalaVersions.map { + case (project, scalaVersions) => + if (scalaVersions.exists(v => CrossVersion.binaryScalaVersion(v) == binaryVersion)) + (project, Some(version), scalaVersions) + else + (project, None, scalaVersions) } - if (included.isEmpty) { - sys.error( - s"""Switch failed: no subprojects list "$version" (or compatible version) in crossScalaVersions setting. - |If you want to force it regardless, call ++ $version!""".stripMargin - ) - } - logSwitchInfo(included, excluded) - included } } - (setScalaVersionForProjects(version, instance, projects, state, extracted), projects.map(_._1)) + val included = projects.collect { + case (project, Some(version), scalaVersions) => (project, version, scalaVersions) + } + val excluded = projects.collect { + case (project, None, scalaVersions) => (project, scalaVersions) + } + + if (included.isEmpty) { + sys.error( + s"""Switch failed: no subprojects list "$version" (or compatible version) in crossScalaVersions setting. + |If you want to force it regardless, call ++ $version!""".stripMargin + ) + } + + logSwitchInfo(included, excluded) + + (setScalaVersionsForProjects(instance, included, state, extracted), included.map(_._1)) } - private def setScalaVersionForProjects( - version: String, + def globFilter(pattern: String, candidates: Seq[String]): Seq[String] = { + def createGlobRegex(remainingPattern: String): String = + remainingPattern.indexOf("*") match { + case -1 => Pattern.quote(remainingPattern) + case n => + val chunk = Pattern.quote(remainingPattern.substring(0, n)) + ".*" + if (remainingPattern.length > n) + chunk + createGlobRegex(remainingPattern.substring(n + 1)) + else chunk + } + val compiledPattern = Pattern.compile(createGlobRegex(pattern)) + candidates.filter(compiledPattern.matcher(_).matches()) + } + + private def setScalaVersionsForProjects( instance: Option[(File, ScalaInstance)], - projects: Seq[(ResolvedReference, Seq[String])], + projects: Seq[(ResolvedReference, String, Seq[String])], state: State, extracted: Extracted ): State = { import extracted._ val newSettings = projects.flatMap { - case (project, scalaVersions) => + case (project, version, scalaVersions) => val scope = Scope(Select(project), Zero, Zero, Zero) instance match { diff --git a/main/src/test/scala/sbt/CrossSpec.scala b/main/src/test/scala/sbt/CrossSpec.scala new file mode 100644 index 000000000..40673d548 --- /dev/null +++ b/main/src/test/scala/sbt/CrossSpec.scala @@ -0,0 +1,20 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt + +object CrossSpec extends verify.BasicTestSuite { + import Cross._ + + test("glob filter should work as expected") { + assert(globFilter("2.13.*", Seq("2.12.8", "2.13.16", "3.0.1")) == Seq("2.13.16")) + assert(globFilter("3.*", Seq("2.12.8", "2.13.16", "3.0.1")) == Seq("3.0.1")) + assert(globFilter("3.*", Seq("3.0.1", "30.1")) == Seq("3.0.1")) + assert(globFilter("2.*", Seq("2.12.8", "2.13.16", "3.0.1")) == Seq("2.12.8", "2.13.16")) + assert(globFilter("4.*", Seq("2.12.8", "2.13.16", "3.0.1")) == Nil) + } +} diff --git a/sbt-app/src/sbt-test/actions/cross-multiproject/test b/sbt-app/src/sbt-test/actions/cross-multiproject/test index 68b8383d5..baf4db9b8 100644 --- a/sbt-app/src/sbt-test/actions/cross-multiproject/test +++ b/sbt-app/src/sbt-test/actions/cross-multiproject/test @@ -41,3 +41,24 @@ $ exists lib/target/scala-2.13 -$ exists lib/target/scala-2.12 # -$ exists sbt-foo/target/scala-2.12 -$ exists sbt-foo/target/scala-2.13 + +# test wildcard switching (2.12) +> clean +> ++ 2.12.* -v compile +$ exists lib/target/scala-2.12 +-$ exists lib/target/scala-2.13 +$ exists sbt-foo/target/scala-2.12 +-$ exists sbt-foo/target/scala-2.13 + +# test wildcard switching (2.13) +> clean +> ++ 2.13.* -v compile +$ exists lib/target/scala-2.13 +-$ exists lib/target/scala-2.12 +# -$ exists sbt-foo/target/scala-2.12 +-$ exists sbt-foo/target/scala-2.13 + +# test wildcard switching (no matches) +-> ++ 3.* +# test wildcard switching (multiple matches) +-> ++ 2.*