Merge pull request #7096 from adpi2/sbt-plugins-maven-path

[1.9.x] Smooth transition to valid Maven pattern of sbt plugins
This commit is contained in:
eugene yokota 2023-02-28 11:00:18 -05:00 committed by GitHub
commit 894789cd36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 296 additions and 3 deletions

View File

@ -2827,9 +2827,52 @@ object Classpaths {
val jvmPublishSettings: Seq[Setting[_]] = Seq(
artifacts := artifactDefs(defaultArtifactTasks).value,
packagedArtifacts := packaged(defaultArtifactTasks).value
packagedArtifacts := packaged(defaultArtifactTasks).value ++
Def
.ifS(sbtPlugin.toTask)(mavenArtifactsOfSbtPlugin)(Def.task(Map.empty[Artifact, File]))
.value
) ++ RemoteCache.projectSettings
/**
* Produces the Maven-compatible artifacts of an sbt plugin.
* It adds the sbt-cross version suffix into the artifact names, and it generates a
* valid POM file, that is a POM file that Maven can resolve.
*/
private def mavenArtifactsOfSbtPlugin: Def.Initialize[Task[Map[Artifact, File]]] =
Def.ifS(publishMavenStyle.toTask)(Def.task {
val crossVersion = sbtCrossVersion.value
val legacyArtifact = (makePom / artifact).value
val pom = makeMavenPomOfSbtPlugin.value
val legacyPackages = packaged(defaultPackages).value
def addSuffix(a: Artifact): Artifact = a.withName(crossVersion(a.name))
val packages = legacyPackages.map { case (artifact, file) => addSuffix(artifact) -> file }
packages + (addSuffix(legacyArtifact) -> pom)
})(Def.task(Map.empty))
private def sbtCrossVersion: Def.Initialize[String => String] = Def.setting {
val sbtV = sbtBinaryVersion.value
val scalaV = scalaBinaryVersion.value
name => name + s"_${scalaV}_$sbtV"
}
/**
* Generates a POM file that Maven can resolve.
* It appends the sbt cross version into all artifactIds of sbt plugins
* (the main one and the dependencies).
*/
private def makeMavenPomOfSbtPlugin: Def.Initialize[Task[File]] = Def.task {
val config = makePomConfiguration.value
val nameWithCross = sbtCrossVersion.value(artifact.value.name)
val version = Keys.version.value
val pomFile = config.file.get.getParentFile / s"$nameWithCross-$version.pom"
val publisher = Keys.publisher.value
val ivySbt = Keys.ivySbt.value
val module = new ivySbt.Module(moduleSettings.value, appendSbtCrossVersion = true)
publisher.makePomFile(module, config.withFile(pomFile), streams.value.log)
pomFile
}
val ivyPublishSettings: Seq[Setting[_]] = publishGlobalDefaults ++ Seq(
artifacts :== Nil,
packagedArtifacts :== Map.empty,

View File

@ -14,7 +14,7 @@ object Dependencies {
// sbt modules
private val ioVersion = nightlyVersion.getOrElse("1.8.0")
private val lmVersion =
sys.props.get("sbt.build.lm.version").orElse(nightlyVersion).getOrElse("1.8.0")
sys.props.get("sbt.build.lm.version").orElse(nightlyVersion).getOrElse("1.9.0-M1")
val zincVersion = nightlyVersion.getOrElse("1.8.0")
private val sbtIO = "org.scala-sbt" %% "io" % ioVersion
@ -77,7 +77,7 @@ object Dependencies {
def addSbtZincCompile = addSbtModule(sbtZincPath, "zincCompile", zincCompile)
def addSbtZincCompileCore = addSbtModule(sbtZincPath, "zincCompileCore", zincCompileCore)
val lmCoursierShaded = "io.get-coursier" %% "lm-coursier-shaded" % "2.0.15"
val lmCoursierShaded = "io.get-coursier" %% "lm-coursier-shaded" % "2.0.16"
def sjsonNew(n: String) =
Def.setting("com.eed3si9n" %% n % "0.9.1") // contrabandSjsonNewVersion.value

View File

@ -0,0 +1,105 @@
// sbt-plugin-example-diamond is a diamond graph of dependencies of sbt plugins.
// sbt-plugin-example-diamond
// / \
// sbt-plugin-example-left sbt-plugin-example-right
// \ /
// sbt-plugin-example-bottom
// Depending on the version of sbt-plugin-example-diamond, we test different patterns
// of dependencies:
// * Some dependencies were published using the deprecated Maven paths, some with the new
// * Wheter the dependency on sbt-plugin-example-bottom needs conflict resolution or not
inThisBuild(
Seq(
csrCacheDirectory := baseDirectory.value / "coursier-cache"
)
)
// only deprecated Maven paths
lazy val v1 = project
.in(file("v1"))
.settings(
localCache,
addSbtPlugin("ch.epfl.scala" % "sbt-plugin-example-diamond" % "0.1.0"),
checkUpdate := checkUpdateDef(
"sbt-plugin-example-diamond-0.1.0.jar",
"sbt-plugin-example-left-0.1.0.jar",
"sbt-plugin-example-right-0.1.0.jar",
"sbt-plugin-example-bottom-0.1.0.jar",
).value
)
// diamond and left use the new Maven paths
lazy val v2 = project
.in(file("v2"))
.settings(
localCache,
addSbtPlugin("ch.epfl.scala" % "sbt-plugin-example-diamond" % "0.2.0"),
checkUpdate := checkUpdateDef(
"sbt-plugin-example-diamond_2.12_1.0-0.2.0.jar",
"sbt-plugin-example-left_2.12_1.0-0.2.0.jar",
"sbt-plugin-example-right-0.1.0.jar",
"sbt-plugin-example-bottom-0.1.0.jar",
).value
)
// conflict resolution on bottom between new and deprecated Maven paths
lazy val v3 = project
.in(file("v3"))
.settings(
localCache,
addSbtPlugin("ch.epfl.scala" % "sbt-plugin-example-diamond" % "0.3.0"),
checkUpdate := checkUpdateDef(
"sbt-plugin-example-diamond_2.12_1.0-0.3.0.jar",
"sbt-plugin-example-left_2.12_1.0-0.3.0.jar",
"sbt-plugin-example-right-0.1.0.jar",
"sbt-plugin-example-bottom_2.12_1.0-0.2.0.jar",
).value
)
// right still uses the deprecated Maven path and it depends on bottom
// which uses the new Maven path
lazy val v4 = project
.in(file("v4"))
.settings(
localCache,
addSbtPlugin("ch.epfl.scala" % "sbt-plugin-example-diamond" % "0.4.0"),
checkUpdate := checkUpdateDef(
"sbt-plugin-example-diamond_2.12_1.0-0.4.0.jar",
"sbt-plugin-example-left_2.12_1.0-0.3.0.jar",
"sbt-plugin-example-right-0.2.0.jar",
"sbt-plugin-example-bottom_2.12_1.0-0.2.0.jar",
).value
)
// only new Maven paths with conflict resolution on bottom
lazy val v5 = project
.in(file("v5"))
.settings(
localCache,
addSbtPlugin("ch.epfl.scala" % "sbt-plugin-example-diamond" % "0.5.0"),
checkUpdate := checkUpdateDef(
"sbt-plugin-example-diamond_2.12_1.0-0.5.0.jar",
"sbt-plugin-example-left_2.12_1.0-0.3.0.jar",
"sbt-plugin-example-right_2.12_1.0-0.3.0.jar",
"sbt-plugin-example-bottom_2.12_1.0-0.3.0.jar",
).value
)
def localCache =
ivyPaths := IvyPaths(baseDirectory.value, Some((ThisBuild / baseDirectory).value / "ivy-cache"))
lazy val checkUpdate = taskKey[Unit]("check the resolved artifacts")
def checkUpdateDef(expected: String*): Def.Initialize[Task[Unit]] = Def.task {
val report = update.value
val obtainedFiles = report.configurations
.find(_.configuration.name == Compile.name)
.toSeq
.flatMap(_.modules)
.flatMap(_.artifacts)
.map(_._2)
val obtainedSet = obtainedFiles.map(_.getName).toSet
val expectedSet = expected.toSet + "scala-library.jar"
assert(obtainedSet == expectedSet, obtainedFiles)
}

View File

@ -0,0 +1,12 @@
> v1 / checkUpdate
> v2 / checkUpdate
> v3 / checkUpdate
> v4 / checkUpdate
> v5 / checkUpdate
> set ThisBuild / useCoursier:=false
> v1 / checkUpdate
> v2 / checkUpdate
> v3 / checkUpdate
> v4 / checkUpdate
> v5 / checkUpdate

View File

@ -0,0 +1,122 @@
import scala.util.matching.Regex
lazy val repo = file("test-repo")
lazy val resolver = Resolver.file("test-repo", repo)
lazy val example = project.in(file("example"))
.enablePlugins(SbtPlugin)
.settings(
organization := "org.example",
addSbtPlugin("ch.epfl.scala" % "sbt-plugin-example-diamond" % "0.5.0"),
publishTo := Some(resolver),
checkPackagedArtifacts := checkPackagedArtifactsDef.value,
checkPublish := checkPublishDef.value
)
lazy val testMaven = project.in(file("test-maven"))
.settings(
addSbtPlugin("org.example" % "example" % "0.1.0-SNAPSHOT"),
externalResolvers -= Resolver.defaultLocal,
resolvers += {
val base = (ThisBuild / baseDirectory).value
MavenRepository("test-repo", s"file://$base/test-repo")
},
checkUpdate := checkUpdateDef(
"example_2.12_1.0-0.1.0-SNAPSHOT.jar",
"sbt-plugin-example-diamond_2.12_1.0-0.5.0.jar",
"sbt-plugin-example-left_2.12_1.0-0.3.0.jar",
"sbt-plugin-example-right_2.12_1.0-0.3.0.jar",
"sbt-plugin-example-bottom_2.12_1.0-0.3.0.jar",
).value
)
lazy val testLocal = project.in(file("test-local"))
.settings(
addSbtPlugin("org.example" % "example" % "0.1.0-SNAPSHOT"),
checkUpdate := checkUpdateDef(
"example.jar", // resolved from local repository
"sbt-plugin-example-diamond_2.12_1.0-0.5.0.jar",
"sbt-plugin-example-left_2.12_1.0-0.3.0.jar",
"sbt-plugin-example-right_2.12_1.0-0.3.0.jar",
"sbt-plugin-example-bottom_2.12_1.0-0.3.0.jar",
).value
)
lazy val checkPackagedArtifacts = taskKey[Unit]("check the packaged artifacts")
lazy val checkPublish = taskKey[Unit]("check publish")
lazy val checkUpdate = taskKey[Unit]("check update")
def checkPackagedArtifactsDef: Def.Initialize[Task[Unit]] = Def.task {
val packagedArtifacts = Keys.packagedArtifacts.value
val deprecatedArtifacts = packagedArtifacts.keys.filter(a => a.name == "example")
assert(deprecatedArtifacts.size == 4)
val artifactsWithCrossVersion = packagedArtifacts.keys.filter(a => a.name == "example_2.12_1.0")
assert(artifactsWithCrossVersion.size == 4)
val deprecatedPom = deprecatedArtifacts.find(_.`type` == "pom")
assert(deprecatedPom.isDefined)
val deprecatedPomContent = IO.read(packagedArtifacts(deprecatedPom.get))
assert(deprecatedPomContent.contains(s"<artifactId>example</artifactId>"))
assert(deprecatedPomContent.contains(s"<artifactId>sbt-plugin-example-diamond</artifactId>"))
val pomWithCrossVersion = artifactsWithCrossVersion.find(_.`type` == "pom")
assert(pomWithCrossVersion.isDefined)
val pomContent = IO.read(packagedArtifacts(pomWithCrossVersion.get))
assert(pomContent.contains(s"<artifactId>example_2.12_1.0</artifactId>"))
assert(pomContent.contains(s"<artifactId>sbt-plugin-example-diamond_2.12_1.0</artifactId>"))
}
def checkPublishDef: Def.Initialize[Task[Unit]] = Def.task {
val _ = publish.value
val org = organization.value
val files = IO.listFiles(repo / org.replace('.', '/') / "example_2.12_1.0" / "0.1.0-SNAPSHOT")
assert(files.nonEmpty)
val Deprecated = s"example-${Regex.quote("0.1.0-SNAPSHOT")}(-javadoc|-sources)?(\\.jar|\\.pom)".r
val WithCrossVersion = s"example${Regex.quote("_2.12_1.0")}-${Regex.quote("0.1.0-SNAPSHOT")}(-javadoc|-sources)?(\\.jar|\\.pom)".r
val deprecatedJars = files.map(_.name).collect { case jar @ Deprecated(_, ".jar") => jar }
assert(deprecatedJars.size == 3, deprecatedJars.mkString(", ")) // bin, sources and javadoc
val jarsWithCrossVersion = files.map(_.name).collect { case jar @ WithCrossVersion(_, ".jar") => jar }
assert(jarsWithCrossVersion.size == 3, jarsWithCrossVersion.mkString(", ")) // bin, sources and javadoc
val deprecatedPom = files
.find { file =>
file.name match {
case pom @ Deprecated(_, ".pom") => true
case _ => false
}
}
assert(deprecatedPom.isDefined, "missing deprecated pom")
val deprecatedPomContent = IO.read(deprecatedPom.get)
assert(deprecatedPomContent.contains(s"<artifactId>example</artifactId>"))
assert(deprecatedPomContent.contains(s"<artifactId>sbt-plugin-example-diamond</artifactId>"))
val pomWithCrossVersion = files
.find { file =>
file.name match {
case pom @ WithCrossVersion(_, ".pom") => true
case _ => false
}
}
assert(pomWithCrossVersion.isDefined, "missing pom with sbt cross-version _2.12_1.0")
val pomContent = IO.read(pomWithCrossVersion.get)
assert(pomContent.contains(s"<artifactId>example_2.12_1.0</artifactId>"))
assert(pomContent.contains(s"<artifactId>sbt-plugin-example-diamond_2.12_1.0</artifactId>"))
}
def checkUpdateDef(expected: String*): Def.Initialize[Task[Unit]] = Def.task {
val report = update.value
val obtainedFiles = report.configurations
.find(_.configuration.name == Compile.name)
.toSeq
.flatMap(_.modules)
.flatMap(_.artifacts)
.map(_._2)
val obtainedSet = obtainedFiles.map(_.getName).toSet
val expectedSet = expected.toSet + "scala-library.jar"
assert(obtainedSet == expectedSet, obtainedFiles)
}

View File

@ -0,0 +1,11 @@
> example / checkPackagedArtifacts
> example / checkPublish
> testMaven / checkUpdate
> set testMaven / useCoursier := false
> testMaven / checkUpdate
> example / publishLocal
> testLocal / checkUpdate
> set testLocal / useCoursier := false
> testLocal / checkUpdate