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