mirror of https://github.com/sbt/sbt.git
[2.x] Fix ivyless sbt plugin publish cross paths (#9416)
Problem: When useIvy := false, ivyless publishing dropped the optional scala_[scalaVersion]/ and sbt_[sbtVersion]/ path segments from Ivy-style plugin publish patterns. This caused sbt 1 plugins published from sbt 2 builds to land under non-plugin Ivy paths, so consumers looking under scala_2.12/sbt_1.0/ could not resolve them. Solution: Read the plugin cross-version attributes from the CsrProject module and use them when constructing ivyless Ivy-layout publish paths. Apply the same substitution for local/file Ivy publishing and remote Ivy-style URL publishing, while continuing to omit those optional segments when the attributes are absent. Add scripted regressions for local and HTTP Ivy-style publishing of an sbt 1 plugin with useIvy := false.
This commit is contained in:
parent
f80e2c38c8
commit
e00efb9d9c
|
|
@ -12,11 +12,13 @@ package internal
|
||||||
import java.io.{ File, IOException }
|
import java.io.{ File, IOException }
|
||||||
import java.net.{ URI, URL }
|
import java.net.{ URI, URL }
|
||||||
import java.util.concurrent.Callable
|
import java.util.concurrent.Callable
|
||||||
|
import java.util.regex.Matcher
|
||||||
|
|
||||||
import gigahorse.AuthScheme
|
import gigahorse.AuthScheme
|
||||||
import gigahorse.support.apachehttp.Gigahorse
|
import gigahorse.support.apachehttp.Gigahorse
|
||||||
import sbt.Def.ScopedKey
|
import sbt.Def.ScopedKey
|
||||||
import sbt.internal.librarymanagement.*
|
import sbt.internal.librarymanagement.*
|
||||||
|
import sbt.internal.librarymanagement.mavenint.PomExtraDependencyAttributes
|
||||||
import sbt.librarymanagement.*
|
import sbt.librarymanagement.*
|
||||||
import sbt.librarymanagement.syntax.*
|
import sbt.librarymanagement.syntax.*
|
||||||
import sbt.util.{ CacheStore, CacheStoreFactory, Level, Logger, Tracked }
|
import sbt.util.{ CacheStore, CacheStoreFactory, Level, Logger, Tracked }
|
||||||
|
|
@ -560,6 +562,11 @@ private[sbt] object LibraryManagement {
|
||||||
version == "3-latest.candidate"
|
version == "3-latest.candidate"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private def pluginCrossPath(project: CsrProject): Seq[String] =
|
||||||
|
val attrs = project.module.attributes
|
||||||
|
attrs.get(PomExtraDependencyAttributes.ScalaVersionKey).map("scala_" + _).toSeq ++
|
||||||
|
attrs.get(PomExtraDependencyAttributes.SbtVersionKey).map("sbt_" + _).toSeq
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Publishes artifacts to the local Ivy repository without using Apache Ivy.
|
* Publishes artifacts to the local Ivy repository without using Apache Ivy.
|
||||||
* Uses the pattern: [org]/[module]/[revision]/[types]/[artifact](-[classifier]).[ext]
|
* Uses the pattern: [org]/[module]/[revision]/[types]/[artifact](-[classifier]).[ext]
|
||||||
|
|
@ -576,8 +583,9 @@ private[sbt] object LibraryManagement {
|
||||||
val moduleName = project.module.name.value
|
val moduleName = project.module.name.value
|
||||||
val version = project.version
|
val version = project.version
|
||||||
|
|
||||||
// Base directory: localRepoBase / org / module / version
|
// Base directory: localRepoBase / org / module / (scala_V/)(sbt_V/) / version
|
||||||
val moduleDir = localRepoBase / org / moduleName / version
|
val moduleDir =
|
||||||
|
pluginCrossPath(project).foldLeft(localRepoBase / org / moduleName)(_ / _) / version
|
||||||
|
|
||||||
log.info(s"Publishing to $moduleDir")
|
log.info(s"Publishing to $moduleDir")
|
||||||
|
|
||||||
|
|
@ -638,6 +646,7 @@ private[sbt] object LibraryManagement {
|
||||||
*/
|
*/
|
||||||
private def substituteIvyArtifactPattern(
|
private def substituteIvyArtifactPattern(
|
||||||
pattern: String,
|
pattern: String,
|
||||||
|
project: CsrProject,
|
||||||
org: String,
|
org: String,
|
||||||
moduleName: String,
|
moduleName: String,
|
||||||
version: String,
|
version: String,
|
||||||
|
|
@ -655,8 +664,18 @@ private[sbt] object LibraryManagement {
|
||||||
s = s.replace("[ext]", ext)
|
s = s.replace("[ext]", ext)
|
||||||
if (classifier.nonEmpty) s = s.replace("(-[classifier])", s"-$classifier")
|
if (classifier.nonEmpty) s = s.replace("(-[classifier])", s"-$classifier")
|
||||||
else s = s.replace("(-[classifier])", "")
|
else s = s.replace("(-[classifier])", "")
|
||||||
// Remove optional Ivy pattern parts (scala/sbt version, branch) for ivyless layout
|
// Substitute or drop optional Ivy pattern parts (scala/sbt version), remove branch for ivyless layout
|
||||||
s = s.replaceAll("\\(scala_[^)]+/\\)", "").replaceAll("\\(sbt_[^)]+/\\)", "")
|
val attrs = project.module.attributes
|
||||||
|
val scalaV = attrs.get(PomExtraDependencyAttributes.ScalaVersionKey)
|
||||||
|
val sbtV = attrs.get(PomExtraDependencyAttributes.SbtVersionKey)
|
||||||
|
s = s.replaceAll(
|
||||||
|
"\\(scala_[^)]+/\\)",
|
||||||
|
scalaV.map(v => Matcher.quoteReplacement(s"scala_$v/")).getOrElse("")
|
||||||
|
)
|
||||||
|
s = s.replaceAll(
|
||||||
|
"\\(sbt_[^)]+/\\)",
|
||||||
|
sbtV.map(v => Matcher.quoteReplacement(s"sbt_$v/")).getOrElse("")
|
||||||
|
)
|
||||||
s = s.replaceAll("\\(\\[branch\\]/\\)", "")
|
s = s.replaceAll("\\(\\[branch\\]/\\)", "")
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
@ -750,6 +769,7 @@ private[sbt] object LibraryManagement {
|
||||||
val artifactName = moduleName
|
val artifactName = moduleName
|
||||||
val pathPattern = substituteIvyArtifactPattern(
|
val pathPattern = substituteIvyArtifactPattern(
|
||||||
artifactPattern,
|
artifactPattern,
|
||||||
|
project,
|
||||||
org,
|
org,
|
||||||
moduleName,
|
moduleName,
|
||||||
version,
|
version,
|
||||||
|
|
@ -771,6 +791,7 @@ private[sbt] object LibraryManagement {
|
||||||
val ivyXmlContent = lmcoursier.IvyXml(project, Nil, Nil)
|
val ivyXmlContent = lmcoursier.IvyXml(project, Nil, Nil)
|
||||||
val ivyPathPattern = substituteIvyArtifactPattern(
|
val ivyPathPattern = substituteIvyArtifactPattern(
|
||||||
ivyPattern,
|
ivyPattern,
|
||||||
|
project,
|
||||||
org,
|
org,
|
||||||
moduleName,
|
moduleName,
|
||||||
version,
|
version,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
ThisBuild / csrCacheDirectory := (ThisBuild / baseDirectory).value / "coursier-cache"
|
||||||
|
|
||||||
|
val publishRepoBase = settingKey[File]("Base directory for publish repo (HTTP server writes here)")
|
||||||
|
|
||||||
|
val publishPort = 3032
|
||||||
|
|
||||||
|
lazy val root = (project in file("."))
|
||||||
|
.enablePlugins(SbtPlugin)
|
||||||
|
.settings(
|
||||||
|
name := "sbt-test-plugin",
|
||||||
|
organization := "com.example",
|
||||||
|
version := "0.1.0-SNAPSHOT",
|
||||||
|
scalaVersion := "2.12.21",
|
||||||
|
pluginCrossBuild / sbtVersion := "1.5.8",
|
||||||
|
publishRepoBase := baseDirectory.value / "repo",
|
||||||
|
publishTo := Some(
|
||||||
|
Resolver
|
||||||
|
.url("test-repo", new java.net.URI(s"http://localhost:$publishPort/").toURL)(using
|
||||||
|
Resolver.ivyStylePatterns
|
||||||
|
)
|
||||||
|
.withAllowInsecureProtocol(true)
|
||||||
|
),
|
||||||
|
useIvy := false,
|
||||||
|
Compile / packageDoc / publishArtifact := false,
|
||||||
|
Compile / packageSrc / publishArtifact := false,
|
||||||
|
)
|
||||||
|
|
||||||
|
val startPublishServer = taskKey[Unit]("Start HTTP server that accepts PUT to repo directory")
|
||||||
|
startPublishServer := {
|
||||||
|
HttpPutServer.start(publishPort, publishRepoBase.value)
|
||||||
|
streams.value.log.info(s"HTTP PUT server started on port $publishPort, writing to ${publishRepoBase.value}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val stopPublishServer = taskKey[Unit]("Stop HTTP server")
|
||||||
|
stopPublishServer := {
|
||||||
|
HttpPutServer.stop()
|
||||||
|
streams.value.log.info("HTTP server stopped")
|
||||||
|
}
|
||||||
|
|
||||||
|
val publishToHttp = taskKey[Unit]("Publish to HTTP server (start server, publish, stop server)")
|
||||||
|
publishToHttp := {
|
||||||
|
startPublishServer.value
|
||||||
|
try publish.value
|
||||||
|
finally stopPublishServer.value
|
||||||
|
}
|
||||||
|
|
||||||
|
val checkPublished = taskKey[Unit]("Check ivyless publish wrote the plugin cross-suffix PUT path")
|
||||||
|
checkPublished := {
|
||||||
|
val log = streams.value.log
|
||||||
|
val base = publishRepoBase.value
|
||||||
|
val org = organization.value
|
||||||
|
val moduleName = normalizedName.value
|
||||||
|
val ver = version.value
|
||||||
|
|
||||||
|
val moduleDir = base / org / moduleName / "scala_2.12" / "sbt_1.0" / ver
|
||||||
|
log.info(s"Checking published files in $moduleDir")
|
||||||
|
|
||||||
|
def listDir(dir: File, indent: String = ""): Unit =
|
||||||
|
if (dir.exists)
|
||||||
|
dir.listFiles.foreach { f =>
|
||||||
|
log.info(s"$indent${f.getName}")
|
||||||
|
if (f.isDirectory) listDir(f, indent + " ")
|
||||||
|
}
|
||||||
|
else log.info(s"${indent}Directory does not exist: $dir")
|
||||||
|
log.info("Contents of publish repo:")
|
||||||
|
listDir(base)
|
||||||
|
|
||||||
|
assert(moduleDir.isDirectory, s"Expected plugin cross-suffix directory $moduleDir to exist")
|
||||||
|
|
||||||
|
val expectedDirs = Seq("jars", "poms", "ivys")
|
||||||
|
expectedDirs.foreach { dir =>
|
||||||
|
val d = moduleDir / dir
|
||||||
|
assert(d.exists && d.isDirectory, s"Expected directory $d to exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
val jarFile = moduleDir / "jars" / s"$moduleName.jar"
|
||||||
|
assert(jarFile.exists, s"Expected $jarFile to exist")
|
||||||
|
assert((moduleDir / "jars" / s"$moduleName.jar.md5").exists, s"Expected md5 checksum to exist")
|
||||||
|
assert((moduleDir / "jars" / s"$moduleName.jar.sha1").exists, s"Expected sha1 checksum to exist")
|
||||||
|
|
||||||
|
val ivyFile = moduleDir / "ivys" / "ivy.xml"
|
||||||
|
assert(ivyFile.exists, s"Expected $ivyFile to exist")
|
||||||
|
assert((moduleDir / "ivys" / "ivy.xml.md5").exists, s"Expected ivy.xml md5 checksum to exist")
|
||||||
|
assert((moduleDir / "ivys" / "ivy.xml.sha1").exists, s"Expected ivy.xml sha1 checksum to exist")
|
||||||
|
|
||||||
|
val ivyContent = IO.read(ivyFile)
|
||||||
|
assert(ivyContent.contains(s"""organisation="$org""""), s"ivy.xml should contain organisation")
|
||||||
|
assert(ivyContent.contains(s"""module="$moduleName""""), s"ivy.xml should contain module name")
|
||||||
|
assert(ivyContent.contains(s"""revision="$ver""""), s"ivy.xml should contain revision")
|
||||||
|
|
||||||
|
val nonCrossDir = base / org / moduleName / ver
|
||||||
|
assert(!nonCrossDir.exists, s"Non-cross directory $nonCrossDir should not exist; cross suffixes are missing")
|
||||||
|
|
||||||
|
log.info("All ivyless publish (HTTP plugin) checks passed!")
|
||||||
|
}
|
||||||
|
|
||||||
|
val cleanPublishRepo = taskKey[Unit]("Clean the publish repo")
|
||||||
|
cleanPublishRepo := {
|
||||||
|
IO.delete(publishRepoBase.value)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import java.io._
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import com.sun.net.httpserver.{ HttpExchange, HttpHandler, HttpServer }
|
||||||
|
|
||||||
|
/** Minimal HTTP server that accepts PUT and writes to a base directory (for ivyless publish scripted test). */
|
||||||
|
object HttpPutServer {
|
||||||
|
private var server: HttpServer = null
|
||||||
|
|
||||||
|
def start(port: Int, baseDir: java.io.File): Unit = {
|
||||||
|
if (server != null) stop()
|
||||||
|
server = HttpServer.create(new InetSocketAddress(port), 0)
|
||||||
|
server.createContext("/", new HttpHandler {
|
||||||
|
override def handle(ex: HttpExchange): Unit = {
|
||||||
|
val method = ex.getRequestMethod
|
||||||
|
if ("PUT".equalsIgnoreCase(method)) {
|
||||||
|
val path = ex.getRequestURI.getRawPath
|
||||||
|
val relativePath = if (path.startsWith("/")) path.substring(1) else path
|
||||||
|
val targetFile = new File(baseDir, relativePath.replace("/", File.separator))
|
||||||
|
targetFile.getParentFile.mkdirs()
|
||||||
|
val in = ex.getRequestBody
|
||||||
|
val out = new FileOutputStream(targetFile)
|
||||||
|
try {
|
||||||
|
in.transferTo(out)
|
||||||
|
} finally {
|
||||||
|
out.close()
|
||||||
|
in.close()
|
||||||
|
}
|
||||||
|
ex.sendResponseHeaders(200, -1)
|
||||||
|
ex.close()
|
||||||
|
} else {
|
||||||
|
ex.sendResponseHeaders(405, -1)
|
||||||
|
ex.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
server.setExecutor(null)
|
||||||
|
server.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
def stop(): Unit = {
|
||||||
|
if (server != null) {
|
||||||
|
server.stop(0)
|
||||||
|
server = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
// empty plugins file
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.example
|
||||||
|
|
||||||
|
object Plugin {
|
||||||
|
def hello: String = "Hello from sbt 1 plugin!"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Test ivyless publish to HTTP Ivy-style repo for an sbt 1 plugin (issue #9383)
|
||||||
|
# When useIvy is false, the Ivy-pattern PUT path must include the
|
||||||
|
# scala_2.12/sbt_1.0 cross suffixes for sbt 1 plugins.
|
||||||
|
|
||||||
|
> cleanPublishRepo
|
||||||
|
> publishToHttp
|
||||||
|
> checkPublished
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
ThisBuild / csrCacheDirectory := (ThisBuild / baseDirectory).value / "coursier-cache"
|
||||||
|
|
||||||
|
val ivyLocalBase = settingKey[File]("Local Ivy repository base")
|
||||||
|
|
||||||
|
lazy val root = (project in file("."))
|
||||||
|
.enablePlugins(SbtPlugin)
|
||||||
|
.settings(
|
||||||
|
name := "sbt-test-plugin",
|
||||||
|
organization := "com.example",
|
||||||
|
version := "0.1.0-SNAPSHOT",
|
||||||
|
scalaVersion := "2.12.21",
|
||||||
|
pluginCrossBuild / sbtVersion := "1.5.8",
|
||||||
|
ivyLocalBase := baseDirectory.value / "local-ivy-repo",
|
||||||
|
ivyPaths := IvyPaths(baseDirectory.value.toString, Some(ivyLocalBase.value.toString)),
|
||||||
|
useIvy := false,
|
||||||
|
Compile / packageDoc / publishArtifact := false,
|
||||||
|
Compile / packageSrc / publishArtifact := false,
|
||||||
|
)
|
||||||
|
|
||||||
|
val cleanLocalIvy = taskKey[Unit]("Clean the local ivy repo")
|
||||||
|
cleanLocalIvy := {
|
||||||
|
IO.delete(ivyLocalBase.value / "local")
|
||||||
|
}
|
||||||
|
|
||||||
|
val checkPublished = taskKey[Unit]("Check ivyless publishLocal wrote the plugin cross-suffix path")
|
||||||
|
checkPublished := {
|
||||||
|
val log = streams.value.log
|
||||||
|
val base = ivyLocalBase.value / "local"
|
||||||
|
val org = organization.value
|
||||||
|
val moduleName = normalizedName.value
|
||||||
|
val ver = version.value
|
||||||
|
|
||||||
|
val moduleDir = base / org / moduleName / "scala_2.12" / "sbt_1.0" / ver
|
||||||
|
log.info(s"Checking published files in $moduleDir")
|
||||||
|
|
||||||
|
def listDir(dir: File, indent: String = ""): Unit =
|
||||||
|
if (dir.exists)
|
||||||
|
dir.listFiles.foreach { f =>
|
||||||
|
log.info(s"$indent${f.getName}")
|
||||||
|
if (f.isDirectory) listDir(f, indent + " ")
|
||||||
|
}
|
||||||
|
else log.info(s"${indent}Directory does not exist: $dir")
|
||||||
|
log.info("Contents of ivyLocalBase:")
|
||||||
|
listDir(ivyLocalBase.value)
|
||||||
|
|
||||||
|
assert(moduleDir.isDirectory, s"Expected plugin cross-suffix directory $moduleDir to exist")
|
||||||
|
|
||||||
|
val expectedDirs = Seq("jars", "poms", "ivys")
|
||||||
|
expectedDirs.foreach { dir =>
|
||||||
|
val d = moduleDir / dir
|
||||||
|
assert(d.exists && d.isDirectory, s"Expected directory $d to exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
val jarFile = moduleDir / "jars" / s"$moduleName.jar"
|
||||||
|
assert(jarFile.exists, s"Expected $jarFile to exist")
|
||||||
|
assert((moduleDir / "jars" / s"$moduleName.jar.md5").exists, s"Expected md5 checksum to exist")
|
||||||
|
assert((moduleDir / "jars" / s"$moduleName.jar.sha1").exists, s"Expected sha1 checksum to exist")
|
||||||
|
|
||||||
|
val ivyFile = moduleDir / "ivys" / "ivy.xml"
|
||||||
|
assert(ivyFile.exists, s"Expected $ivyFile to exist")
|
||||||
|
assert((moduleDir / "ivys" / "ivy.xml.md5").exists, s"Expected ivy.xml md5 checksum to exist")
|
||||||
|
assert((moduleDir / "ivys" / "ivy.xml.sha1").exists, s"Expected ivy.xml sha1 checksum to exist")
|
||||||
|
|
||||||
|
val ivyContent = IO.read(ivyFile)
|
||||||
|
assert(ivyContent.contains(s"""organisation="$org""""), s"ivy.xml should contain organisation")
|
||||||
|
assert(ivyContent.contains(s"""module="$moduleName""""), s"ivy.xml should contain module name")
|
||||||
|
assert(ivyContent.contains(s"""revision="$ver""""), s"ivy.xml should contain revision")
|
||||||
|
|
||||||
|
val nonCrossDir = base / org / moduleName / ver
|
||||||
|
assert(!nonCrossDir.exists, s"Non-cross directory $nonCrossDir should not exist; cross suffixes are missing")
|
||||||
|
|
||||||
|
log.info("All ivyless publishLocal plugin checks passed!")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
// empty plugins file
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.example
|
||||||
|
|
||||||
|
object Plugin {
|
||||||
|
def hello: String = "Hello from sbt 1 plugin!"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Test ivyless publishLocal for an sbt 1 plugin (issue #9383)
|
||||||
|
# When useIvy is false, publishLocal must write artifacts under the
|
||||||
|
# scala_2.12/sbt_1.0 cross-suffix path, matching the Ivy PluginPattern.
|
||||||
|
|
||||||
|
> cleanLocalIvy
|
||||||
|
> publishLocal
|
||||||
|
> checkPublished
|
||||||
Loading…
Reference in New Issue