Smooth transition to valid Maven pattern of sbt plugin

For an sbt plugin, we publish two POM files, the legacy one, and the
new Maven compatible one. The name of the new POM file contains the sbt
cross-version _2.12_1.0. The format of the new POM file is also slightly
different, because we append the sbt cross-version to all artifactIds of
sbt plugins. Hence Maven can resolve the new sbt plugin POM and its
dependencies.

When resolving an sbt plugin, we first try to resolve the new Maven POM
and if it fails we fallback on the legacy one. When parsing the new POM
format, we remove the sbt cross-version from all artifact IDs so that
there is no mismatch between old and new format of dependencies.
This commit is contained in:
Adrien Piquerez 2022-12-19 17:17:22 +01:00
parent 8e64caae8f
commit 8020ec4d7c
4 changed files with 188 additions and 12 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

@ -1,12 +1,12 @@
> v1/checkUpdate
> v2/checkUpdate
> v3/checkUpdate
> v4/checkUpdate
> v5/checkUpdate
> v1 / checkUpdate
> v2 / checkUpdate
> v3 / checkUpdate
> v4 / checkUpdate
> v5 / checkUpdate
> set ThisBuild/useCoursier:=true
> 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