From 8020ec4d7cc1efc5275375e9a9585ecc18a213b4 Mon Sep 17 00:00:00 2001 From: Adrien Piquerez Date: Mon, 19 Dec 2022 17:17:22 +0100 Subject: [PATCH] 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. --- main/src/main/scala/sbt/Defaults.scala | 45 ++++++- .../sbt-plugin-diamond/test | 22 ++-- .../sbt-plugin-publish/build.sbt | 122 ++++++++++++++++++ .../sbt-plugin-publish/test | 11 ++ 4 files changed, 188 insertions(+), 12 deletions(-) create mode 100644 sbt-app/src/sbt-test/dependency-management/sbt-plugin-publish/build.sbt create mode 100644 sbt-app/src/sbt-test/dependency-management/sbt-plugin-publish/test diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 81aa5e509..5c02b1a52 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -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, diff --git a/sbt-app/src/sbt-test/dependency-management/sbt-plugin-diamond/test b/sbt-app/src/sbt-test/dependency-management/sbt-plugin-diamond/test index 2ba8a5cca..32fc99ced 100644 --- a/sbt-app/src/sbt-test/dependency-management/sbt-plugin-diamond/test +++ b/sbt-app/src/sbt-test/dependency-management/sbt-plugin-diamond/test @@ -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 diff --git a/sbt-app/src/sbt-test/dependency-management/sbt-plugin-publish/build.sbt b/sbt-app/src/sbt-test/dependency-management/sbt-plugin-publish/build.sbt new file mode 100644 index 000000000..bc05305f8 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/sbt-plugin-publish/build.sbt @@ -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"example")) + assert(deprecatedPomContent.contains(s"sbt-plugin-example-diamond")) + + val pomWithCrossVersion = artifactsWithCrossVersion.find(_.`type` == "pom") + assert(pomWithCrossVersion.isDefined) + val pomContent = IO.read(packagedArtifacts(pomWithCrossVersion.get)) + assert(pomContent.contains(s"example_2.12_1.0")) + assert(pomContent.contains(s"sbt-plugin-example-diamond_2.12_1.0")) +} + +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"example")) + assert(deprecatedPomContent.contains(s"sbt-plugin-example-diamond")) + + 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"example_2.12_1.0")) + assert(pomContent.contains(s"sbt-plugin-example-diamond_2.12_1.0")) +} + +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) +} diff --git a/sbt-app/src/sbt-test/dependency-management/sbt-plugin-publish/test b/sbt-app/src/sbt-test/dependency-management/sbt-plugin-publish/test new file mode 100644 index 000000000..ec720452b --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/sbt-plugin-publish/test @@ -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