From 8e64caae8f01283cbfaa1df8761265c7d8f3c49b Mon Sep 17 00:00:00 2001 From: Adrien Piquerez Date: Thu, 15 Dec 2022 13:44:11 +0100 Subject: [PATCH 1/4] Add scripted to test the sbt plugin resolution As of sbt 1.9, we publish deprecated and valid poms. In this test we check that sbt resolve the valid pom of an sbt plugin and fallback to the deprecated pom if the valid pom cannot be found. --- .../sbt-plugin-diamond/build.sbt | 105 ++++++++++++++++++ .../sbt-plugin-diamond/test | 12 ++ 2 files changed, 117 insertions(+) create mode 100644 sbt-app/src/sbt-test/dependency-management/sbt-plugin-diamond/build.sbt create mode 100644 sbt-app/src/sbt-test/dependency-management/sbt-plugin-diamond/test diff --git a/sbt-app/src/sbt-test/dependency-management/sbt-plugin-diamond/build.sbt b/sbt-app/src/sbt-test/dependency-management/sbt-plugin-diamond/build.sbt new file mode 100644 index 000000000..1a33daa26 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/sbt-plugin-diamond/build.sbt @@ -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) +} 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 new file mode 100644 index 000000000..2ba8a5cca --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/sbt-plugin-diamond/test @@ -0,0 +1,12 @@ +> v1/checkUpdate +> v2/checkUpdate +> v3/checkUpdate +> v4/checkUpdate +> v5/checkUpdate + +> set ThisBuild/useCoursier:=true +> v1/checkUpdate +> v2/checkUpdate +> v3/checkUpdate +> v4/checkUpdate +> v5/checkUpdate From 8020ec4d7cc1efc5275375e9a9585ecc18a213b4 Mon Sep 17 00:00:00 2001 From: Adrien Piquerez Date: Mon, 19 Dec 2022 17:17:22 +0100 Subject: [PATCH 2/4] 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 From d05913f3b94658bcb019b8c800f8214a59295735 Mon Sep 17 00:00:00 2001 From: Adrien Piquerez Date: Wed, 22 Feb 2023 09:37:51 +0100 Subject: [PATCH 3/4] Bump lm-coursier-shaded to 2.0.16 --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index c25f2dcbd..ac2368e46 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -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 From ada716fc1f942996c3c0c7f7d86d6af08150034f Mon Sep 17 00:00:00 2001 From: Adrien Piquerez Date: Mon, 27 Feb 2023 09:42:36 +0100 Subject: [PATCH 4/4] Bump librarymanagement to 1.9.0-M1 --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index ac2368e46..afacaf68c 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -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