mirror of https://github.com/sbt/sbt.git
[2.x] feat: Ivyless Maven repo publish (#8692)
- Add ivylessPublishMavenToFile and ivylessPublishMavenToUrl for Maven layout - Handle MavenCache and MavenRepo in ivylessPublishTask (file + HTTP) - Add credentialFor for realm+host credential matching per Publishing docs - Consume HTTP response body on success to avoid connection leak - Add scripted tests: ivyless-publish-maven, ivyless-publish-maven-http
This commit is contained in:
parent
2f27b5cecd
commit
54548041cf
|
|
@ -650,6 +650,19 @@ private[sbt] object LibraryManagement {
|
|||
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.
|
||||
|
|
@ -659,22 +672,19 @@ private[sbt] object LibraryManagement {
|
|||
sourceFile: File,
|
||||
credentials: Option[Credentials.DirectCredentials],
|
||||
log: Logger
|
||||
): Unit = {
|
||||
): Unit =
|
||||
val baseReq = Gigahorse.url(url.toString).put(sourceFile)
|
||||
val req = credentials.filter(_.host == url.getHost) match {
|
||||
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)
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
val body = response.bodyAsString
|
||||
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.
|
||||
|
|
@ -738,11 +748,11 @@ private[sbt] object LibraryManagement {
|
|||
artifact.extension
|
||||
)
|
||||
val url = URI.create(pathPattern).toURL()
|
||||
httpPut(url, sourceFile, directCreds.find(_.host == url.getHost), log)
|
||||
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, directCreds.find(_.host == url.getHost), log)
|
||||
try httpPut(checksumUrl, cf, credentialFor(checksumUrl, directCreds, None), log)
|
||||
finally cf.delete()
|
||||
}
|
||||
}
|
||||
|
|
@ -762,16 +772,131 @@ private[sbt] object LibraryManagement {
|
|||
val ivyTmp = File.createTempFile("ivy", ".xml")
|
||||
try {
|
||||
IO.write(ivyTmp, ivyXmlContent)
|
||||
httpPut(ivyUrl, ivyTmp, directCreds.find(_.host == ivyUrl.getHost), log)
|
||||
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, directCreds.find(_.host == ivyUrl.getHost), log)
|
||||
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
|
||||
val artifactId = project.module.name.value
|
||||
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)")
|
||||
|
||||
/**
|
||||
* 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
|
||||
val artifactId = project.module.name.value
|
||||
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)
|
||||
.toVector
|
||||
|
||||
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.
|
||||
|
|
@ -869,9 +994,46 @@ private[sbt] object LibraryManagement {
|
|||
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
|
||||
log.warn(s"Ivyless Maven publish: unsupported root '$root'. Falling back to Ivy.")
|
||||
val conf = publishConfiguration.value
|
||||
val module = ivyModule.value
|
||||
publisher.value.publish(module, conf, log)
|
||||
case _ =>
|
||||
log.warn(
|
||||
"Ivyless publish only supports URLRepository (Resolver.url) or FileRepository (Resolver.file). Falling back to Ivy."
|
||||
"Ivyless publish only supports URLRepository, FileRepository, or MavenRepository. Falling back to Ivy."
|
||||
)
|
||||
val conf = publishConfiguration.value
|
||||
val module = ivyModule.value
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package com.example
|
||||
|
||||
object Lib {
|
||||
def hello: String = "Hello from a!"
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
object B {
|
||||
def msg: String = com.example.Lib.hello
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
ThisBuild / csrCacheDirectory := (ThisBuild / baseDirectory).value / "coursier-cache"
|
||||
ThisBuild / organization := "com.example"
|
||||
ThisBuild / version := "0.1.0-SNAPSHOT"
|
||||
ThisBuild / scalaVersion := "3.8.1"
|
||||
|
||||
val publishRepoBase = settingKey[File]("Base directory for Maven publish repo (HTTP server writes here)")
|
||||
ThisBuild / publishRepoBase := (ThisBuild / baseDirectory).value / "repo"
|
||||
val publishPort = 3031
|
||||
|
||||
lazy val root = (project in file("."))
|
||||
.aggregate(a, b)
|
||||
.settings(
|
||||
publish / skip := true,
|
||||
)
|
||||
|
||||
lazy val a = project
|
||||
.settings(
|
||||
publishMavenStyle := true,
|
||||
publishTo := Some(
|
||||
sbt.librarymanagement.MavenRepo("test-repo", s"http://localhost:$publishPort/")
|
||||
.withAllowInsecureProtocol(true)
|
||||
),
|
||||
useIvy := false,
|
||||
Compile / packageDoc / publishArtifact := false,
|
||||
Compile / packageSrc / publishArtifact := false,
|
||||
)
|
||||
|
||||
lazy val b = project
|
||||
.settings(
|
||||
libraryDependencies += organization.value %% "a" % version.value,
|
||||
resolvers += sbt.librarymanagement.MavenRepo(
|
||||
"test-repo",
|
||||
s"http://localhost:$publishPort/"
|
||||
).withAllowInsecureProtocol(true),
|
||||
)
|
||||
|
||||
val startPublishServer = taskKey[Unit]("Start HTTP server that accepts PUT to repo directory")
|
||||
Global / startPublishServer := {
|
||||
HttpPutServer.start(publishPort, (ThisBuild / publishRepoBase).value)
|
||||
streams.value.log.info(s"HTTP PUT server started on port $publishPort, writing to ${(ThisBuild / publishRepoBase).value}")
|
||||
}
|
||||
|
||||
val stopPublishServer = taskKey[Unit]("Stop HTTP server")
|
||||
Global / stopPublishServer := {
|
||||
HttpPutServer.stop()
|
||||
streams.value.log.info("HTTP PUT server stopped")
|
||||
}
|
||||
|
||||
val checkMavenPublish = taskKey[Unit]("Check that ivyless Maven publish produced the expected files")
|
||||
Global / checkMavenPublish := {
|
||||
val log = streams.value.log
|
||||
val base = (ThisBuild / publishRepoBase).value
|
||||
val groupId = (ThisBuild / organization).value
|
||||
val artifactId = "a_3"
|
||||
val ver = (ThisBuild / version).value
|
||||
val groupPath = groupId.replace('.', '/')
|
||||
val versionDir = base / groupPath / artifactId / ver
|
||||
|
||||
log.info(s"Checking published Maven layout in $versionDir")
|
||||
|
||||
def listDir(dir: File, indent: String = ""): Unit = {
|
||||
if (dir.exists) {
|
||||
dir.listFiles.foreach { f =>
|
||||
log.info(s"$indent${f.getName}")
|
||||
if (f.isDirectory) listDir(f, indent + " ")
|
||||
}
|
||||
} else {
|
||||
log.info(s"${indent}Directory does not exist: $dir")
|
||||
}
|
||||
}
|
||||
log.info("Contents of publish repo:")
|
||||
listDir(base)
|
||||
|
||||
assert(versionDir.exists && versionDir.isDirectory, s"Expected version dir $versionDir to exist")
|
||||
|
||||
val pomFile = versionDir / s"$artifactId-$ver.pom"
|
||||
assert(pomFile.exists, s"Expected $pomFile to exist")
|
||||
assert(new File(pomFile.getPath + ".md5").exists, s"Expected pom md5 checksum")
|
||||
assert(new File(pomFile.getPath + ".sha1").exists, s"Expected pom sha1 checksum")
|
||||
|
||||
val jarFile = versionDir / s"$artifactId-$ver.jar"
|
||||
assert(jarFile.exists, s"Expected $jarFile to exist")
|
||||
assert(new File(jarFile.getPath + ".md5").exists, s"Expected jar md5 checksum")
|
||||
assert(new File(jarFile.getPath + ".sha1").exists, s"Expected jar sha1 checksum")
|
||||
|
||||
val pomContent = IO.read(pomFile)
|
||||
assert(pomContent.contains(s"<groupId>$groupId</groupId>"), s"POM should contain groupId")
|
||||
assert(pomContent.contains(s"<artifactId>$artifactId</artifactId>"), s"POM should contain artifactId")
|
||||
assert(pomContent.contains(s"<version>$ver</version>"), s"POM should contain version")
|
||||
|
||||
log.info("All ivyless Maven publish (HTTP) checks passed!")
|
||||
}
|
||||
|
||||
val cleanPublishRepo = taskKey[Unit]("Clean the publish repo")
|
||||
Global / cleanPublishRepo := {
|
||||
IO.delete((ThisBuild / publishRepoBase).value)
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import java.io._
|
||||
import java.net.InetSocketAddress
|
||||
import com.sun.net.httpserver.{ HttpExchange, HttpHandler, HttpServer }
|
||||
|
||||
object HttpPutServer {
|
||||
private var server: HttpServer = null
|
||||
|
||||
def start(port: Int, baseDir: java.io.File): Unit = {
|
||||
if (server != null) stop()
|
||||
server = HttpServer.create(new InetSocketAddress(port), 0)
|
||||
server.createContext("/", new HttpHandler {
|
||||
override def handle(ex: HttpExchange): Unit = {
|
||||
val method = ex.getRequestMethod
|
||||
val path = ex.getRequestURI.getRawPath
|
||||
val relativePath = if (path.startsWith("/")) path.substring(1) else path
|
||||
val targetFile = new File(baseDir, relativePath.replace("/", File.separator))
|
||||
|
||||
if ("PUT".equalsIgnoreCase(method)) {
|
||||
targetFile.getParentFile.mkdirs()
|
||||
val in = ex.getRequestBody
|
||||
val out = new FileOutputStream(targetFile)
|
||||
try {
|
||||
val buf = new Array[Byte](8192)
|
||||
var n = 0
|
||||
while ({ n = in.read(buf); n } != -1) out.write(buf, 0, n)
|
||||
} finally {
|
||||
out.close()
|
||||
in.close()
|
||||
}
|
||||
ex.sendResponseHeaders(200, -1)
|
||||
ex.close()
|
||||
} else if ("GET".equalsIgnoreCase(method)) {
|
||||
val baseCanon = baseDir.getCanonicalPath + File.separator
|
||||
val fileCanon = targetFile.getCanonicalPath
|
||||
if (!fileCanon.startsWith(baseCanon)) {
|
||||
ex.sendResponseHeaders(403, -1)
|
||||
ex.close()
|
||||
} else if (targetFile.isFile) {
|
||||
val bytes = java.nio.file.Files.readAllBytes(targetFile.toPath)
|
||||
ex.sendResponseHeaders(200, bytes.length)
|
||||
val out = ex.getResponseBody
|
||||
try out.write(bytes)
|
||||
finally out.close()
|
||||
ex.close()
|
||||
} else {
|
||||
ex.sendResponseHeaders(404, -1)
|
||||
ex.close()
|
||||
}
|
||||
} else {
|
||||
ex.sendResponseHeaders(405, -1)
|
||||
ex.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
server.setExecutor(null)
|
||||
server.start()
|
||||
}
|
||||
|
||||
def stop(): Unit = {
|
||||
if (server != null) {
|
||||
server.stop(0)
|
||||
server = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
// empty plugins file
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# Test ivyless publish to Maven repo via HTTP - issue #8688
|
||||
# a publishes to HTTP server; b consumes a from the same Maven repo (server stays up for resolve)
|
||||
|
||||
> cleanPublishRepo
|
||||
> startPublishServer
|
||||
> a/publish
|
||||
> checkMavenPublish
|
||||
> b/compile
|
||||
> stopPublishServer
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
ThisBuild / csrCacheDirectory := (ThisBuild / baseDirectory).value / "coursier-cache"
|
||||
|
||||
name := "lib1"
|
||||
organization := "com.example"
|
||||
version := "0.1.0-SNAPSHOT"
|
||||
scalaVersion := "3.8.1"
|
||||
|
||||
publishMavenStyle := true
|
||||
val publishRepoBase = settingKey[File]("Base directory for Maven publish repo")
|
||||
publishRepoBase := baseDirectory.value / "repo"
|
||||
|
||||
publishTo := Some(sbt.librarymanagement.MavenCache("local-maven", publishRepoBase.value))
|
||||
|
||||
useIvy := false
|
||||
|
||||
Compile / packageDoc / publishArtifact := false
|
||||
Compile / packageSrc / publishArtifact := false
|
||||
|
||||
val checkMavenPublish = taskKey[Unit]("Check that ivyless Maven publish produced the expected files")
|
||||
checkMavenPublish := {
|
||||
val log = streams.value.log
|
||||
val base = publishRepoBase.value
|
||||
val groupId = organization.value
|
||||
val artifactId = normalizedName.value + "_3"
|
||||
val ver = version.value
|
||||
val groupPath = groupId.replace('.', '/')
|
||||
val versionDir = base / groupPath / artifactId / ver
|
||||
|
||||
log.info(s"Checking published Maven layout in $versionDir")
|
||||
|
||||
def listDir(dir: File, indent: String = ""): Unit = {
|
||||
if (dir.exists) {
|
||||
dir.listFiles.foreach { f =>
|
||||
log.info(s"$indent${f.getName}")
|
||||
if (f.isDirectory) listDir(f, indent + " ")
|
||||
}
|
||||
} else {
|
||||
log.info(s"${indent}Directory does not exist: $dir")
|
||||
}
|
||||
}
|
||||
log.info("Contents of publish repo:")
|
||||
listDir(base)
|
||||
|
||||
assert(versionDir.exists && versionDir.isDirectory, s"Expected version dir $versionDir to exist")
|
||||
|
||||
val pomFile = versionDir / s"$artifactId-$ver.pom"
|
||||
assert(pomFile.exists, s"Expected $pomFile to exist")
|
||||
assert(new File(pomFile.getPath + ".md5").exists, s"Expected pom md5 checksum")
|
||||
assert(new File(pomFile.getPath + ".sha1").exists, s"Expected pom sha1 checksum")
|
||||
|
||||
val jarFile = versionDir / s"$artifactId-$ver.jar"
|
||||
assert(jarFile.exists, s"Expected $jarFile to exist")
|
||||
assert(new File(jarFile.getPath + ".md5").exists, s"Expected jar md5 checksum")
|
||||
assert(new File(jarFile.getPath + ".sha1").exists, s"Expected jar sha1 checksum")
|
||||
|
||||
val pomContent = IO.read(pomFile)
|
||||
assert(pomContent.contains(s"<groupId>$groupId</groupId>"), s"POM should contain groupId")
|
||||
assert(pomContent.contains(s"<artifactId>$artifactId</artifactId>"), s"POM should contain artifactId")
|
||||
assert(pomContent.contains(s"<version>$ver</version>"), s"POM should contain version")
|
||||
|
||||
log.info("All ivyless Maven publish checks passed!")
|
||||
}
|
||||
|
||||
val cleanPublishRepo = taskKey[Unit]("Clean the publish repo")
|
||||
cleanPublishRepo := {
|
||||
IO.delete(publishRepoBase.value)
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
// empty plugins file
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.example
|
||||
|
||||
object Lib {
|
||||
def hello: String = "Hello from lib1!"
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# Test ivyless publish to Maven repo (file) - issue #8688
|
||||
# When useIvy is false and publishTo is MavenCache, publish uses ivyless Maven layout
|
||||
|
||||
> cleanPublishRepo
|
||||
> publish
|
||||
> checkMavenPublish
|
||||
Loading…
Reference in New Issue