Merge pull request #8138 from eed3si9n/wip/merge-1.11.x

[2.x] Merge branch '1.11.x'
This commit is contained in:
eugene yokota 2025-05-27 00:25:00 -04:00 committed by GitHub
commit 5fabfdff32
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 489 additions and 9 deletions

View File

@ -646,6 +646,7 @@ lazy val remoteCacheProj = (project in file("sbt-remote-cache"))
// Implementation and support code for defining actions.
lazy val actionsProj = (project in file("main-actions"))
.enablePlugins(ContrabandPlugin, JsonCodecPlugin)
.dependsOn(
completeProj,
runProj,
@ -661,7 +662,11 @@ lazy val actionsProj = (project in file("main-actions"))
testedBaseSettings,
name := "Actions",
libraryDependencies += sjsonNewScalaJson.value,
libraryDependencies += jline3Terminal,
libraryDependencies ++= Seq(gigahorseApacheHttp, jline3Terminal),
Compile / managedSourceDirectories +=
baseDirectory.value / "src" / "main" / "contraband-scala",
Compile / generateContrabands / sourceManaged := baseDirectory.value / "src" / "main" / "contraband-scala",
Compile / generateContrabands / contrabandFormatsForType := ContrabandConfig.getFormats,
mimaSettings,
mimaBinaryIssueFilters ++= Seq(
// Removed unused private[sbt] nested class

View File

@ -0,0 +1,17 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.internal.sona
sealed abstract class DeploymentState extends Serializable
object DeploymentState {
case object PENDING extends DeploymentState
case object VALIDATING extends DeploymentState
case object VALIDATED extends DeploymentState
case object PUBLISHING extends DeploymentState
case object PUBLISHED extends DeploymentState
case object FAILED extends DeploymentState
}

View File

@ -0,0 +1,45 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.internal.sona
/** https://central.sonatype.org/publish/publish-portal-api/#uploading-a-deployment-bundle */
final class PublisherStatus private (
val deploymentId: String,
val deploymentName: String,
val deploymentState: sbt.internal.sona.DeploymentState,
val purls: Vector[String]) extends Serializable {
override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match {
case x: PublisherStatus => (this.deploymentId == x.deploymentId) && (this.deploymentName == x.deploymentName) && (this.deploymentState == x.deploymentState) && (this.purls == x.purls)
case _ => false
})
override def hashCode: Int = {
37 * (37 * (37 * (37 * (37 * (17 + "sbt.internal.sona.PublisherStatus".##) + deploymentId.##) + deploymentName.##) + deploymentState.##) + purls.##)
}
override def toString: String = {
"PublisherStatus(" + deploymentId + ", " + deploymentName + ", " + deploymentState + ", " + purls + ")"
}
private def copy(deploymentId: String = deploymentId, deploymentName: String = deploymentName, deploymentState: sbt.internal.sona.DeploymentState = deploymentState, purls: Vector[String] = purls): PublisherStatus = {
new PublisherStatus(deploymentId, deploymentName, deploymentState, purls)
}
def withDeploymentId(deploymentId: String): PublisherStatus = {
copy(deploymentId = deploymentId)
}
def withDeploymentName(deploymentName: String): PublisherStatus = {
copy(deploymentName = deploymentName)
}
def withDeploymentState(deploymentState: sbt.internal.sona.DeploymentState): PublisherStatus = {
copy(deploymentState = deploymentState)
}
def withPurls(purls: Vector[String]): PublisherStatus = {
copy(purls = purls)
}
}
object PublisherStatus {
def apply(deploymentId: String, deploymentName: String, deploymentState: sbt.internal.sona.DeploymentState, purls: Vector[String]): PublisherStatus = new PublisherStatus(deploymentId, deploymentName, deploymentState, purls)
}

View File

@ -0,0 +1,37 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.internal.sona.codec
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
trait DeploymentStateFormats { self: sjsonnew.BasicJsonProtocol =>
implicit lazy val DeploymentStateFormat: JsonFormat[sbt.internal.sona.DeploymentState] = new JsonFormat[sbt.internal.sona.DeploymentState] {
override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.sona.DeploymentState = {
__jsOpt match {
case Some(__js) =>
unbuilder.readString(__js) match {
case "PENDING" => sbt.internal.sona.DeploymentState.PENDING
case "VALIDATING" => sbt.internal.sona.DeploymentState.VALIDATING
case "VALIDATED" => sbt.internal.sona.DeploymentState.VALIDATED
case "PUBLISHING" => sbt.internal.sona.DeploymentState.PUBLISHING
case "PUBLISHED" => sbt.internal.sona.DeploymentState.PUBLISHED
case "FAILED" => sbt.internal.sona.DeploymentState.FAILED
}
case None =>
deserializationError("Expected JsString but found None")
}
}
override def write[J](obj: sbt.internal.sona.DeploymentState, builder: Builder[J]): Unit = {
val str = obj match {
case sbt.internal.sona.DeploymentState.PENDING => "PENDING"
case sbt.internal.sona.DeploymentState.VALIDATING => "VALIDATING"
case sbt.internal.sona.DeploymentState.VALIDATED => "VALIDATED"
case sbt.internal.sona.DeploymentState.PUBLISHING => "PUBLISHING"
case sbt.internal.sona.DeploymentState.PUBLISHED => "PUBLISHED"
case sbt.internal.sona.DeploymentState.FAILED => "FAILED"
}
builder.writeString(str)
}
}
}

View File

@ -0,0 +1,10 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.internal.sona.codec
trait JsonProtocol extends sjsonnew.BasicJsonProtocol
with sbt.internal.sona.codec.DeploymentStateFormats
with sbt.internal.sona.codec.PublisherStatusFormats
object JsonProtocol extends JsonProtocol

View File

@ -0,0 +1,33 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.internal.sona.codec
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
trait PublisherStatusFormats { self: sbt.internal.sona.codec.DeploymentStateFormats & sjsonnew.BasicJsonProtocol =>
implicit lazy val PublisherStatusFormat: JsonFormat[sbt.internal.sona.PublisherStatus] = new JsonFormat[sbt.internal.sona.PublisherStatus] {
override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.sona.PublisherStatus = {
__jsOpt match {
case Some(__js) =>
unbuilder.beginObject(__js)
val deploymentId = unbuilder.readField[String]("deploymentId")
val deploymentName = unbuilder.readField[String]("deploymentName")
val deploymentState = unbuilder.readField[sbt.internal.sona.DeploymentState]("deploymentState")
val purls = unbuilder.readField[Vector[String]]("purls")
unbuilder.endObject()
sbt.internal.sona.PublisherStatus(deploymentId, deploymentName, deploymentState, purls)
case None =>
deserializationError("Expected JsObject but found None")
}
}
override def write[J](obj: sbt.internal.sona.PublisherStatus, builder: Builder[J]): Unit = {
builder.beginObject()
builder.addField("deploymentId", obj.deploymentId)
builder.addField("deploymentName", obj.deploymentName)
builder.addField("deploymentState", obj.deploymentState)
builder.addField("purls", obj.purls)
builder.endObject()
}
}
}

View File

@ -0,0 +1,21 @@
package sbt.internal.sona
@target(Scala)
@codecPackage("sbt.internal.sona.codec")
@fullCodec("JsonProtocol")
enum DeploymentState {
PENDING
VALIDATING
VALIDATED
PUBLISHING
PUBLISHED
FAILED
}
## https://central.sonatype.org/publish/publish-portal-api/#uploading-a-deployment-bundle
type PublisherStatus {
deploymentId: String!
deploymentName: String!
deploymentState: sbt.internal.sona.DeploymentState!
purls: [String]
}

View File

@ -0,0 +1,195 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
package internal
package sona
import gigahorse.*, support.apachehttp.Gigahorse
import java.net.URLEncoder
import java.util.Base64
import java.nio.charset.StandardCharsets
import java.nio.file.Path
import sbt.util.Logger
import sjsonnew.JsonFormat
import sjsonnew.support.scalajson.unsafe.{ Converter, Parser }
import sjsonnew.shaded.scalajson.ast.unsafe.JValue
import scala.annotation.nowarn
import scala.concurrent.*, duration.*
class Sona(client: SonaClient) extends AutoCloseable {
def uploadBundle(
bundleZipPath: Path,
deploymentName: String,
pt: PublishingType,
log: Logger,
): Unit = {
val deploymentId = client.uploadBundle(bundleZipPath, deploymentName, pt, log)
client.waitForDeploy(deploymentId, deploymentName, pt, 1, log)
}
def close(): Unit = client.close()
}
class SonaClient(reqTransform: Request => Request) extends AutoCloseable {
import SonaClient.baseUrl
val gigahorseConfig = Gigahorse.config
.withRequestTimeout(2.minute)
.withReadTimeout(2.minute)
val http = Gigahorse.http(gigahorseConfig)
def uploadBundle(
bundleZipPath: Path,
deploymentName: String,
publishingType: PublishingType,
log: Logger,
): String = {
val res = retryF(maxAttempt = 2) { (attempt: Int) =>
log.info(s"uploading bundle to the Central Portal (attempt: $attempt)")
// addQuery string doesn't work for post
val q = queryString(
"name" -> deploymentName,
"publishingType" -> (publishingType match {
case PublishingType.Automatic => "AUTOMATIC"
case PublishingType.UserManaged => "USER_MANAGED"
})
)
val req = Gigahorse
.url(s"${baseUrl}/publisher/upload?$q")
.post(
MultipartFormBody(
FormPart("bundle", bundleZipPath.toFile())
)
)
.withRequestTimeout(600.second)
http.run(reqTransform(req), Gigahorse.asString)
}
awaitWithMessage(res, "uploading...", log)
}
def queryString(kv: (String, String)*): String =
kv.map { case (k, v) =>
val encodedV = URLEncoder.encode(v, "UTF-8")
s"$k=$encodedV"
}.mkString("&")
def waitForDeploy(
deploymentId: String,
deploymentName: String,
publishingType: PublishingType,
attempt: Int,
log: Logger,
): Unit = {
val status = deploymentStatus(deploymentId)
log.info(s"deployment $deploymentName ${status.deploymentState} ${attempt}/n")
val sleepSec =
if (attempt <= 3) List(5, 5, 10, 15)(attempt)
else 30
status.deploymentState match {
case DeploymentState.FAILED => sys.error(s"deployment $deploymentId failed")
case DeploymentState.PENDING | DeploymentState.PUBLISHING | DeploymentState.VALIDATING =>
Thread.sleep(sleepSec * 1000L)
waitForDeploy(deploymentId, deploymentName, publishingType, attempt + 1, log)
case DeploymentState.PUBLISHED if publishingType == PublishingType.Automatic => ()
case DeploymentState.VALIDATED if publishingType == PublishingType.UserManaged => ()
case DeploymentState.VALIDATED =>
Thread.sleep(sleepSec * 1000L)
waitForDeploy(deploymentId, deploymentName, publishingType, attempt + 1, log)
case _ =>
Thread.sleep(sleepSec * 1000L)
waitForDeploy(deploymentId, deploymentName, publishingType, attempt + 1, log)
}
}
def deploymentStatus(deploymentId: String): PublisherStatus = {
val res = retryF(maxAttempt = 5) { (attempt: Int) =>
deploymentStatusF(deploymentId)
}
Await.result(res, 600.seconds)
}
/**
* https://central.sonatype.org/publish/publish-portal-api/#verify-status-of-the-deployment
*/
def deploymentStatusF(deploymentId: String): Future[PublisherStatus] = {
val req = Gigahorse
.url(s"${baseUrl}/publisher/status")
.addQueryString("id" -> deploymentId)
.post("", StandardCharsets.UTF_8)
http.run(reqTransform(req), SonaClient.asPublisherStatus)
}
/**
* Retry future function on any error.
*/
@nowarn
def retryF[A1](maxAttempt: Int)(f: Int => Future[A1]): Future[A1] = {
import scala.concurrent.ExecutionContext.Implicits.*
def impl(retry: Int): Future[A1] = {
val res = f(retry + 1)
res.recoverWith {
case _ if retry < maxAttempt =>
Thread.sleep(5000)
impl(retry + 1)
}
}
impl(0)
}
def awaitWithMessage[A1](f: Future[A1], msg: String, log: Logger): A1 = {
import scala.concurrent.ExecutionContext.Implicits.*
def loop(attempt: Int): Unit =
if (!f.isCompleted) {
if (attempt > 0) {
log.info(msg)
}
Future {
blocking {
Thread.sleep(30.second.toMillis)
}
}.foreach(_ => loop(attempt + 1))
} else ()
loop(0)
Await.result(f, 600.seconds)
}
def close(): Unit = http.close()
}
object Sona {
def host: String = SonaClient.host
def oauthClient(userName: String, userToken: String): Sona =
new Sona(SonaClient.oauthClient(userName, userToken))
}
object SonaClient {
import sbt.internal.sona.codec.JsonProtocol.*
val host: String = "central.sonatype.com"
val baseUrl: String = s"https://$host/api/v1"
val asJson: FullResponse => JValue = (r: FullResponse) =>
Parser.parseFromByteBuffer(r.bodyAsByteBuffer).get
def as[A1: JsonFormat]: FullResponse => A1 = asJson.andThen(Converter.fromJsonUnsafe[A1])
val asPublisherStatus: FullResponse => PublisherStatus = as[PublisherStatus]
def oauthClient(userName: String, userToken: String): SonaClient =
new SonaClient(OAuthClient(userName, userToken))
}
private case class OAuthClient(userName: String, userToken: String)
extends Function1[Request, Request] {
val base64Credentials =
Base64.getEncoder.encodeToString(s"${userName}:${userToken}".getBytes(StandardCharsets.UTF_8))
def apply(request: Request): Request =
request.addHeaders("Authorization" -> s"Bearer $base64Credentials")
override def toString: String = "OAuthClient(****)"
}
sealed trait PublishingType
object PublishingType {
case object Automatic extends PublishingType
case object UserManaged extends PublishingType
}

View File

@ -10,7 +10,7 @@ package sbt
import java.io.{ File, PrintWriter }
import java.nio.file.{ Files, Path as NioPath }
import java.util.Optional
import java.util.{ Optional, UUID }
import java.util.concurrent.TimeUnit
import lmcoursier.CoursierDependencyResolution
import lmcoursier.definitions.{ Configuration as CConfiguration }
@ -48,6 +48,7 @@ import sbt.internal.server.{
ServerHandler,
VirtualTerminal
}
import sbt.internal.sona.Sona
import sbt.internal.testing.TestLogger
import sbt.internal.util.Attributed.data
import sbt.internal.util.Types.*
@ -292,6 +293,16 @@ object Defaults extends BuildCommon {
ScalaArtifacts.Artifacts.map(a => InclExclRule(scalaOrganization.value, a)).toSet
),
csrCacheDirectory := LMCoursier.defaultCacheLocation,
stagingDirectory := (ThisBuild / baseDirectory).value / "target" / "sona-staging",
localStaging := Some(Resolver.file("local-staging", stagingDirectory.value)),
sonaBundle := Publishing
.makeBundle(
stagingDirectory.value.toPath(),
((ThisBuild / baseDirectory).value / "target" / "sona-bundle" / "bundle.zip").toPath()
)
.toFile(),
sonaBundle / aggregate :== false,
commands ++= Seq(Publishing.sonaRelease, Publishing.sonaUpload),
)
/** Core non-plugin settings for sbt builds. These *must* be on every build or the sbt engine will fail to run at all. */
@ -2810,7 +2821,21 @@ object Classpaths {
makeIvyXml := deliverTask(makeIvyXmlConfiguration).value,
publish := publishOrSkip(publishConfiguration, publish / skip).value,
publishLocal := publishOrSkip(publishLocalConfiguration, publishLocal / skip).value,
publishM2 := publishOrSkip(publishM2Configuration, publishM2 / skip).value
publishM2 := publishOrSkip(publishM2Configuration, publishM2 / skip).value,
credentials ++= {
val alreadyContainsCentralCredentials: Boolean = credentials.value.exists {
case d: DirectCredentials => d.host == Sona.host
case _ => false
}
if (!alreadyContainsCentralCredentials) SysProp.sonatypeCredentalsEnv.toSeq
else Nil
},
sonaDeploymentName := {
val o = organization.value
val v = version.value
val uuid = UUID.randomUUID().toString().take(8)
s"$o:$v:$uuid"
},
)
def baseGlobalDefaults =

View File

@ -600,6 +600,10 @@ object Keys {
val forceUpdatePeriod = settingKey[Option[FiniteDuration]]("Duration after which to force a full update to occur").withRank(CSetting)
val versionScheme = settingKey[Option[String]]("""Version scheme used for the subproject: Supported values are Some("early-semver"), Some("pvp"), and Some("semver-spec")""").withRank(BSetting)
val libraryDependencySchemes = settingKey[Seq[ModuleID]]("""Version scheme to use for specific modules set as "org" %% "name" % "<scheme>": Supported values are "early-semver", "pvp", "semver-spec", "always", and "strict".""").withRank(BSetting)
val stagingDirectory = settingKey[File]("Local staging directory for Sonatype publishing").withRank(CSetting)
val sonaBundle = taskKey[File]("Local bundle for Sonatype publishing").withRank(DTask)
val localStaging = settingKey[Option[Resolver]]("Local staging resolver for Sonatype publishing").withRank(CSetting)
val sonaDeploymentName = settingKey[String]("The name used for deployment").withRank(DSetting)
val classifiersModule = taskKey[GetClassifiersModule]("classifiers-module").withRank(CTask)
val compatibilityWarningOptions = settingKey[CompatibilityWarningOptions]("Configures warnings around Maven incompatibility.").withRank(CSetting)

View File

@ -11,6 +11,13 @@ package sbt.internal
private[sbt] object Banner {
def apply(version: String): Option[String] =
version match {
case v if v.startsWith("1.11.0") =>
Some(s"""
|Here are some highlights of sbt 1.11.0:
| - The Central Repository publishing
|See https://eed3si9n.com/sbt-1.11.0 for full release notes.
|Hide the banner for this release by running `skipBanner`.
|""".stripMargin.linesIterator.mkString("\n"))
case v if v.startsWith("1.10.0") =>
Some(s"""
|Here are some highlights of sbt 1.10.0:

View File

@ -241,6 +241,17 @@ object SysProp {
lazy val sbtCredentialsEnv: Option[Credentials] =
sys.env.get("SBT_CREDENTIALS").map(raw => new FileCredentials(new File(raw)))
def sonatypeCredentalsEnv: Option[Credentials] =
for {
username <- sys.env.get("SONATYPE_USERNAME")
password <- sys.env.get("SONATYPE_PASSWORD")
} yield Credentials(
"Sonatype Nexus Repository Manager",
sona.Sona.host,
username,
password
)
private[sbt] def setSwovalTempDir(): Unit = {
val _ = getOrUpdateSwovalTmpDir(
runtimeDirectory.resolve("swoval").toString

View File

@ -0,0 +1,69 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
package internal
package librarymanagement
import java.nio.file.Path
import sbt.internal.util.MessageOnlyException
import sbt.io.IO
import sbt.io.Path.contentOf
import sbt.librarymanagement.ivy.Credentials
import sona.{ Sona, PublishingType }
object Publishing {
val sonaRelease: Command =
Command.command("sonaRelease")(sonatypeReleaseAction(PublishingType.Automatic))
val sonaUpload: Command =
Command.command("sonaUpload")(sonatypeReleaseAction(PublishingType.UserManaged))
def makeBundle(stagingDir: Path, bundlePath: Path): Path = {
if (bundlePath.toFile().exists()) {
IO.delete(bundlePath.toFile())
}
IO.zip(
sources = contentOf(stagingDir.toFile()),
outputZip = bundlePath.toFile(),
time = Some(0L),
)
bundlePath
}
private def sonatypeReleaseAction(pt: PublishingType)(s0: State): State = {
import sbt.ProjectExtra.extract
val extracted = Project.extract(s0)
val log = extracted.get(Keys.sLog)
val dn = extracted.get(Keys.sonaDeploymentName)
val v = extracted.get(Keys.version)
if (v.endsWith("-SNAPSHOT")) {
log.error("""SNAPSHOTs are not supported on the Central Portal;
configure ThisBuild / publishTo to publish directly to the central-snapshots.
see https://www.scala-sbt.org/1.x/docs/Using-Sonatype.html for details.""")
s0.fail
} else {
val (s1, bundle) = extracted.runTask(Keys.sonaBundle, s0)
val (s2, creds) = extracted.runTask(Keys.credentials, s1)
val client = fromCreds(creds)
try {
client.uploadBundle(bundle.toPath(), dn, pt, log)
s2
} finally {
client.close()
}
}
}
private def fromCreds(creds: Seq[Credentials]): Sona = {
val cred = Credentials
.forHost(creds, Sona.host)
.getOrElse(throw new MessageOnlyException(s"no credentials are found for ${Sona.host}"))
Sona.oauthClient(cred.userName, cred.passwd)
}
}

View File

@ -115,7 +115,7 @@ object Dependencies {
// lm dependencies
val jsch = "com.github.mwiede" % "jsch" % "0.2.17" intransitive ()
val gigahorseApacheHttp = "com.eed3si9n" %% "gigahorse-apache-http" % "0.7.0"
val gigahorseApacheHttp = "com.eed3si9n" %% "gigahorse-apache-http" % "0.9.3"
// lm-coursier dependencies
val dataclassScalafixVersion = "0.1.0"

View File

@ -1 +1 @@
sbt.version=1.10.7
sbt.version=1.11.0

2
sbt
View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash
set +e
declare builtin_sbt_version="1.10.11"
declare builtin_sbt_version="1.11.0"
declare -a residual_args
declare -a java_args
declare -a scalac_args

View File

@ -18,12 +18,14 @@ lazy val root = (project in file("."))
// Calling "distinct" as there are different entries for sources and javadoc classifiers with same module
val moduleIds = moduleReports.map(_.module).distinct
val moduleIdsShort = moduleIds.map(m => s"${m.organization}:${m.name}")
val moduleIdsShort = moduleIds
.filter(m => m.name != "launcher-interface") // I get different result locally
.map(m => s"${m.organization}:${m.name}")
val expectedModuleIds = Seq(
"com.eed3si9n:gigahorse-apache-http_3",
"com.eed3si9n:gigahorse-core_3",
"com.eed3si9n:shaded-apache-httpasyncclient",
"com.eed3si9n:shaded-apache-httpclient5",
"com.eed3si9n:shaded-jawn-parser_3",
"com.eed3si9n:shaded-scalajson_3",
"com.eed3si9n:sjson-new-core_3",
@ -66,7 +68,6 @@ lazy val root = (project in file("."))
"org.scala-sbt.jline:jline",
"org.scala-sbt:compiler-interface",
"org.scala-sbt:io_3",
"org.scala-sbt:launcher-interface",
"org.scala-sbt:sbinary_3",
"org.scala-sbt:template-resolver",
"org.scala-sbt:test-interface",