Merge pull request #9446 from eed3si9n/bport2/mix2

[2.0.x] Backports
This commit is contained in:
eugene yokota 2026-07-12 23:03:33 -04:00 committed by GitHub
commit 6207378a49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 363 additions and 5 deletions

View File

@ -3603,7 +3603,13 @@ object Classpaths {
o %% "librarymanagement-ivy",
o %% "util-logging",
o %% "util-position",
o %% "io"
o %% "io",
"com.eed3si9n" %% "sjson-new-core",
"com.eed3si9n" %% "gigahorse-core",
"com.typesafe" %% "ssl-config-core",
"org.reactivestreams" % "reactive-streams",
"org.slf4j" % "slf4j-api",
"com.typesafe" % "config",
)
if (isMeta && !force) excludes.toVector ++ sbtModulesExcludes
else excludes

View File

@ -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 }
@ -560,6 +562,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]
@ -576,8 +583,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")
@ -638,6 +646,7 @@ private[sbt] object LibraryManagement {
*/
private def substituteIvyArtifactPattern(
pattern: String,
project: CsrProject,
org: String,
moduleName: String,
version: String,
@ -655,8 +664,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
}
@ -750,6 +769,7 @@ private[sbt] object LibraryManagement {
val artifactName = moduleName
val pathPattern = substituteIvyArtifactPattern(
artifactPattern,
project,
org,
moduleName,
version,
@ -771,6 +791,7 @@ private[sbt] object LibraryManagement {
val ivyXmlContent = lmcoursier.IvyXml(project, Nil, Nil)
val ivyPathPattern = substituteIvyArtifactPattern(
ivyPattern,
project,
org,
moduleName,
version,

View File

@ -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)
}

View File

@ -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
}
}
}

View File

@ -0,0 +1 @@
// empty plugins file

View File

@ -0,0 +1,5 @@
package com.example
object Plugin {
def hello: String = "Hello from sbt 1 plugin!"
}

View File

@ -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

View File

@ -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!")
}

View File

@ -0,0 +1 @@
// empty plugins file

View File

@ -0,0 +1,5 @@
package com.example
object Plugin {
def hello: String = "Hello from sbt 1 plugin!"
}

View File

@ -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

View File

@ -0,0 +1,69 @@
ThisBuild / scalaVersion := "3.8.4"
ThisBuild / organization := "com.example"
libraryDependencies += "com.typesafe" % "config" % "1.4.5"
val checkMetabuildClasspath = taskKey[Unit]("Checks the effective metabuild classpath")
val checkProjectClasspath = taskKey[Unit]("Checks the project classpath")
checkMetabuildClasspath := Def.uncached {
val buildUnit = loadedBuild.value.units(thisProjectRef.value.build).unit
val classpath = buildUnit.plugins.pluginData.dependencyClasspath
val managedModules = classpath
.flatMap(_.get(Keys.moduleIDStr))
.map(Classpaths.moduleIdJsonKeyFormat.read)
.map(module => module.organization -> module.name)
.toSet
val launcherOwnedModules = Set(
"com.eed3si9n" -> "sjson-new-core_3",
"com.eed3si9n" -> "gigahorse-core_3",
"com.typesafe" -> "ssl-config-core_3",
"org.reactivestreams" -> "reactive-streams",
"org.slf4j" -> "slf4j-api",
"com.typesafe" -> "config",
)
val pluginModules = Set(
"ch.epfl.scala" -> "sbt-github-dependency-submission_sbt2_3",
"com.eed3si9n" -> "gigahorse-asynchttpclient_3",
"com.eed3si9n" -> "shaded-asynchttpclient",
)
val converter = buildUnit.converter
val launcherJars = classpath
.filter(_.get(Keys.moduleIDStr).isEmpty)
.map(entry => converter.toPath(entry.data).getFileName.toString)
val launcherJarNames = Set(
"sjson-new-core_3",
"gigahorse-core_3",
"ssl-config-core_3",
"reactive-streams",
"slf4j-api",
"config",
)
assert(
managedModules.intersect(launcherOwnedModules).isEmpty,
s"found plugin-managed sbt modules: ${managedModules.intersect(launcherOwnedModules)}",
)
assert(
pluginModules.subsetOf(managedModules),
s"missing plugin modules: ${pluginModules.diff(managedModules)}",
)
assert(
launcherJarNames.forall(name => launcherJars.count(_.startsWith(s"$name-")) == 1),
s"expected one launcher jar for each sbt module: $launcherJars",
)
}
checkProjectClasspath := Def.uncached {
val modules = (Compile / externalDependencyClasspath).value
.flatMap(_.get(Keys.moduleIDStr))
.map(Classpaths.moduleIdJsonKeyFormat.read)
assert(
modules.exists(module =>
module.organization == "com.typesafe" &&
module.name == "config" &&
module.revision == "1.4.5"
),
s"missing project dependency: $modules",
)
}

View File

@ -0,0 +1,6 @@
import sbt.*
import sbt.Keys.*
object Example {
lazy val settings = Seq(javaOptions += "-Dfoo=bar")
}

View File

@ -0,0 +1 @@
addSbtPlugin("ch.epfl.scala" % "sbt-github-dependency-submission" % "3.2.3")

View File

@ -0,0 +1,10 @@
# Issue 8207: a metabuild plugin must not downgrade sjson-new core.
# Loading this build exercises the metabuild compile, which summons
# the JsonFormat for String via the caching macro on javaOptions.
# When the plugin downgrades sjson-new core on the metabuild
# classpath, project load fails with a missing given. A successful
# load is the regression assertion.
> projects
> checkMetabuildClasspath
> checkProjectClasspath
> githubGenerateSnapshot {}