mirror of https://github.com/sbt/sbt.git
[2.x] feat: Implement ivyless publishLocal (#8634)
Fixes #8631 **Changes:** - Add `useIvy` setting key (defaults to `true`) - Add `ivylessPublishLocalImpl` helper that publishes without Ivy - Modify `publishLocal` to use ivyless publisher when `useIvy := false` - Generate ivy.xml via `lmcoursier.IvyXml` - Generate MD5/SHA-1 checksums for all files - Add scripted test `dependency-management/ivyless-publish-local`
This commit is contained in:
parent
cf8899919d
commit
5789a7ef77
|
|
@ -2866,6 +2866,7 @@ object Classpaths {
|
|||
Seq(
|
||||
publishMavenStyle :== true,
|
||||
sbtPluginPublishLegacyMavenStyle :== false,
|
||||
useIvy :== true,
|
||||
publishArtifact :== true,
|
||||
(Test / publishArtifact) :== false
|
||||
)
|
||||
|
|
@ -2976,7 +2977,7 @@ object Classpaths {
|
|||
deliverLocal := deliverTask(makeIvyXmlLocalConfiguration).value,
|
||||
makeIvyXml := deliverTask(makeIvyXmlConfiguration).value,
|
||||
publish := publishOrSkip(publishConfiguration, publish / skip).value,
|
||||
publishLocal := publishOrSkip(publishLocalConfiguration, publishLocal / skip).value,
|
||||
publishLocal := LibraryManagement.ivylessPublishLocalTask.value,
|
||||
publishM2 := publishOrSkip(publishM2Configuration, publishM2 / skip).value,
|
||||
credentials ++= Def.uncached {
|
||||
val alreadyContainsCentralCredentials: Boolean = credentials.value.exists {
|
||||
|
|
|
|||
|
|
@ -550,6 +550,7 @@ object Keys {
|
|||
val packagedArtifacts = taskKey[Map[Artifact, HashedVirtualFileRef]]("Packages all artifacts for publishing and maps the Artifact definition to the generated file.").withRank(CTask)
|
||||
val publishMavenStyle = settingKey[Boolean]("Configures whether to generate and publish a pom (true) or Ivy file (false).").withRank(BSetting)
|
||||
val sbtPluginPublishLegacyMavenStyle = settingKey[Boolean]("Configuration for generating the legacy pom of sbt plugins, to publish to Maven").withRank(CSetting)
|
||||
val useIvy = settingKey[Boolean]("Use Ivy for publishing. When false, use the ivyless publisher.").withRank(BSetting)
|
||||
|
||||
@transient
|
||||
val credentials = taskKey[Seq[Credentials]]("The credentials to use for updating and publishing.").withRank(BMinusTask)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import sbt.io.syntax.*
|
|||
import sbt.ProjectExtra.*
|
||||
import sjsonnew.JsonFormat
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
import lmcoursier.definitions.Project as CsrProject
|
||||
|
||||
private[sbt] object LibraryManagement {
|
||||
given linter: sbt.dsl.LinterLevel.Ignore.type = sbt.dsl.LinterLevel.Ignore
|
||||
|
|
@ -543,4 +544,119 @@ private[sbt] object LibraryManagement {
|
|||
def isDynamicScalaVersion(version: String): Boolean = {
|
||||
version == "3-latest.candidate"
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 / version
|
||||
val moduleDir = 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")
|
||||
|
||||
// Publish each artifact
|
||||
artifacts.foreach: (artifact, sourceFile) =>
|
||||
val folder = typeToFolder(artifact.`type`)
|
||||
val targetDir = moduleDir / folder
|
||||
|
||||
// Construct filename using module name (includes Scala version suffix) + classifier + extension
|
||||
val classifier = artifact.classifier.map("-" + _).getOrElse("")
|
||||
val fileName = s"$moduleName$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)")
|
||||
|
||||
// Generate and write ivy.xml
|
||||
val ivyXmlContent = lmcoursier.IvyXml(project, Nil, Nil)
|
||||
val ivysDir = moduleDir / "ivys"
|
||||
val ivyXmlFile = ivysDir / "ivy.xml"
|
||||
if !ivyXmlFile.exists || overwrite then
|
||||
IO.createDirectory(ivysDir)
|
||||
IO.write(ivyXmlFile, ivyXmlContent)
|
||||
log.info(s"Published $ivyXmlFile")
|
||||
writeChecksums(ivyXmlFile)
|
||||
else log.warn(s"$ivyXmlFile already exists, skipping (overwrite=$overwrite)")
|
||||
end ivylessPublishLocal
|
||||
|
||||
/**
|
||||
* 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
|
||||
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.map { case (a, f) => (a, f) }
|
||||
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)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
ThisBuild / csrCacheDirectory := (ThisBuild / baseDirectory).value / "coursier-cache"
|
||||
|
||||
name := "lib1"
|
||||
organization := "com.example"
|
||||
version := "0.1.0-SNAPSHOT"
|
||||
scalaVersion := "3.8.1"
|
||||
|
||||
// Use a fixed path for local ivy repo to avoid sbt 2.x output sharding
|
||||
val ivyLocalBase = settingKey[File]("Local Ivy repository base")
|
||||
ivyLocalBase := baseDirectory.value / "local-ivy-repo"
|
||||
|
||||
// Custom ivy paths to write under a fixed directory
|
||||
ivyPaths := IvyPaths(
|
||||
baseDirectory.value.toString,
|
||||
Some(ivyLocalBase.value.toString)
|
||||
)
|
||||
|
||||
// Use ivyless publisher
|
||||
useIvy := false
|
||||
|
||||
// Disable doc generation to speed up the test
|
||||
Compile / packageDoc / publishArtifact := false
|
||||
Compile / packageSrc / publishArtifact := false
|
||||
|
||||
// Task to print debug info
|
||||
val printPaths = taskKey[Unit]("Print paths for debugging")
|
||||
printPaths := {
|
||||
val log = streams.value.log
|
||||
val ip = ivyPaths.value
|
||||
log.info(s"ivyPaths.baseDirectory = ${ip.baseDirectory}")
|
||||
log.info(s"ivyPaths.ivyHome = ${ip.ivyHome}")
|
||||
log.info(s"ivyLocalBase = ${ivyLocalBase.value}")
|
||||
log.info(s"useIvy = ${useIvy.value}")
|
||||
}
|
||||
|
||||
// Task to check that files were published correctly
|
||||
val checkIvylessPublish = taskKey[Unit]("Check that ivyless publish produced the expected files")
|
||||
checkIvylessPublish := {
|
||||
val log = streams.value.log
|
||||
val base = ivyLocalBase.value / "local"
|
||||
val org = organization.value
|
||||
val moduleName = normalizedName.value + "_3"
|
||||
val ver = version.value
|
||||
|
||||
val moduleDir = base / org / moduleName / ver
|
||||
|
||||
log.info(s"ivyLocalBase = ${ivyLocalBase.value}")
|
||||
log.info(s"Checking published files in $moduleDir")
|
||||
|
||||
// List what's actually in the ivy-repo directory
|
||||
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)
|
||||
|
||||
// Check that the main artifacts 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")
|
||||
}
|
||||
|
||||
// Check jar file and checksums
|
||||
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")
|
||||
|
||||
// Check ivy.xml and checksums
|
||||
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")
|
||||
|
||||
// Check ivy.xml content
|
||||
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")
|
||||
|
||||
log.info("All ivyless publish checks passed!")
|
||||
}
|
||||
|
||||
// Task to clean the local ivy repo
|
||||
val cleanLocalIvy = taskKey[Unit]("Clean the local ivy repo")
|
||||
cleanLocalIvy := {
|
||||
val base = ivyLocalBase.value / "local"
|
||||
IO.delete(base)
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
// empty plugins file
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.example
|
||||
|
||||
object Lib {
|
||||
def hello: String = "Hello from lib1!"
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# Test ivyless publishLocal
|
||||
# This test verifies that when useIvy is set to false in build.sbt,
|
||||
# publishLocal produces the expected file structure
|
||||
|
||||
# Clean any previous test state
|
||||
> cleanLocalIvy
|
||||
|
||||
# Print paths for debugging (should show useIvy = false)
|
||||
> printPaths
|
||||
|
||||
# Run publishLocal with ivyless publisher
|
||||
> publishLocal
|
||||
|
||||
# Verify the published files
|
||||
> checkIvylessPublish
|
||||
|
|
@ -23,6 +23,7 @@ object Digest:
|
|||
val tokens = parse(d)
|
||||
s"${tokens._1}-${tokens._2}"
|
||||
def algo: String = parse(d)._1
|
||||
def hashHexString: String = parse(d)._2
|
||||
def toBytes: Array[Byte] = parse(d)._4
|
||||
def sizeBytes: Long = parse(d)._3
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue