mirror of https://github.com/sbt/sbt.git
[2.x] fix: Fixes publishing with sbt-pgp (#9533)
**Problem** Ivyless publishing doesn't work with sbt-pgp. **Solution** This refactors the ivyless publishing code and revives publishOrSkip, which is called by the plugin. Instead of overriding publish task, implement proper PublisherInterface.
This commit is contained in:
parent
b338dfcec2
commit
90582a3438
|
|
@ -3096,9 +3096,9 @@ object Classpaths {
|
|||
.map(m => d.withRevision(m.module.revision))
|
||||
}.distinct
|
||||
}.value,
|
||||
publish := LibraryManagement.ivylessPublishTask.tag(Tags.Publish, Tags.Network).value,
|
||||
publishLocal := LibraryManagement.ivylessPublishLocalTask.value,
|
||||
publishM2 := LibraryManagement.ivylessPublishM2Task.tag(Tags.Publish, Tags.Network).value,
|
||||
publish := publishOrSkip(publishConfiguration, publish / skip).value,
|
||||
publishLocal := publishOrSkip(publishLocalConfiguration, publishLocal / skip).value,
|
||||
publishM2 := publishOrSkip(publishM2Configuration, publishM2 / skip).value,
|
||||
credentials ++= Def.uncached {
|
||||
val alreadyContainsCentralCredentials: Boolean = credentials.value.exists {
|
||||
case d: Credentials.DirectCredentials => d.host == Sona.host
|
||||
|
|
@ -3509,9 +3509,24 @@ object Classpaths {
|
|||
},
|
||||
ivySbt := Def.uncached((): Any),
|
||||
ivyModule := Def.uncached((): Any),
|
||||
publisher := Def.uncached(
|
||||
Classpaths.defaultPublisher(dependencyResolution.value, fullResolvers.value.toVector)
|
||||
),
|
||||
publisher := Def.uncached {
|
||||
val ivyHome = ivyPaths.value.ivyHome.map(new File(_)).getOrElse {
|
||||
new File(sys.props("user.home")) / ".ivy2"
|
||||
}
|
||||
val localResolver = Resolver.file("local", ivyHome / "local")(using Resolver.ivyStylePatterns)
|
||||
// publishLocal/publishM2/publish target these by name (see publishConfig's resolverName
|
||||
// default and publishM2Configuration below).
|
||||
val knownResolvers = localResolver +: otherResolvers.value
|
||||
Publisher(
|
||||
GenericPublisher(
|
||||
dependencyResolution.value,
|
||||
fullResolvers.value.toVector,
|
||||
csrProject.value.withPublications(csrPublications.value),
|
||||
allCredentials.value,
|
||||
knownResolvers
|
||||
)
|
||||
)
|
||||
},
|
||||
allCredentials := Def.uncached(LMCoursier.allCredentialsTask.value),
|
||||
transitiveUpdate := Def.uncached(transitiveUpdateTask.value),
|
||||
updateCacheName := {
|
||||
|
|
@ -3934,15 +3949,17 @@ object Classpaths {
|
|||
): Initialize[Task[Unit]] =
|
||||
Def
|
||||
.taskIf {
|
||||
if (skip.value) {
|
||||
if skip.value then
|
||||
val log = streams.value.log
|
||||
val ref = thisProjectRef.value
|
||||
logSkipPublish(log, ref)
|
||||
} else {
|
||||
sys.error(
|
||||
"publishOrSkip requires the sbt-ivy plugin. Use publish/publishLocal for ivyless publishing."
|
||||
)
|
||||
}
|
||||
else
|
||||
val conf = config.value
|
||||
val log = streams.value.log
|
||||
val intf = publisher.value
|
||||
val module =
|
||||
intf.moduleDescriptor(moduleSettings.value.asInstanceOf[ModuleDescriptorConfiguration])
|
||||
intf.publish(module, conf, log)
|
||||
}
|
||||
.tag(Tags.Publish, Tags.Network)
|
||||
|
||||
|
|
@ -4395,53 +4412,6 @@ object Classpaths {
|
|||
val name = "inter-project"
|
||||
override def toString: String = name
|
||||
|
||||
/** Default publisher that delegates moduleDescriptor to Coursier and generates POM without Ivy. */
|
||||
private[sbt] def defaultPublisher(
|
||||
lm: DependencyResolution,
|
||||
resolvers: Vector[Resolver] = Vector.empty,
|
||||
): Publisher =
|
||||
Publisher(new PublisherInterface {
|
||||
def moduleDescriptor(moduleSetting: ModuleDescriptorConfiguration): ModuleDescriptor =
|
||||
lm.moduleDescriptor(moduleSetting)
|
||||
def publish(
|
||||
module: ModuleDescriptor,
|
||||
configuration: PublishConfiguration,
|
||||
log: Logger
|
||||
): Unit =
|
||||
sys.error("Ivy-based publish requires the sbt-ivy plugin or useIvy := true")
|
||||
def makePomFile(
|
||||
module: ModuleDescriptor,
|
||||
configuration: MakePomConfiguration,
|
||||
log: Logger
|
||||
): java.io.File =
|
||||
val file = configuration.file.getOrElse(sys.error("makePom file must be specified."))
|
||||
val ms = module.moduleSettings.asInstanceOf[ModuleDescriptorConfiguration]
|
||||
val mid = ms.module
|
||||
val info = configuration.moduleInfo.orElse(Option(ms.moduleInfo))
|
||||
val deps = module.directDependencies
|
||||
val extra = configuration.extra.getOrElse(scala.xml.NodeSeq.Empty)
|
||||
val confs = configuration.configurations
|
||||
val scalaInfo = ms.scalaModuleInfo
|
||||
val pomXml =
|
||||
sbt.internal.PomGenerator.makePom(
|
||||
mid,
|
||||
info,
|
||||
deps,
|
||||
confs,
|
||||
extra,
|
||||
scalaInfo,
|
||||
resolvers,
|
||||
configuration.filterRepositories,
|
||||
configuration.allRepositories,
|
||||
)
|
||||
val processed = configuration.process(pomXml)
|
||||
val printer = new scala.xml.PrettyPrinter(1000, 4)
|
||||
val formatted = scala.xml.XML.loadString(printer.format(processed))
|
||||
scala.xml.XML.save(file.getAbsolutePath, formatted, "UTF-8", xmlDecl = true)
|
||||
log.info("Wrote " + file.getAbsolutePath)
|
||||
file
|
||||
})
|
||||
|
||||
def makeProducts: Initialize[Task[Seq[File]]] = Def.task {
|
||||
val c = fileConverter.value
|
||||
val resourceDirs = resourceDirectories.value
|
||||
|
|
|
|||
|
|
@ -9,16 +9,12 @@
|
|||
package sbt
|
||||
package internal
|
||||
|
||||
import java.io.{ File, IOException }
|
||||
import java.net.{ URI, URL }
|
||||
import java.io.File
|
||||
import java.util.concurrent.Callable
|
||||
import java.util.regex.Matcher
|
||||
|
||||
import gigahorse.AuthScheme
|
||||
import gigahorse.support.apachehttp.Gigahorse
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
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 }
|
||||
|
|
@ -26,9 +22,6 @@ import sbt.io.IO
|
|||
import sbt.io.syntax.*
|
||||
import sbt.ProjectExtra.*
|
||||
import sjsonnew.JsonFormat
|
||||
import scala.concurrent.*
|
||||
import scala.concurrent.duration.*
|
||||
import lmcoursier.definitions.Project as CsrProject
|
||||
|
||||
private[sbt] object LibraryManagement {
|
||||
given linter: sbt.dsl.LinterLevel.Ignore.type = sbt.dsl.LinterLevel.Ignore
|
||||
|
|
@ -556,664 +549,4 @@ 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]
|
||||
*/
|
||||
def ivylessPublishLocal(
|
||||
project: CsrProject,
|
||||
artifacts: Vector[(Artifact, File)],
|
||||
checksumAlgorithms: Vector[String],
|
||||
localRepoBase: File,
|
||||
overwrite: Boolean,
|
||||
log: Logger
|
||||
): Unit =
|
||||
val org = project.module.organization.value
|
||||
val moduleName = project.module.name.value
|
||||
val version = project.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")
|
||||
|
||||
// Helper to map artifact type to folder name
|
||||
def typeToFolder(tpe: String): String = tpe match
|
||||
case "jar" => "jars"
|
||||
case "src" | "source" | "sources" => "srcs"
|
||||
case "doc" | "docs" | "javadoc" | "javadocs" => "docs"
|
||||
case "pom" => "poms"
|
||||
case "ivy" => "ivys"
|
||||
case other => other + "s"
|
||||
|
||||
// Helper to write checksums for a file using sbt.util.Digest
|
||||
def writeChecksums(file: File): Unit =
|
||||
checksumAlgorithms.foreach: algo =>
|
||||
val digestAlgo = algo.toLowerCase match
|
||||
case "md5" => sbt.util.Digest.Md5
|
||||
case "sha1" => sbt.util.Digest.Sha1
|
||||
case other =>
|
||||
throw new IllegalArgumentException(s"Unsupported checksum algorithm: $other")
|
||||
val digest = sbt.util.Digest(digestAlgo, file.toPath)
|
||||
val checksumFile = new File(file.getPath + "." + algo.toLowerCase)
|
||||
IO.write(checksumFile, digest.hashHexString)
|
||||
log.debug(s"Wrote checksum: $checksumFile")
|
||||
|
||||
// Write ivy.xml first (so ivys/ exists even if artifact copy fails)
|
||||
val ivysDir = moduleDir / "ivys"
|
||||
val ivyXmlFile = ivysDir / "ivy.xml"
|
||||
IO.createDirectory(ivysDir)
|
||||
val ivyXmlContent = lmcoursier.IvyXml(project, Nil, Nil)
|
||||
if !ivyXmlFile.exists || overwrite then
|
||||
IO.write(ivyXmlFile, ivyXmlContent)
|
||||
log.info(s"Published $ivyXmlFile")
|
||||
writeChecksums(ivyXmlFile)
|
||||
else log.warn(s"$ivyXmlFile already exists, skipping (overwrite=$overwrite)")
|
||||
|
||||
// Build a lookup from (type, classifier, ext) to cross-versioned publication name
|
||||
val pubNameLookup: Map[(String, String, String), String] =
|
||||
project.publications.map { (_, pub) =>
|
||||
(pub.`type`.value, pub.classifier.value, pub.ext.value) -> pub.name
|
||||
}.toMap
|
||||
|
||||
// Publish each artifact
|
||||
artifacts.foreach: (artifact, sourceFile) =>
|
||||
val folder = typeToFolder(artifact.`type`)
|
||||
val targetDir = moduleDir / folder
|
||||
|
||||
// Look up the cross-versioned artifact name from publications, fall back to module name
|
||||
val classifierStr = artifact.classifier.getOrElse("")
|
||||
val artName = pubNameLookup
|
||||
.getOrElse((artifact.`type`, classifierStr, artifact.extension), moduleName)
|
||||
val classifier = artifact.classifier.map("-" + _).getOrElse("")
|
||||
val fileName = s"$artName$classifier.${artifact.extension}"
|
||||
val targetFile = targetDir / fileName
|
||||
|
||||
if !targetFile.exists || overwrite then
|
||||
IO.createDirectory(targetDir)
|
||||
IO.copyFile(sourceFile, targetFile)
|
||||
log.info(s"Published $targetFile")
|
||||
writeChecksums(targetFile)
|
||||
else log.warn(s"$targetFile already exists, skipping (overwrite=$overwrite)")
|
||||
end ivylessPublishLocal
|
||||
|
||||
/**
|
||||
* Substitutes Ivy pattern placeholders for artifact URL.
|
||||
* Matches ivylessPublishLocal layout: [organisation]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext]
|
||||
*/
|
||||
private def substituteIvyArtifactPattern(
|
||||
pattern: String,
|
||||
project: CsrProject,
|
||||
org: String,
|
||||
moduleName: String,
|
||||
version: String,
|
||||
typeFolder: String,
|
||||
artifactName: String,
|
||||
classifier: String,
|
||||
ext: String
|
||||
): String = {
|
||||
var s = pattern
|
||||
s = s.replace("[organisation]", org)
|
||||
s = s.replace("[module]", moduleName)
|
||||
s = s.replace("[revision]", version)
|
||||
s = s.replace("[type]s", typeFolder)
|
||||
s = s.replace("[artifact]", artifactName)
|
||||
s = s.replace("[ext]", ext)
|
||||
if (classifier.nonEmpty) s = s.replace("(-[classifier])", s"-$classifier")
|
||||
else s = s.replace("(-[classifier])", "")
|
||||
// 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
|
||||
}
|
||||
|
||||
/**
|
||||
* Picks credentials for a URL. Matches host; when realm is given, prefers credential with matching realm (per Publishing docs).
|
||||
*/
|
||||
private def credentialFor(
|
||||
url: URL,
|
||||
credentials: Seq[Credentials.DirectCredentials],
|
||||
realm: Option[String] = None
|
||||
): Option[Credentials.DirectCredentials] =
|
||||
val byHost = credentials.filter(_.host == url.getHost)
|
||||
realm match
|
||||
case Some(r) => byHost.find(_.realm == r).orElse(byHost.headOption)
|
||||
case None => byHost.headOption
|
||||
|
||||
/**
|
||||
* HTTP PUT a file to a URL with optional Basic auth.
|
||||
* Uses Gigahorse (Apache HttpClient) per sbt tech stack.
|
||||
*/
|
||||
private def httpPut(
|
||||
url: URL,
|
||||
sourceFile: File,
|
||||
credentials: Option[Credentials.DirectCredentials],
|
||||
log: Logger
|
||||
): Unit =
|
||||
val baseReq = Gigahorse.url(url.toString).put(sourceFile)
|
||||
val req = credentials match
|
||||
case Some(dc) => baseReq.withAuth(dc.userName, dc.passwd, AuthScheme.Basic)
|
||||
case None => baseReq
|
||||
val f = sbt.librarymanagement.Http.http.processFull(req)
|
||||
val response = Await.result(f, 5.minutes)
|
||||
val body = response.bodyAsString
|
||||
if response.status < 200 || response.status >= 300 then
|
||||
throw new IOException(
|
||||
s"PUT $url failed: ${response.status} ${response.statusText}$body"
|
||||
)
|
||||
log.info(s"Published $url")
|
||||
|
||||
/**
|
||||
* Publishes artifacts to a remote Ivy repo (URLRepository) without using Apache Ivy.
|
||||
* Uses HTTP PUT; supports credentials. Produces the same layout as ivylessPublishLocal.
|
||||
*/
|
||||
def ivylessPublish(
|
||||
project: CsrProject,
|
||||
artifacts: Vector[(Artifact, File)],
|
||||
checksumAlgorithms: Vector[String],
|
||||
urlRepo: sbt.librarymanagement.URLRepository,
|
||||
credentials: Seq[Credentials],
|
||||
overwrite: Boolean,
|
||||
log: Logger
|
||||
): Unit = {
|
||||
val org = project.module.organization.value
|
||||
val moduleName = project.module.name.value
|
||||
val version = project.version
|
||||
val artifactPattern = urlRepo.patterns.artifactPatterns.headOption.getOrElse(
|
||||
sys.error("URLRepository has no artifact pattern")
|
||||
)
|
||||
val ivyPattern = urlRepo.patterns.ivyPatterns.headOption.getOrElse(
|
||||
sys.error("URLRepository has no ivy pattern")
|
||||
)
|
||||
val directCreds = credentials.collect { case d: Credentials.DirectCredentials => d }
|
||||
|
||||
def typeToFolder(tpe: String): String = tpe match
|
||||
case "jar" => "jars"
|
||||
case "src" | "source" | "sources" => "srcs"
|
||||
case "doc" | "docs" | "javadoc" | "javadocs" => "docs"
|
||||
case "pom" => "poms"
|
||||
case "ivy" => "ivys"
|
||||
case other => other + "s"
|
||||
|
||||
def writeChecksums(file: File): Vector[(File, String)] =
|
||||
checksumAlgorithms.map { algo =>
|
||||
val digestAlgo = algo.toLowerCase match
|
||||
case "md5" => sbt.util.Digest.Md5
|
||||
case "sha1" => sbt.util.Digest.Sha1
|
||||
case other =>
|
||||
throw new IllegalArgumentException(s"Unsupported checksum algorithm: $other")
|
||||
val digest = sbt.util.Digest(digestAlgo, file.toPath)
|
||||
val content = digest.hashHexString
|
||||
val suffix = "." + algo.toLowerCase
|
||||
val tmpFile = File.createTempFile("checksum", suffix)
|
||||
IO.write(tmpFile, content)
|
||||
(tmpFile, suffix)
|
||||
}
|
||||
|
||||
artifacts.foreach { case (artifact, sourceFile) =>
|
||||
val folder = typeToFolder(artifact.`type`)
|
||||
val classifier = artifact.classifier.map("-" + _).getOrElse("")
|
||||
val artifactName = moduleName
|
||||
val pathPattern = substituteIvyArtifactPattern(
|
||||
artifactPattern,
|
||||
project,
|
||||
org,
|
||||
moduleName,
|
||||
version,
|
||||
folder,
|
||||
artifactName,
|
||||
classifier,
|
||||
artifact.extension
|
||||
)
|
||||
val url = URI.create(pathPattern).toURL()
|
||||
httpPut(url, sourceFile, credentialFor(url, directCreds, None), log)
|
||||
val checksums = writeChecksums(sourceFile)
|
||||
checksums.foreach { case (cf, suffix) =>
|
||||
val checksumUrl = URI.create(pathPattern + suffix).toURL()
|
||||
try httpPut(checksumUrl, cf, credentialFor(checksumUrl, directCreds, None), log)
|
||||
finally cf.delete()
|
||||
}
|
||||
}
|
||||
|
||||
val ivyXmlContent = lmcoursier.IvyXml(project, Nil, Nil)
|
||||
val ivyPathPattern = substituteIvyArtifactPattern(
|
||||
ivyPattern,
|
||||
project,
|
||||
org,
|
||||
moduleName,
|
||||
version,
|
||||
"ivys",
|
||||
"ivy",
|
||||
"",
|
||||
"xml"
|
||||
)
|
||||
val ivyUrl = URI.create(ivyPathPattern).toURL()
|
||||
val ivyTmp = File.createTempFile("ivy", ".xml")
|
||||
try {
|
||||
IO.write(ivyTmp, ivyXmlContent)
|
||||
httpPut(ivyUrl, ivyTmp, credentialFor(ivyUrl, directCreds, None), log)
|
||||
val checksums = writeChecksums(ivyTmp)
|
||||
checksums.foreach { case (cf, suffix) =>
|
||||
val checksumUrl = URI.create(ivyPathPattern + suffix).toURL()
|
||||
try httpPut(checksumUrl, cf, credentialFor(checksumUrl, directCreds, None), log)
|
||||
finally cf.delete()
|
||||
}
|
||||
} finally ivyTmp.delete()
|
||||
}
|
||||
|
||||
/**
|
||||
* Maven layout path: groupId/artifactId/version/artifactId-version[-classifier].ext
|
||||
*/
|
||||
private def mavenLayoutPath(
|
||||
groupId: String,
|
||||
artifactId: String,
|
||||
version: String,
|
||||
artifact: Artifact
|
||||
): String =
|
||||
val groupPath = groupId.replace('.', '/')
|
||||
val classifierPart = artifact.classifier.map("-" + _).getOrElse("")
|
||||
val fileName = s"$artifactId-$version$classifierPart.${artifact.extension}"
|
||||
s"$groupPath/$artifactId/$version/$fileName"
|
||||
|
||||
private def writeChecksumsForFile(
|
||||
targetFile: File,
|
||||
algorithms: Vector[String],
|
||||
log: Logger
|
||||
): Unit =
|
||||
algorithms.foreach: algo =>
|
||||
val digestAlgo = algo.toLowerCase match
|
||||
case "md5" => sbt.util.Digest.Md5
|
||||
case "sha1" => sbt.util.Digest.Sha1
|
||||
case other =>
|
||||
throw new IllegalArgumentException(s"Unsupported checksum algorithm: $other")
|
||||
val digest = sbt.util.Digest(digestAlgo, targetFile.toPath)
|
||||
val checksumFile = new File(targetFile.getPath + "." + algo.toLowerCase)
|
||||
IO.write(checksumFile, digest.hashHexString)
|
||||
log.debug(s"Wrote checksum: $checksumFile")
|
||||
|
||||
/**
|
||||
* Publishes artifacts to a local Maven repo (Maven layout) without using Apache Ivy.
|
||||
* Layout: groupId/artifactId/version/artifactId-version[-classifier].ext
|
||||
*/
|
||||
def ivylessPublishMavenToFile(
|
||||
project: CsrProject,
|
||||
artifacts: Vector[(Artifact, File)],
|
||||
checksumAlgorithms: Vector[String],
|
||||
repoBase: File,
|
||||
overwrite: Boolean,
|
||||
log: Logger
|
||||
): Unit =
|
||||
if repoBase == null then throw new IllegalArgumentException("repoBase must not be null")
|
||||
val groupId = project.module.organization.value
|
||||
// Derive artifactId: for sbt 2 plugins, module.name has cross-version (e.g. sbt-example_sbt2_3).
|
||||
// For sbt 1 plugins, mavenArtifactsOfSbtPlugin cross-versions the POM artifact name (e.g. sbt-example_2.12_1.0).
|
||||
val baseModuleName = project.module.name.value
|
||||
val pomArtName = artifacts.collectFirst { case (a, _) if a.`type` == "pom" => a.name }
|
||||
val artifactId = pomArtName match
|
||||
case Some(name) if name.startsWith(baseModuleName) && name != baseModuleName => name
|
||||
case _ => baseModuleName
|
||||
val version = project.version
|
||||
val groupPath = groupId.replace('.', '/')
|
||||
val versionDir = new File(repoBase, s"$groupPath/$artifactId/$version")
|
||||
log.info(s"Publishing to Maven repo: $versionDir")
|
||||
|
||||
artifacts.foreach:
|
||||
case (artifact, sourceFile) =>
|
||||
val path = mavenLayoutPath(groupId, artifactId, version, artifact)
|
||||
val targetFile = new File(repoBase, path.replace('/', File.separatorChar))
|
||||
if !targetFile.exists || overwrite then
|
||||
targetFile.getParentFile.mkdirs()
|
||||
IO.copyFile(sourceFile, targetFile)
|
||||
log.info(s"Published $targetFile")
|
||||
writeChecksumsForFile(targetFile, checksumAlgorithms, log)
|
||||
else log.warn(s"$targetFile already exists, skipping (overwrite=$overwrite)")
|
||||
|
||||
if version.endsWith("-SNAPSHOT") then
|
||||
writeMavenMetadataLocal(versionDir, groupId, artifactId, version, log)
|
||||
|
||||
private def writeMavenMetadataLocal(
|
||||
versionDir: File,
|
||||
groupId: String,
|
||||
artifactId: String,
|
||||
version: String,
|
||||
log: Logger
|
||||
): Unit =
|
||||
val timestamp = new java.text.SimpleDateFormat("yyyyMMddHHmmss").format(new java.util.Date())
|
||||
val metadata =
|
||||
s"""|<?xml version="1.0" encoding="UTF-8"?>
|
||||
|<metadata modelVersion="1.1.0">
|
||||
| <groupId>$groupId</groupId>
|
||||
| <artifactId>$artifactId</artifactId>
|
||||
| <version>$version</version>
|
||||
| <versioning>
|
||||
| <snapshot>
|
||||
| <localCopy>true</localCopy>
|
||||
| </snapshot>
|
||||
| <lastUpdated>$timestamp</lastUpdated>
|
||||
| </versioning>
|
||||
|</metadata>
|
||||
|""".stripMargin
|
||||
val metadataFile = new File(versionDir, "maven-metadata-local.xml")
|
||||
IO.write(metadataFile, metadata)
|
||||
log.info(s"Published $metadataFile")
|
||||
|
||||
/**
|
||||
* Publishes artifacts to a remote Maven repo (HTTP) without using Apache Ivy.
|
||||
* Same layout as ivylessPublishMavenToFile; uses HTTP PUT with optional Basic auth.
|
||||
*/
|
||||
def ivylessPublishMavenToUrl(
|
||||
project: CsrProject,
|
||||
artifacts: Vector[(Artifact, File)],
|
||||
checksumAlgorithms: Vector[String],
|
||||
baseUrl: String,
|
||||
credentials: Seq[Credentials],
|
||||
overwrite: Boolean,
|
||||
log: Logger
|
||||
): Unit =
|
||||
if baseUrl == null || baseUrl.trim.isEmpty then
|
||||
throw new IllegalArgumentException("baseUrl must not be null or empty")
|
||||
val groupId = project.module.organization.value
|
||||
// Derive artifactId: for sbt 2 plugins, module.name has cross-version (e.g. sbt-example_sbt2_3).
|
||||
// For sbt 1 plugins, mavenArtifactsOfSbtPlugin cross-versions the POM artifact name (e.g. sbt-example_2.12_1.0).
|
||||
val baseModuleName = project.module.name.value
|
||||
val pomArtName = artifacts.collectFirst { case (a, _) if a.`type` == "pom" => a.name }
|
||||
val artifactId = pomArtName match
|
||||
case Some(name) if name.startsWith(baseModuleName) && name != baseModuleName => name
|
||||
case _ => baseModuleName
|
||||
val version = project.version
|
||||
val directCreds = credentials.collect:
|
||||
case d: Credentials.DirectCredentials => d
|
||||
|
||||
def writeChecksums(file: File): Vector[(File, String)] =
|
||||
checksumAlgorithms
|
||||
.map: algo =>
|
||||
val digestAlgo = algo.toLowerCase match
|
||||
case "md5" => sbt.util.Digest.Md5
|
||||
case "sha1" => sbt.util.Digest.Sha1
|
||||
case other =>
|
||||
throw new IllegalArgumentException(s"Unsupported checksum algorithm: $other")
|
||||
val digest = sbt.util.Digest(digestAlgo, file.toPath)
|
||||
val content = digest.hashHexString
|
||||
val suffix = "." + algo.toLowerCase
|
||||
val tmpFile = File.createTempFile("checksum", suffix)
|
||||
IO.write(tmpFile, content)
|
||||
(tmpFile, suffix)
|
||||
|
||||
val base = baseUrl.stripSuffix("/") + "/"
|
||||
artifacts.foreach:
|
||||
case (artifact, sourceFile) =>
|
||||
val path = mavenLayoutPath(groupId, artifactId, version, artifact)
|
||||
val url = URI.create(base + path).toURL()
|
||||
try
|
||||
httpPut(url, sourceFile, credentialFor(url, directCreds, None), log)
|
||||
val checksums = writeChecksums(sourceFile)
|
||||
checksums.foreach:
|
||||
case (cf, suffix) =>
|
||||
val checksumUrl = URI.create(base + path + suffix).toURL()
|
||||
try httpPut(checksumUrl, cf, credentialFor(checksumUrl, directCreds, None), log)
|
||||
finally cf.delete()
|
||||
catch
|
||||
case e: IOException =>
|
||||
throw new IOException(s"Failed to publish $path: ${e.getMessage}", e)
|
||||
|
||||
/**
|
||||
* Publishes artifacts to a local file repo (FileRepository) without using Apache Ivy.
|
||||
* Same layout as ivylessPublishLocal; used for testing without an HTTP server.
|
||||
*/
|
||||
def ivylessPublishToFile(
|
||||
project: CsrProject,
|
||||
artifacts: Vector[(Artifact, File)],
|
||||
checksumAlgorithms: Vector[String],
|
||||
fileRepo: sbt.librarymanagement.FileRepository,
|
||||
overwrite: Boolean,
|
||||
log: Logger
|
||||
): Unit = {
|
||||
val pattern = fileRepo.patterns.artifactPatterns.headOption.getOrElse(
|
||||
sys.error("FileRepository has no artifact pattern")
|
||||
)
|
||||
val baseStr =
|
||||
if (pattern.contains("[organisation]"))
|
||||
pattern.substring(0, pattern.indexOf("[organisation]"))
|
||||
else pattern
|
||||
val normalized = baseStr.replace('\\', '/').stripSuffix("/")
|
||||
val localRepoBase =
|
||||
if (normalized.startsWith("file:")) new File(new java.net.URI(normalized))
|
||||
else new File(normalized)
|
||||
val repoDir = localRepoBase.getAbsoluteFile
|
||||
val isMavenLayout = fileRepo.patterns.isMavenCompatible
|
||||
if isMavenLayout then
|
||||
log.info(s"Ivyless publish (Maven layout) to file repo: $repoDir")
|
||||
ivylessPublishMavenToFile(project, artifacts, checksumAlgorithms, repoDir, overwrite, log)
|
||||
else
|
||||
log.info(s"Ivyless publish (Ivy layout) to file repo: $repoDir")
|
||||
ivylessPublishLocal(project, artifacts, checksumAlgorithms, repoDir, overwrite, log)
|
||||
}
|
||||
|
||||
/**
|
||||
* Task initializer for ivyless publish (remote Ivy repo or file repo).
|
||||
* When useIvy is false and publishTo is URLRepository or FileRepository, uses ivyless publish; otherwise uses Ivy.
|
||||
*/
|
||||
def ivylessPublishTask: Def.Initialize[Task[Unit]] =
|
||||
import Keys.*
|
||||
Def.ifS(Def.task { (publish / skip).value })(
|
||||
Def.task {
|
||||
val log = streams.value.log
|
||||
val ref = thisProjectRef.value
|
||||
log.debug(s"Skipping publish for ${Reference.display(ref)}")
|
||||
}
|
||||
)(
|
||||
Def.ifS(Def.task { useIvy.value })(
|
||||
Def.task {
|
||||
val log = streams.value.log
|
||||
val conf = publishConfiguration.value
|
||||
val module = ivyModule.value.asInstanceOf[ModuleDescriptor]
|
||||
val publisherInterface = publisher.value
|
||||
publisherInterface.publish(module, conf, log)
|
||||
}
|
||||
)(
|
||||
Def.task {
|
||||
val log = streams.value.log
|
||||
val resolver = sbt.Classpaths.getPublishTo(publishTo.value)
|
||||
val project = csrProject.value.withPublications(csrPublications.value)
|
||||
val config = publishConfiguration.value
|
||||
val artifacts = config.artifacts
|
||||
resolver match {
|
||||
case urlRepo: sbt.librarymanagement.URLRepository =>
|
||||
val creds = allCredentials.value
|
||||
ivylessPublish(
|
||||
project,
|
||||
artifacts,
|
||||
config.checksums,
|
||||
urlRepo,
|
||||
creds,
|
||||
config.overwrite,
|
||||
log
|
||||
)
|
||||
case fileRepo: sbt.librarymanagement.FileRepository =>
|
||||
ivylessPublishToFile(
|
||||
project,
|
||||
artifacts,
|
||||
config.checksums,
|
||||
fileRepo,
|
||||
config.overwrite,
|
||||
log
|
||||
)
|
||||
case pbr: sbt.librarymanagement.PatternsBasedRepository
|
||||
if pbr.patterns.artifactPatterns.headOption.exists { pat =>
|
||||
pat.contains("[organisation]") && !pat.trim.startsWith("http")
|
||||
} =>
|
||||
// File repo detected by pattern (e.g. scripted classloader makes type match fail)
|
||||
val pat = pbr.patterns.artifactPatterns.head
|
||||
val baseStr =
|
||||
pat.substring(0, pat.indexOf("[organisation]")).replace('\\', '/').stripSuffix("/")
|
||||
val repoDir =
|
||||
(if (baseStr.startsWith("file:")) new File(new java.net.URI(baseStr))
|
||||
else new File(baseStr)).getAbsoluteFile
|
||||
if pbr.patterns.isMavenCompatible then
|
||||
log.info(s"Ivyless publish (Maven layout) to file repo: $repoDir")
|
||||
ivylessPublishMavenToFile(
|
||||
project,
|
||||
artifacts,
|
||||
config.checksums,
|
||||
repoDir,
|
||||
config.overwrite,
|
||||
log
|
||||
)
|
||||
else
|
||||
log.info(s"Ivyless publish (Ivy layout) to file repo: $repoDir")
|
||||
ivylessPublishLocal(
|
||||
project,
|
||||
artifacts,
|
||||
config.checksums,
|
||||
repoDir,
|
||||
config.overwrite,
|
||||
log
|
||||
)
|
||||
case mavenCache: sbt.librarymanagement.MavenCache =>
|
||||
ivylessPublishMavenToFile(
|
||||
project,
|
||||
artifacts,
|
||||
config.checksums,
|
||||
mavenCache.rootFile,
|
||||
config.overwrite,
|
||||
log
|
||||
)
|
||||
case mavenRepo: sbt.librarymanagement.MavenRepo =>
|
||||
val root = mavenRepo.root.stripSuffix("/")
|
||||
if root.startsWith("http://") || root.startsWith("https://") then
|
||||
val creds = allCredentials.value
|
||||
ivylessPublishMavenToUrl(
|
||||
project,
|
||||
artifacts,
|
||||
config.checksums,
|
||||
root,
|
||||
creds,
|
||||
config.overwrite,
|
||||
log
|
||||
)
|
||||
else if root.startsWith("file:") then
|
||||
val repoBase = new File(URI.create(root))
|
||||
ivylessPublishMavenToFile(
|
||||
project,
|
||||
artifacts,
|
||||
config.checksums,
|
||||
repoBase,
|
||||
config.overwrite,
|
||||
log
|
||||
)
|
||||
else
|
||||
sys.error(
|
||||
s"Ivyless Maven publish: unsupported root '$root'. Set useIvy := true or use a supported repository (http/https/file)."
|
||||
)
|
||||
case other =>
|
||||
sys.error(
|
||||
s"Ivyless publish does not support ${other.getClass.getName}. Set useIvy := true or use URLRepository, FileRepository, or MavenRepository."
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* Task initializer for ivyless publishLocal.
|
||||
* Uses Def.ifS for proper selective functor behavior.
|
||||
*/
|
||||
def ivylessPublishLocalTask: Def.Initialize[Task[Unit]] =
|
||||
import Keys.*
|
||||
Def.ifS(Def.task { (publishLocal / skip).value })(
|
||||
// skip = true
|
||||
Def.task {
|
||||
val log = streams.value.log
|
||||
val ref = thisProjectRef.value
|
||||
log.debug(s"Skipping publishLocal for ${Reference.display(ref)}")
|
||||
}
|
||||
)(
|
||||
// skip = false
|
||||
Def.ifS(Def.task { useIvy.value })(
|
||||
// useIvy = true: use Ivy-based publisher
|
||||
Def.task {
|
||||
val log = streams.value.log
|
||||
val conf = publishLocalConfiguration.value
|
||||
val module = ivyModule.value.asInstanceOf[ModuleDescriptor]
|
||||
val publisherInterface = publisher.value
|
||||
publisherInterface.publish(module, conf, log)
|
||||
}
|
||||
)(
|
||||
// useIvy = false: use ivyless publisher
|
||||
Def.task {
|
||||
val log = streams.value.log
|
||||
val project = csrProject.value.withPublications(csrPublications.value)
|
||||
val config = publishLocalConfiguration.value
|
||||
val artifacts = config.artifacts
|
||||
val checksumAlgos = config.checksums
|
||||
val ivyHome = ivyPaths.value.ivyHome.map(new File(_)).getOrElse {
|
||||
val userHome = new File(System.getProperty("user.home"))
|
||||
userHome / ".ivy2"
|
||||
}
|
||||
val localRepoBase = ivyHome / "local"
|
||||
val overwriteFlag = config.overwrite
|
||||
ivylessPublishLocal(project, artifacts, checksumAlgos, localRepoBase, overwriteFlag, log)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* Task initializer for ivyless publishM2 (publish to local Maven ~/.m2 repository).
|
||||
* Uses Def.ifS for proper selective functor behavior.
|
||||
*/
|
||||
def ivylessPublishM2Task: Def.Initialize[Task[Unit]] =
|
||||
import Keys.*
|
||||
Def.ifS(Def.task { (publishM2 / skip).value })(
|
||||
// skip = true
|
||||
Def.task {
|
||||
val log = streams.value.log
|
||||
val ref = thisProjectRef.value
|
||||
log.debug(s"Skipping publishM2 for ${Reference.display(ref)}")
|
||||
}
|
||||
)(
|
||||
// skip = false
|
||||
Def.ifS(Def.task { useIvy.value })(
|
||||
// useIvy = true: use Ivy-based publisher
|
||||
Def.task {
|
||||
val log = streams.value.log
|
||||
val conf = publishM2Configuration.value
|
||||
val module = ivyModule.value.asInstanceOf[ModuleDescriptor]
|
||||
val publisherInterface = publisher.value
|
||||
publisherInterface.publish(module, conf, log)
|
||||
}
|
||||
)(
|
||||
// useIvy = false: use ivyless publisher to Maven local
|
||||
Def.task {
|
||||
val log = streams.value.log
|
||||
val project = csrProject.value.withPublications(csrPublications.value)
|
||||
val config = publishM2Configuration.value
|
||||
val artifacts = config.artifacts
|
||||
val m2Repo = Resolver.publishMavenLocal
|
||||
ivylessPublishMavenToFile(
|
||||
project,
|
||||
artifacts,
|
||||
config.checksums,
|
||||
m2Repo.rootFile,
|
||||
config.overwrite,
|
||||
log
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,601 @@
|
|||
/*
|
||||
* 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.io.{ File, IOException }
|
||||
import java.net.{ URI, URL }
|
||||
import java.util.regex.Matcher
|
||||
|
||||
import gigahorse.AuthScheme
|
||||
import gigahorse.support.apachehttp.Gigahorse
|
||||
import sbt.internal.librarymanagement.mavenint.PomExtraDependencyAttributes
|
||||
import sbt.librarymanagement.*
|
||||
import sbt.util.Logger
|
||||
import sbt.io.IO
|
||||
import sbt.io.syntax.*
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.duration.*
|
||||
import lmcoursier.definitions.Project as CsrProject
|
||||
|
||||
/**
|
||||
* Publishes artifacts without Apache Ivy.
|
||||
*/
|
||||
class GenericPublisher private[sbt] (
|
||||
dependencyResolution: DependencyResolution,
|
||||
pomRepositories: Vector[Resolver],
|
||||
project: CsrProject,
|
||||
credentials: Seq[Credentials],
|
||||
resolvers: Seq[Resolver]
|
||||
) extends PublisherInterface:
|
||||
|
||||
// Extension used for PGP signature files; checksums are not generated for these.
|
||||
private val signatureExt = ".asc"
|
||||
|
||||
override def moduleDescriptor(moduleSetting: ModuleDescriptorConfiguration): ModuleDescriptor =
|
||||
dependencyResolution.moduleDescriptor(moduleSetting)
|
||||
|
||||
override def makePomFile(
|
||||
module: ModuleDescriptor,
|
||||
configuration: MakePomConfiguration,
|
||||
log: Logger
|
||||
): File =
|
||||
val file = configuration.file.getOrElse(sys.error("makePom file must be specified."))
|
||||
val ms = module.moduleSettings.asInstanceOf[ModuleDescriptorConfiguration]
|
||||
val mid = ms.module
|
||||
val info = configuration.moduleInfo.orElse(Option(ms.moduleInfo))
|
||||
val deps = module.directDependencies
|
||||
val extra = configuration.extra.getOrElse(scala.xml.NodeSeq.Empty)
|
||||
val confs = configuration.configurations
|
||||
val scalaInfo = ms.scalaModuleInfo
|
||||
val pomXml =
|
||||
sbt.internal.PomGenerator.makePom(
|
||||
mid,
|
||||
info,
|
||||
deps,
|
||||
confs,
|
||||
extra,
|
||||
scalaInfo,
|
||||
pomRepositories,
|
||||
configuration.filterRepositories,
|
||||
configuration.allRepositories,
|
||||
)
|
||||
val processed = configuration.process(pomXml)
|
||||
val printer = new scala.xml.PrettyPrinter(1000, 4)
|
||||
val formatted = scala.xml.XML.loadString(printer.format(processed))
|
||||
scala.xml.XML.save(file.getAbsolutePath, formatted, "UTF-8", xmlDecl = true)
|
||||
log.info("Wrote " + file.getAbsolutePath)
|
||||
file
|
||||
|
||||
override def publish(
|
||||
module: ModuleDescriptor,
|
||||
configuration: PublishConfiguration,
|
||||
log: Logger
|
||||
): Unit =
|
||||
val name = configuration.resolverName.getOrElse(
|
||||
sys.error("GenericPublisher.publish requires PublishConfiguration.resolverName to be set")
|
||||
)
|
||||
val target = resolvers.filter(_.name == name) match
|
||||
case Seq(r) => r
|
||||
case Seq() => sys.error(s"no resolver named '$name' is configured")
|
||||
case _ =>
|
||||
sys.error(
|
||||
s"multiple resolvers are named '$name'; " +
|
||||
s"'local' and '${Resolver.publishMavenLocal.name}' are reserved for " +
|
||||
"publishLocal and publishM2 respectively"
|
||||
)
|
||||
val artifacts = configuration.artifacts
|
||||
target match
|
||||
case urlRepo: URLRepository =>
|
||||
ivylessPublish(artifacts, configuration.checksums, urlRepo, configuration.overwrite, log)
|
||||
case fileRepo: FileRepository =>
|
||||
ivylessPublishToFile(
|
||||
artifacts,
|
||||
configuration.checksums,
|
||||
fileRepo,
|
||||
configuration.overwrite,
|
||||
log
|
||||
)
|
||||
case pbr: PatternsBasedRepository if pbr.patterns.artifactPatterns.headOption.exists { pat =>
|
||||
pat.contains("[organisation]") && !pat.trim.startsWith("http")
|
||||
} =>
|
||||
// File repo detected by pattern (e.g. scripted classloader makes type match fail)
|
||||
val pat = pbr.patterns.artifactPatterns.head
|
||||
val baseStr =
|
||||
pat.substring(0, pat.indexOf("[organisation]")).replace('\\', '/').stripSuffix("/")
|
||||
val repoDir =
|
||||
(if (baseStr.startsWith("file:")) new File(new java.net.URI(baseStr))
|
||||
else new File(baseStr)).getAbsoluteFile
|
||||
if pbr.patterns.isMavenCompatible then
|
||||
log.info(s"Ivyless publish (Maven layout) to file repo: $repoDir")
|
||||
ivylessPublishMavenToFile(
|
||||
artifacts,
|
||||
configuration.checksums,
|
||||
repoDir,
|
||||
configuration.overwrite,
|
||||
log
|
||||
)
|
||||
else
|
||||
log.info(s"Ivyless publish (Ivy layout) to file repo: $repoDir")
|
||||
ivylessPublishLocal(
|
||||
artifacts,
|
||||
configuration.checksums,
|
||||
repoDir,
|
||||
configuration.overwrite,
|
||||
log
|
||||
)
|
||||
case mavenCache: MavenCache =>
|
||||
ivylessPublishMavenToFile(
|
||||
artifacts,
|
||||
configuration.checksums,
|
||||
mavenCache.rootFile,
|
||||
configuration.overwrite,
|
||||
log
|
||||
)
|
||||
case mavenRepo: MavenRepo =>
|
||||
val root = mavenRepo.root.stripSuffix("/")
|
||||
if root.startsWith("http://") || root.startsWith("https://") then
|
||||
ivylessPublishMavenToUrl(
|
||||
artifacts,
|
||||
configuration.checksums,
|
||||
root,
|
||||
configuration.overwrite,
|
||||
log
|
||||
)
|
||||
else if root.startsWith("file:") then
|
||||
val repoBase = new File(URI.create(root))
|
||||
ivylessPublishMavenToFile(
|
||||
artifacts,
|
||||
configuration.checksums,
|
||||
repoBase,
|
||||
configuration.overwrite,
|
||||
log
|
||||
)
|
||||
else
|
||||
sys.error(
|
||||
s"ivyless Maven publish: unsupported root '$root'; use a supported repository (http/https/file)."
|
||||
)
|
||||
case other =>
|
||||
sys.error(
|
||||
s"ivyless publish does not support ${other.getClass.getName}; use URLRepository, FileRepository, or MavenRepository."
|
||||
)
|
||||
end publish
|
||||
|
||||
private def pluginCrossPath: Seq[String] =
|
||||
val attrs = project.module.attributes
|
||||
attrs.get(PomExtraDependencyAttributes.ScalaVersionKey).map("scala_" + _).toSeq ++
|
||||
attrs.get(PomExtraDependencyAttributes.SbtVersionKey).map("sbt_" + _).toSeq
|
||||
|
||||
private def typeToFolder(tpe: String): String = tpe match
|
||||
case "jar" => "jars"
|
||||
case "src" | "source" | "sources" => "srcs"
|
||||
case "doc" | "docs" | "javadoc" | "javadocs" => "docs"
|
||||
case "pom" => "poms"
|
||||
case "ivy" => "ivys"
|
||||
case other => other + "s"
|
||||
|
||||
/**
|
||||
* Publishes artifacts to the local Ivy repository without using Apache Ivy.
|
||||
* Uses the pattern: [org]/[module]/[revision]/[types]/[artifact](-[classifier]).[ext]
|
||||
*/
|
||||
private def ivylessPublishLocal(
|
||||
artifacts: Vector[(Artifact, File)],
|
||||
checksumAlgorithms: Vector[String],
|
||||
localRepoBase: File,
|
||||
overwrite: Boolean,
|
||||
log: Logger
|
||||
): Unit =
|
||||
val org = project.module.organization.value
|
||||
val moduleName = project.module.name.value
|
||||
val version = project.version
|
||||
|
||||
// Base directory: localRepoBase / org / module / (scala_V/)(sbt_V/) / version
|
||||
val moduleDir =
|
||||
pluginCrossPath.foldLeft(localRepoBase / org / moduleName)(_ / _) / version
|
||||
|
||||
log.info(s"Publishing to $moduleDir")
|
||||
|
||||
// Write ivy.xml first (so ivys/ exists even if artifact copy fails)
|
||||
val ivysDir = moduleDir / "ivys"
|
||||
val ivyXmlFile = ivysDir / "ivy.xml"
|
||||
IO.createDirectory(ivysDir)
|
||||
val ivyXmlContent = lmcoursier.IvyXml(project, Nil, Nil)
|
||||
if !ivyXmlFile.exists || overwrite then
|
||||
IO.write(ivyXmlFile, ivyXmlContent)
|
||||
log.info(s"published $ivyXmlFile")
|
||||
writeChecksumsForFile(ivyXmlFile, checksumAlgorithms, log)
|
||||
else log.warn(s"$ivyXmlFile already exists, skipping (overwrite=$overwrite)")
|
||||
|
||||
// Build a lookup from (type, classifier, ext) to cross-versioned publication name
|
||||
val pubNameLookup: Map[(String, String, String), String] =
|
||||
project.publications.map { (_, pub) =>
|
||||
(pub.`type`.value, pub.classifier.value, pub.ext.value) -> pub.name
|
||||
}.toMap
|
||||
|
||||
// Publish each artifact
|
||||
artifacts.foreach: (artifact, sourceFile) =>
|
||||
val folder = typeToFolder(artifact.`type`)
|
||||
val targetDir = moduleDir / folder
|
||||
|
||||
// Look up the cross-versioned artifact name from publications, fall back to module name
|
||||
val classifierStr = artifact.classifier.getOrElse("")
|
||||
val artName = pubNameLookup
|
||||
.getOrElse((artifact.`type`, classifierStr, artifact.extension), moduleName)
|
||||
val classifier = artifact.classifier.map("-" + _).getOrElse("")
|
||||
val fileName = s"$artName$classifier.${artifact.extension}"
|
||||
val targetFile = targetDir / fileName
|
||||
|
||||
if !targetFile.exists || overwrite then
|
||||
IO.createDirectory(targetDir)
|
||||
IO.copyFile(sourceFile, targetFile)
|
||||
log.info(s"published $targetFile")
|
||||
if !targetFile.getName.endsWith(signatureExt) then
|
||||
writeChecksumsForFile(targetFile, checksumAlgorithms, log)
|
||||
else log.warn(s"$targetFile already exists, skipping (overwrite=$overwrite)")
|
||||
end ivylessPublishLocal
|
||||
|
||||
/**
|
||||
* Substitutes Ivy pattern placeholders for artifact URL.
|
||||
* Matches ivylessPublishLocal layout: [organisation]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext]
|
||||
*/
|
||||
private def substituteIvyArtifactPattern(
|
||||
pattern: String,
|
||||
org: String,
|
||||
moduleName: String,
|
||||
version: String,
|
||||
typeFolder: String,
|
||||
artifactName: String,
|
||||
classifier: String,
|
||||
ext: String
|
||||
): String = {
|
||||
var s = pattern
|
||||
s = s.replace("[organisation]", org)
|
||||
s = s.replace("[module]", moduleName)
|
||||
s = s.replace("[revision]", version)
|
||||
s = s.replace("[type]s", typeFolder)
|
||||
s = s.replace("[artifact]", artifactName)
|
||||
s = s.replace("[ext]", ext)
|
||||
if (classifier.nonEmpty) s = s.replace("(-[classifier])", s"-$classifier")
|
||||
else s = s.replace("(-[classifier])", "")
|
||||
// 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
|
||||
}
|
||||
|
||||
/**
|
||||
* Picks credentials for a URL. Matches host; when realm is given, prefers credential with matching realm (per Publishing docs).
|
||||
*/
|
||||
private def credentialFor(
|
||||
url: URL,
|
||||
credentials: Seq[Credentials.DirectCredentials],
|
||||
realm: Option[String] = None
|
||||
): Option[Credentials.DirectCredentials] =
|
||||
val byHost = credentials.filter(_.host == url.getHost)
|
||||
realm match
|
||||
case Some(r) => byHost.find(_.realm == r).orElse(byHost.headOption)
|
||||
case None => byHost.headOption
|
||||
|
||||
/**
|
||||
* HTTP PUT a file to a URL with optional Basic auth.
|
||||
* Uses Gigahorse (Apache HttpClient) per sbt tech stack.
|
||||
*/
|
||||
private def httpPut(
|
||||
url: URL,
|
||||
sourceFile: File,
|
||||
credentials: Option[Credentials.DirectCredentials],
|
||||
log: Logger
|
||||
): Unit =
|
||||
val baseReq = Gigahorse.url(url.toString).put(sourceFile)
|
||||
val req = credentials match
|
||||
case Some(dc) => baseReq.withAuth(dc.userName, dc.passwd, AuthScheme.Basic)
|
||||
case None => baseReq
|
||||
val f = sbt.librarymanagement.Http.http.processFull(req)
|
||||
val response = Await.result(f, 5.minutes)
|
||||
val body = response.bodyAsString
|
||||
if response.status < 200 || response.status >= 300 then
|
||||
throw new IOException(
|
||||
s"PUT $url failed: ${response.status} ${response.statusText}$body"
|
||||
)
|
||||
log.info(s"published $url")
|
||||
|
||||
/**
|
||||
* Publishes artifacts to a remote Ivy repo (URLRepository) without using Apache Ivy.
|
||||
* Uses HTTP PUT; supports credentials. Produces the same layout as ivylessPublishLocal.
|
||||
*/
|
||||
private def ivylessPublish(
|
||||
artifacts: Vector[(Artifact, File)],
|
||||
checksumAlgorithms: Vector[String],
|
||||
urlRepo: URLRepository,
|
||||
overwrite: Boolean,
|
||||
log: Logger
|
||||
): Unit = {
|
||||
val org = project.module.organization.value
|
||||
val moduleName = project.module.name.value
|
||||
val version = project.version
|
||||
val artifactPattern = urlRepo.patterns.artifactPatterns.headOption.getOrElse(
|
||||
sys.error("URLRepository has no artifact pattern")
|
||||
)
|
||||
val ivyPattern = urlRepo.patterns.ivyPatterns.headOption.getOrElse(
|
||||
sys.error("URLRepository has no ivy pattern")
|
||||
)
|
||||
val directCreds = credentials.collect { case d: Credentials.DirectCredentials => d }
|
||||
|
||||
artifacts.foreach { case (artifact, sourceFile) =>
|
||||
val folder = typeToFolder(artifact.`type`)
|
||||
val classifier = artifact.classifier.map("-" + _).getOrElse("")
|
||||
val artifactName = moduleName
|
||||
val pathPattern = substituteIvyArtifactPattern(
|
||||
artifactPattern,
|
||||
org,
|
||||
moduleName,
|
||||
version,
|
||||
folder,
|
||||
artifactName,
|
||||
classifier,
|
||||
artifact.extension
|
||||
)
|
||||
val url = URI.create(pathPattern).toURL()
|
||||
httpPut(url, sourceFile, credentialFor(url, directCreds, None), log)
|
||||
if !url.toString.endsWith(signatureExt) then
|
||||
val checksums = writeChecksumsToTempFiles(sourceFile, checksumAlgorithms)
|
||||
checksums.foreach { case (cf, suffix) =>
|
||||
val checksumUrl = URI.create(pathPattern + suffix).toURL()
|
||||
try httpPut(checksumUrl, cf, credentialFor(checksumUrl, directCreds, None), log)
|
||||
finally cf.delete()
|
||||
}
|
||||
}
|
||||
|
||||
val ivyXmlContent = lmcoursier.IvyXml(project, Nil, Nil)
|
||||
val ivyPathPattern = substituteIvyArtifactPattern(
|
||||
ivyPattern,
|
||||
org,
|
||||
moduleName,
|
||||
version,
|
||||
"ivys",
|
||||
"ivy",
|
||||
"",
|
||||
"xml"
|
||||
)
|
||||
val ivyUrl = URI.create(ivyPathPattern).toURL()
|
||||
val ivyTmp = File.createTempFile("ivy", ".xml")
|
||||
try {
|
||||
IO.write(ivyTmp, ivyXmlContent)
|
||||
httpPut(ivyUrl, ivyTmp, credentialFor(ivyUrl, directCreds, None), log)
|
||||
val checksums = writeChecksumsToTempFiles(ivyTmp, checksumAlgorithms)
|
||||
checksums.foreach { case (cf, suffix) =>
|
||||
val checksumUrl = URI.create(ivyPathPattern + suffix).toURL()
|
||||
try httpPut(checksumUrl, cf, credentialFor(checksumUrl, directCreds, None), log)
|
||||
finally cf.delete()
|
||||
}
|
||||
} finally ivyTmp.delete()
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishes artifacts to a local file repo (FileRepository) without using Apache Ivy.
|
||||
* Same layout as ivylessPublishLocal; used for testing without an HTTP server.
|
||||
*/
|
||||
private def ivylessPublishToFile(
|
||||
artifacts: Vector[(Artifact, File)],
|
||||
checksumAlgorithms: Vector[String],
|
||||
fileRepo: FileRepository,
|
||||
overwrite: Boolean,
|
||||
log: Logger
|
||||
): Unit = {
|
||||
val pattern = fileRepo.patterns.artifactPatterns.headOption.getOrElse(
|
||||
sys.error("FileRepository has no artifact pattern")
|
||||
)
|
||||
val baseStr =
|
||||
if (pattern.contains("[organisation]"))
|
||||
pattern.substring(0, pattern.indexOf("[organisation]"))
|
||||
else pattern
|
||||
val normalized = baseStr.replace('\\', '/').stripSuffix("/")
|
||||
val localRepoBase =
|
||||
if (normalized.startsWith("file:")) new File(new java.net.URI(normalized))
|
||||
else new File(normalized)
|
||||
val repoDir = localRepoBase.getAbsoluteFile
|
||||
val isMavenLayout = fileRepo.patterns.isMavenCompatible
|
||||
if isMavenLayout then
|
||||
log.info(s"Ivyless publish (Maven layout) to file repo: $repoDir")
|
||||
ivylessPublishMavenToFile(artifacts, checksumAlgorithms, repoDir, overwrite, log)
|
||||
else
|
||||
log.info(s"Ivyless publish (Ivy layout) to file repo: $repoDir")
|
||||
ivylessPublishLocal(artifacts, checksumAlgorithms, repoDir, overwrite, log)
|
||||
}
|
||||
|
||||
/**
|
||||
* Maven layout path: groupId/artifactId/version/artifactId-version[-classifier].ext
|
||||
*/
|
||||
private def mavenLayoutPath(
|
||||
groupId: String,
|
||||
artifactId: String,
|
||||
version: String,
|
||||
artifact: Artifact
|
||||
): String =
|
||||
val groupPath = groupId.replace('.', '/')
|
||||
val classifierPart = artifact.classifier.map("-" + _).getOrElse("")
|
||||
val fileName = s"$artifactId-$version$classifierPart.${artifact.extension}"
|
||||
s"$groupPath/$artifactId/$version/$fileName"
|
||||
|
||||
private def normalizedChecksumAlgorithm(algo: String): String =
|
||||
algo.toLowerCase match
|
||||
case a @ ("md5" | "sha1") => a
|
||||
case other =>
|
||||
throw new IllegalArgumentException(s"Unsupported checksum algorithm: $other")
|
||||
|
||||
/**
|
||||
* Writes a `targetFile.<algo>` checksum file alongside `targetFile` for each algorithm.
|
||||
*/
|
||||
private def writeChecksumsForFile(
|
||||
targetFile: File,
|
||||
algorithms: Vector[String],
|
||||
log: Logger
|
||||
): Unit =
|
||||
algorithms.foreach: algo =>
|
||||
val digestAlgo = normalizedChecksumAlgorithm(algo)
|
||||
val digest = sbt.util.Digest(digestAlgo, targetFile.toPath)
|
||||
val checksumFile = new File(targetFile.getPath + "." + digestAlgo)
|
||||
IO.write(checksumFile, digest.hashHexString)
|
||||
log.debug(s"Wrote checksum: $checksumFile")
|
||||
|
||||
/**
|
||||
* Computes checksums for `file` and writes each to its own temp file, paired with its
|
||||
* suffix (e.g. ".md5"), for callers that need to HTTP PUT them elsewhere before discarding.
|
||||
*/
|
||||
private def writeChecksumsToTempFiles(
|
||||
file: File,
|
||||
algorithms: Vector[String]
|
||||
): Vector[(File, String)] =
|
||||
algorithms.map: algo =>
|
||||
val digestAlgo = normalizedChecksumAlgorithm(algo)
|
||||
val digest = sbt.util.Digest(digestAlgo, file.toPath)
|
||||
val suffix = "." + digestAlgo
|
||||
val tmpFile = File.createTempFile("checksum", suffix)
|
||||
IO.write(tmpFile, digest.hashHexString)
|
||||
(tmpFile, suffix)
|
||||
|
||||
/**
|
||||
* Publishes artifacts to a local Maven repo (Maven layout) without using Apache Ivy.
|
||||
* Layout: groupId/artifactId/version/artifactId-version[-classifier].ext
|
||||
*/
|
||||
private def ivylessPublishMavenToFile(
|
||||
artifacts: Vector[(Artifact, File)],
|
||||
checksumAlgorithms: Vector[String],
|
||||
repoBase: File,
|
||||
overwrite: Boolean,
|
||||
log: Logger
|
||||
): Unit =
|
||||
if repoBase == null then throw new IllegalArgumentException("repoBase must not be null")
|
||||
val groupId = project.module.organization.value
|
||||
// Derive artifactId: for sbt 2 plugins, module.name has cross-version (e.g. sbt-example_sbt2_3).
|
||||
// For sbt 1 plugins, mavenArtifactsOfSbtPlugin cross-versions the POM artifact name (e.g. sbt-example_2.12_1.0).
|
||||
val baseModuleName = project.module.name.value
|
||||
val pomArtName = artifacts.collectFirst { case (a, _) if a.`type` == "pom" => a.name }
|
||||
val artifactId = pomArtName match
|
||||
case Some(name) if name.startsWith(baseModuleName) && name != baseModuleName => name
|
||||
case _ => baseModuleName
|
||||
val version = project.version
|
||||
val groupPath = groupId.replace('.', '/')
|
||||
val versionDir = new File(repoBase, s"$groupPath/$artifactId/$version")
|
||||
log.info(s"Publishing to Maven repo: $versionDir")
|
||||
|
||||
artifacts.foreach:
|
||||
case (artifact, sourceFile) =>
|
||||
val path = mavenLayoutPath(groupId, artifactId, version, artifact)
|
||||
val targetFile = new File(repoBase, path.replace('/', File.separatorChar))
|
||||
if !targetFile.exists || overwrite then
|
||||
targetFile.getParentFile.mkdirs()
|
||||
IO.copyFile(sourceFile, targetFile)
|
||||
log.info(s"published $targetFile")
|
||||
if !targetFile.toString.endsWith(signatureExt) then
|
||||
writeChecksumsForFile(targetFile, checksumAlgorithms, log)
|
||||
else log.warn(s"$targetFile already exists, skipping (overwrite=$overwrite)")
|
||||
|
||||
if version.endsWith("-SNAPSHOT") then
|
||||
writeMavenMetadataLocal(versionDir, groupId, artifactId, version, log)
|
||||
|
||||
private def writeMavenMetadataLocal(
|
||||
versionDir: File,
|
||||
groupId: String,
|
||||
artifactId: String,
|
||||
version: String,
|
||||
log: Logger
|
||||
): Unit =
|
||||
val timestamp = new java.text.SimpleDateFormat("yyyyMMddHHmmss").format(new java.util.Date())
|
||||
val metadata =
|
||||
s"""|<?xml version="1.0" encoding="UTF-8"?>
|
||||
|<metadata modelVersion="1.1.0">
|
||||
| <groupId>$groupId</groupId>
|
||||
| <artifactId>$artifactId</artifactId>
|
||||
| <version>$version</version>
|
||||
| <versioning>
|
||||
| <snapshot>
|
||||
| <localCopy>true</localCopy>
|
||||
| </snapshot>
|
||||
| <lastUpdated>$timestamp</lastUpdated>
|
||||
| </versioning>
|
||||
|</metadata>
|
||||
|""".stripMargin
|
||||
val metadataFile = new File(versionDir, "maven-metadata-local.xml")
|
||||
IO.write(metadataFile, metadata)
|
||||
log.info(s"published $metadataFile")
|
||||
|
||||
/**
|
||||
* Publishes artifacts to a remote Maven repo (HTTP) without using Apache Ivy.
|
||||
* Same layout as ivylessPublishMavenToFile; uses HTTP PUT with optional Basic auth.
|
||||
*/
|
||||
private def ivylessPublishMavenToUrl(
|
||||
artifacts: Vector[(Artifact, File)],
|
||||
checksumAlgorithms: Vector[String],
|
||||
baseUrl: String,
|
||||
overwrite: Boolean,
|
||||
log: Logger
|
||||
): Unit =
|
||||
if baseUrl == null || baseUrl.trim.isEmpty then
|
||||
throw new IllegalArgumentException("baseUrl must not be null or empty")
|
||||
val groupId = project.module.organization.value
|
||||
// Derive artifactId: for sbt 2 plugins, module.name has cross-version (e.g. sbt-example_sbt2_3).
|
||||
// For sbt 1 plugins, mavenArtifactsOfSbtPlugin cross-versions the POM artifact name (e.g. sbt-example_2.12_1.0).
|
||||
val baseModuleName = project.module.name.value
|
||||
val pomArtName = artifacts.collectFirst { case (a, _) if a.`type` == "pom" => a.name }
|
||||
val artifactId = pomArtName match
|
||||
case Some(name) if name.startsWith(baseModuleName) && name != baseModuleName => name
|
||||
case _ => baseModuleName
|
||||
val version = project.version
|
||||
val directCreds = credentials.collect:
|
||||
case d: Credentials.DirectCredentials => d
|
||||
|
||||
val base = baseUrl.stripSuffix("/") + "/"
|
||||
artifacts.foreach:
|
||||
case (artifact, sourceFile) =>
|
||||
val path = mavenLayoutPath(groupId, artifactId, version, artifact)
|
||||
val url = URI.create(base + path).toURL()
|
||||
try
|
||||
httpPut(url, sourceFile, credentialFor(url, directCreds, None), log)
|
||||
if !sourceFile.toString.endsWith(signatureExt) then
|
||||
val checksums = writeChecksumsToTempFiles(sourceFile, checksumAlgorithms)
|
||||
checksums.foreach:
|
||||
case (cf, suffix) =>
|
||||
val checksumUrl = URI.create(base + path + suffix).toURL()
|
||||
try httpPut(checksumUrl, cf, credentialFor(checksumUrl, directCreds, None), log)
|
||||
finally cf.delete()
|
||||
catch
|
||||
case e: IOException =>
|
||||
throw new IOException(s"Failed to publish $path: ${e.getMessage}", e)
|
||||
end GenericPublisher
|
||||
|
||||
object GenericPublisher:
|
||||
def apply(
|
||||
dependencyResolution: DependencyResolution,
|
||||
pomRepositories: Vector[Resolver],
|
||||
project: CsrProject,
|
||||
credentials: Seq[Credentials]
|
||||
): GenericPublisher =
|
||||
apply(dependencyResolution, pomRepositories, project, credentials, Nil)
|
||||
|
||||
def apply(
|
||||
dependencyResolution: DependencyResolution,
|
||||
pomRepositories: Vector[Resolver],
|
||||
project: CsrProject,
|
||||
credentials: Seq[Credentials],
|
||||
resolvers: Seq[Resolver]
|
||||
): GenericPublisher =
|
||||
new GenericPublisher(dependencyResolution, pomRepositories, project, credentials, resolvers)
|
||||
end GenericPublisher
|
||||
|
|
@ -1,2 +1,17 @@
|
|||
Global / credentials := Seq(Credentials("", "pgp", "", "test password"))
|
||||
Global / pgpSecretRing := baseDirectory.value / "secring.pgp"
|
||||
Global / pgpPublicRing := baseDirectory.value / "pubring.pgp"
|
||||
Global / useGpg := false
|
||||
|
||||
scalaVersion := "3.8.4"
|
||||
organization := "com.example"
|
||||
name := "app"
|
||||
version := "1.0"
|
||||
publishLocal := {}
|
||||
|
||||
publishTo := {
|
||||
val centralSnapshots = "https://central.sonatype.com/repository/maven-snapshots/"
|
||||
if isSnapshot.value then Some("central-snapshots" at centralSnapshots)
|
||||
else localStaging.value
|
||||
}
|
||||
usePgpKeyHex("AA2DBC9295B91B7A")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Version: BCPG v1.51
|
||||
|
||||
mQENBFVcLH0BCACmERkKh73zr+0nFl7/6WWP3QcNa3msWDlArP6GzQEOmFBNZEtv
|
||||
1BRSKz1fGl4aZzpNPDh2rNcsjVhJPzwVLUzsJuWgBa2EdYafnsUjQ37V998iFhtZ
|
||||
ZgbXieJ2yMvaNPPrPjgYBAZFrnQmH0oQDx+GWDd5Y3lQBx0heIJA5YjPN3meDzqZ
|
||||
FC0qxSVeCPFYaxAFoMnNDEohpvGV3iiUTAyTgSDofxJqVV20oeMCJzC89VskCjSV
|
||||
KIlCnKIRFA+WtND1AsJuIBM/x4JQkRF8xc/2tS5xGXSQllgwxHXEnhDDXcT0o6pY
|
||||
Ni2xYSG0gcmwaBGvx8N1RjWcVms/iJ4ViayxABEBAAG0C1Rlc3QgPFRlc3Q+iQEc
|
||||
BBMBAgAGBQJVXCx9AAoJEKotvJKVuRt6fQkH/1XhdHxOB5m92TasMkxCqjuK0EtQ
|
||||
0HEIkoRC+Z9gOUqlhK58y+8DK9ZAEx/e09CDK1N+x/R3xQUKh2KbUpS7yiNzzqZO
|
||||
OejWtmWw6gYNHy1COP9WKT8Qgr+z4d7GBTrGthEhvZftHyyqlN6JZJZ2ZcvE/Odz
|
||||
QkMbd4aeRXi+KgzHp4fa1hLaA2BT7TT1trYH2L6OI6VbcEebnz+up738liHq9p/R
|
||||
JrJP4JjLzWtEkL83pE6FHhXowsdaG8WKYfWRstX7RixHq5P13PW/iAZEAmJb08ER
|
||||
mmguiicjExRk7UaIVwKPa5P1DAOgeK/ejCLsxrUKspIgt6JLQFbprGZHRdE=
|
||||
=/1s8
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
Version: BCPG v1.51
|
||||
|
||||
lQO+BFVcLH0BCACmERkKh73zr+0nFl7/6WWP3QcNa3msWDlArP6GzQEOmFBNZEtv
|
||||
1BRSKz1fGl4aZzpNPDh2rNcsjVhJPzwVLUzsJuWgBa2EdYafnsUjQ37V998iFhtZ
|
||||
ZgbXieJ2yMvaNPPrPjgYBAZFrnQmH0oQDx+GWDd5Y3lQBx0heIJA5YjPN3meDzqZ
|
||||
FC0qxSVeCPFYaxAFoMnNDEohpvGV3iiUTAyTgSDofxJqVV20oeMCJzC89VskCjSV
|
||||
KIlCnKIRFA+WtND1AsJuIBM/x4JQkRF8xc/2tS5xGXSQllgwxHXEnhDDXcT0o6pY
|
||||
Ni2xYSG0gcmwaBGvx8N1RjWcVms/iJ4ViayxABEBAAH+AwMCQJCBiVce8z5gAZr+
|
||||
L8W6NOs8orCIgLbCjKAHuKan9mZXtvOaml/8EDiBjLvwekUQj0OI6S29y5QsQpvn
|
||||
lo8vXAsJnA/Q0olrAim2aZPOXVjQOYsOwExo4SAu7zXBQ3w3+jYsM5kHYPWICr3g
|
||||
3LzhVUmDTHjein/Xa9X7M8bzAY0Esoabl1aSKZ9K15P2ss7noHXrKZPxRk3jb0XP
|
||||
jHE2h5zTLLkdyXnZ74ILVYYEOjqe7P/+eWBL5TIan73ekNCKkgRBKI+pWB8Bc8sA
|
||||
ww2WtlkpsogVItjZ7spk03HVykSr/hS2TnVoR1vX+C8bPLjvwksNORWELf9z32vA
|
||||
ASgqoqeKMc+qHLPnZOrNIoCwUgBHLq/XL09E4Sav1TnqQt14Ya3oTiWQARMY+6jC
|
||||
FGpHhKhx4XjrBjUyGWm8EzC/IPWIXSs9oeOxfSag7QNCf3F1fOjUOYkt32bTpEiM
|
||||
X5sxrovkW/bh0U7thnIsHeklDx99U89F3J0K5svN66f7XplN3wYgwBJA/Fjymgj0
|
||||
zYN0cibnL1rp6zyMRSu5iDQRq1A9rFzsOQb4+gyNMUvlSM1ajA/sFvHsk0xFXdPG
|
||||
ADOx9Tn3f4JN58ylGnMygtBmcS4f+lDVS0Q96lRuyFRTze/+JuhVTPNk2kClNp8y
|
||||
98dc4UMawEyZ55EbxhVKx10jZ74Sswk8N15NhSZN5IKBUwQ1JWAoMnn0UDFeniBW
|
||||
gjmD/82a0QzosJRkOEKqaCUK02FUgFNcB/6Aauj5Pm6vDehWnk+4Kz4f2QrqdD+Y
|
||||
DquqcQ88gPj7jJnRX/+lMwKd73PeK3GfpvWCkRcliUs47LQ585uc03lArxpG2j74
|
||||
hG9Lxt/B545sqsYt2ViJ0hNBRgwfUdGy6NVef4F6JxVxkD32eavDDUBfXC/a5zRu
|
||||
m7QLVGVzdCA8VGVzdD6JARwEEwECAAYFAlVcLH0ACgkQqi28kpW5G3p9CQf/VeF0
|
||||
fE4Hmb3ZNqwyTEKqO4rQS1DQcQiShEL5n2A5SqWErnzL7wMr1kATH97T0IMrU37H
|
||||
9HfFBQqHYptSlLvKI3POpk456Na2ZbDqBg0fLUI4/1YpPxCCv7Ph3sYFOsa2ESG9
|
||||
l+0fLKqU3olklnZly8T853NCQxt3hp5FeL4qDMenh9rWEtoDYFPtNPW2tgfYvo4j
|
||||
pVtwR5ufP66nvfyWIer2n9Emsk/gmMvNa0SQvzekToUeFejCx1obxYph9ZGy1ftG
|
||||
LEerk/Xc9b+IBkQCYlvTwRGaaC6KJyMTFGTtRohXAo9rk/UMA6B4r96MIuzGtQqy
|
||||
kiC3oktAVumsZkdF0Q==
|
||||
=wGX/
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
||||
|
|
@ -1 +1,4 @@
|
|||
> plugins
|
||||
> publishSigned
|
||||
|
||||
$ exists target/sona-staging/com/example/app_3/1.0/app_3-1.0.jar.asc
|
||||
# $ absent target/sona-staging/com/example/app_3/1.0/app_3-1.0-sources.jar.asc.md5
|
||||
|
|
|
|||
|
|
@ -16,7 +16,13 @@ import sbt.Def.{ Initialize, Setting }
|
|||
import sbt.Keys.*
|
||||
import sbt.ProjectExtra.*
|
||||
import sbt.internal.LibraryManagement
|
||||
import sbt.internal.librarymanagement.{ IvyActions, IvySbt, IvyXml, ProjectResolver }
|
||||
import sbt.internal.librarymanagement.{
|
||||
GenericPublisher,
|
||||
IvyActions,
|
||||
IvySbt,
|
||||
IvyXml,
|
||||
ProjectResolver
|
||||
}
|
||||
import sbt.internal.librarymanagement.ivy.*
|
||||
import sbt.io.syntax.*
|
||||
import sbt.librarymanagement.*
|
||||
|
|
@ -67,7 +73,22 @@ object IvyDependencyPlugin extends AutoPlugin:
|
|||
}
|
||||
)(
|
||||
Def.task {
|
||||
Classpaths.defaultPublisher(dependencyResolution.value, fullResolvers.value.toVector)
|
||||
val ivyHome = ivyPaths.value.ivyHome.map(new File(_)).getOrElse {
|
||||
new File(System.getProperty("user.home")) / ".ivy2"
|
||||
}
|
||||
val localResolver =
|
||||
Resolver.file("local", ivyHome / "local")(using Resolver.ivyStylePatterns)
|
||||
// otherResolvers already has Resolver.publishMavenLocal +: publishTo.value.toVector
|
||||
val knownResolvers = localResolver +: otherResolvers.value
|
||||
Publisher(
|
||||
GenericPublisher(
|
||||
dependencyResolution.value,
|
||||
fullResolvers.value.toVector,
|
||||
csrProject.value.withPublications(csrPublications.value),
|
||||
allCredentials.value,
|
||||
knownResolvers
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
.value
|
||||
|
|
|
|||
Loading…
Reference in New Issue