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/project/Dependencies.scala b/project/Dependencies.scala
index c25f2dcbd..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
@@ -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
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..32fc99ced
--- /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:=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