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
66f38defdc
commit
e7d25a0971
|
|
@ -12,11 +12,13 @@ package internal
|
|||
import java.io.{ File, IOException }
|
||||
import java.net.{ URI, URL }
|
||||
import java.util.concurrent.Callable
|
||||
import java.util.regex.Matcher
|
||||
|
||||
import gigahorse.AuthScheme
|
||||
import gigahorse.support.apachehttp.Gigahorse
|
||||
import sbt.Def.ScopedKey
|
||||
import sbt.internal.librarymanagement.*
|
||||
import sbt.internal.librarymanagement.mavenint.PomExtraDependencyAttributes
|
||||
import sbt.librarymanagement.*
|
||||
import sbt.librarymanagement.syntax.*
|
||||
import sbt.util.{ CacheStore, CacheStoreFactory, Level, Logger, Tracked }
|
||||
|
|
@ -549,6 +551,11 @@ private[sbt] object LibraryManagement {
|
|||
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.
|
||||
* Uses the pattern: [org]/[module]/[revision]/[types]/[artifact](-[classifier]).[ext]
|
||||
|
|
@ -565,8 +572,9 @@ private[sbt] object LibraryManagement {
|
|||
val moduleName = project.module.name.value
|
||||
val version = project.version
|
||||
|
||||
// Base directory: localRepoBase / org / module / version
|
||||
val moduleDir = localRepoBase / org / moduleName / version
|
||||
// Base directory: localRepoBase / org / module / (scala_V/)(sbt_V/) / version
|
||||
val moduleDir =
|
||||
pluginCrossPath(project).foldLeft(localRepoBase / org / moduleName)(_ / _) / version
|
||||
|
||||
log.info(s"Publishing to $moduleDir")
|
||||
|
||||
|
|
@ -636,6 +644,7 @@ private[sbt] object LibraryManagement {
|
|||
*/
|
||||
private def substituteIvyArtifactPattern(
|
||||
pattern: String,
|
||||
project: CsrProject,
|
||||
org: String,
|
||||
moduleName: String,
|
||||
version: String,
|
||||
|
|
@ -653,8 +662,18 @@ private[sbt] object LibraryManagement {
|
|||
s = s.replace("[ext]", ext)
|
||||
if (classifier.nonEmpty) s = s.replace("(-[classifier])", s"-$classifier")
|
||||
else s = s.replace("(-[classifier])", "")
|
||||
// Remove optional Ivy pattern parts (scala/sbt version, branch) for ivyless layout
|
||||
s = s.replaceAll("\\(scala_[^)]+/\\)", "").replaceAll("\\(sbt_[^)]+/\\)", "")
|
||||
// Substitute or drop optional Ivy pattern parts (scala/sbt version), remove branch for ivyless layout
|
||||
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
|
||||
}
|
||||
|
|
@ -748,6 +767,7 @@ private[sbt] object LibraryManagement {
|
|||
val artifactName = moduleName
|
||||
val pathPattern = substituteIvyArtifactPattern(
|
||||
artifactPattern,
|
||||
project,
|
||||
org,
|
||||
moduleName,
|
||||
version,
|
||||
|
|
@ -769,6 +789,7 @@ private[sbt] object LibraryManagement {
|
|||
val ivyXmlContent = lmcoursier.IvyXml(project, Nil, Nil)
|
||||
val ivyPathPattern = substituteIvyArtifactPattern(
|
||||
ivyPattern,
|
||||
project,
|
||||
org,
|
||||
moduleName,
|
||||
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