From b460bb871e3dd40804f87eed58d9c32f75ed3a2f Mon Sep 17 00:00:00 2001 From: bitloi <89318445+bitloi@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:20:38 -0500 Subject: [PATCH] [2.x] feat: Ivyless publish to Ivy repo (#8686) Implements ivyless publish task as part of #7639 (drop Ivy dependency). Fixes #8639. - Add ivylessPublish for URLRepository: HTTP PUT with optional Basic auth, same layout as ivyless publishLocal (artifacts + ivy.xml + checksums). - Add ivylessPublishToFile for FileRepository: write to local path for testing without HTTP server. - Add ivylessPublishTask: when useIvy is false, use ivyless path for URLRepository or FileRepository; otherwise use Ivy. - Wire publish in Defaults to LibraryManagement.ivylessPublishTask (tagged Publish, Network). - Add scripted test dependency-management/ivyless-publish using Resolver.file to verify ivyless publish produces identical layout. Credentials supported via allCredentials (Basic auth for PUT). --- main/src/main/scala/sbt/Defaults.scala | 2 +- .../sbt/internal/LibraryManagement.scala | 291 +++++++++++++++++- .../ivyless-publish-http/build.sbt | 95 ++++++ .../project/HttpPutServer.scala | 49 +++ .../ivyless-publish-http/project/plugins.sbt | 1 + .../src/main/scala/Lib.scala | 5 + .../ivyless-publish-http/test | 7 + .../ivyless-publish/build.sbt | 70 +++++ .../ivyless-publish/project/plugins.sbt | 1 + .../ivyless-publish/src/main/scala/Lib.scala | 5 + .../ivyless-publish/test | 7 + 11 files changed, 519 insertions(+), 14 deletions(-) create mode 100644 sbt-app/src/sbt-test/dependency-management/ivyless-publish-http/build.sbt create mode 100644 sbt-app/src/sbt-test/dependency-management/ivyless-publish-http/project/HttpPutServer.scala create mode 100644 sbt-app/src/sbt-test/dependency-management/ivyless-publish-http/project/plugins.sbt create mode 100644 sbt-app/src/sbt-test/dependency-management/ivyless-publish-http/src/main/scala/Lib.scala create mode 100644 sbt-app/src/sbt-test/dependency-management/ivyless-publish-http/test create mode 100644 sbt-app/src/sbt-test/dependency-management/ivyless-publish/build.sbt create mode 100644 sbt-app/src/sbt-test/dependency-management/ivyless-publish/project/plugins.sbt create mode 100644 sbt-app/src/sbt-test/dependency-management/ivyless-publish/src/main/scala/Lib.scala create mode 100644 sbt-app/src/sbt-test/dependency-management/ivyless-publish/test diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index ca4e2d967..5b4035094 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -2991,7 +2991,7 @@ object Classpaths { .map(m => d.withRevision(m.module.revision)) }.distinct }.value, - publish := publishOrSkip(publishConfiguration, publish / skip).value, + publish := LibraryManagement.ivylessPublishTask.tag(Tags.Publish, Tags.Network).value, publishLocal := LibraryManagement.ivylessPublishLocalTask.value, publishM2 := publishOrSkip(publishM2Configuration, publishM2 / skip).value, credentials ++= Def.uncached { diff --git a/main/src/main/scala/sbt/internal/LibraryManagement.scala b/main/src/main/scala/sbt/internal/LibraryManagement.scala index 2394fed67..fe82db2ac 100644 --- a/main/src/main/scala/sbt/internal/LibraryManagement.scala +++ b/main/src/main/scala/sbt/internal/LibraryManagement.scala @@ -9,9 +9,12 @@ package sbt package internal -import java.io.File +import java.io.{ File, IOException } +import java.net.{ URI, URL } import java.util.concurrent.Callable +import gigahorse.{ AuthScheme } +import gigahorse.support.apachehttp.Gigahorse import sbt.Def.ScopedKey import sbt.internal.librarymanagement.* import sbt.librarymanagement.* @@ -21,7 +24,8 @@ import sbt.io.IO import sbt.io.syntax.* import sbt.ProjectExtra.* import sjsonnew.JsonFormat -import scala.concurrent.duration.FiniteDuration +import scala.concurrent.* +import scala.concurrent.duration.* import lmcoursier.definitions.Project as CsrProject private[sbt] object LibraryManagement { @@ -588,6 +592,17 @@ private[sbt] object LibraryManagement { 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)") + // Publish each artifact artifacts.foreach: (artifact, sourceFile) => val folder = typeToFolder(artifact.`type`) @@ -604,19 +619,269 @@ private[sbt] object LibraryManagement { 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 + /** + * 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])", "") + // Remove optional Ivy pattern parts (scala/sbt version, branch) for ivyless layout + s = s.replaceAll("\\(scala_[^)]+/\\)", "").replaceAll("\\(sbt_[^)]+/\\)", "") + s = s.replaceAll("\\(\\[branch\\]/\\)", "") + s + } + + /** + * 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.filter(_.host == url.getHost) 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 + 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) + }.toVector + + 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, directCreds.find(_.host == url.getHost), 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) + 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, directCreds.find(_.host == ivyUrl.getHost), 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) + 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. + */ + 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 + log.info(s"Ivyless publish 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 + 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.map { case (a, f) => (a, f) } + 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 + log.info(s"Ivyless publish to file repo: $repoDir") + ivylessPublishLocal( + project, + artifacts, + config.checksums, + repoDir, + config.overwrite, + log + ) + case _ => + log.warn( + "Ivyless publish only supports URLRepository (Resolver.url) or FileRepository (Resolver.file). Falling back to Ivy." + ) + val conf = publishConfiguration.value + val module = ivyModule.value + val publisherInterface = publisher.value + publisherInterface.publish(module, conf, log) + } + } + ) + ) + /** * Task initializer for ivyless publishLocal. * Uses Def.ifS for proper selective functor behavior. diff --git a/sbt-app/src/sbt-test/dependency-management/ivyless-publish-http/build.sbt b/sbt-app/src/sbt-test/dependency-management/ivyless-publish-http/build.sbt new file mode 100644 index 000000000..7a494ce2c --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/ivyless-publish-http/build.sbt @@ -0,0 +1,95 @@ +ThisBuild / csrCacheDirectory := (ThisBuild / baseDirectory).value / "coursier-cache" + +name := "lib1" +organization := "com.example" +version := "0.1.0-SNAPSHOT" +scalaVersion := "3.8.1" + +val publishRepoBase = settingKey[File]("Base directory for publish repo (HTTP server writes here)") +publishRepoBase := baseDirectory.value / "repo" + +val publishPort = 3030 + +// Publish to HTTP server (localhost) - ivyless publish uses PUT +// Resolver.url expects java.net.URL; in build.sbt "url" is sbt.URI, so use java.net.URL explicitly +publishTo := Some( + Resolver.url("test-repo", new java.net.URL(s"http://localhost:$publishPort/"))(Resolver.ivyStylePatterns) + .withAllowInsecureProtocol(true) +) + +useIvy := false + +Compile / packageDoc / publishArtifact := false +Compile / packageSrc / publishArtifact := false + +val startPublishServer = taskKey[Unit]("Start HTTP server that accepts PUT to repo directory") +startPublishServer := { + HttpPutServer.start(publishPort, publishRepoBase.value) + streams.value.log.info(s"HTTP PUT server started on port $publishPort, writing to ${publishRepoBase.value}") +} + +val stopPublishServer = taskKey[Unit]("Stop HTTP server") +stopPublishServer := { + HttpPutServer.stop() + streams.value.log.info("HTTP PUT server stopped") +} + +val publishToHttp = taskKey[Unit]("Publish to HTTP server (start server, publish, stop server)") +publishToHttp := { + startPublishServer.value + try publish.value + finally stopPublishServer.value +} + +val checkIvylessPublish = taskKey[Unit]("Check that ivyless publish produced the expected files") +checkIvylessPublish := { + val log = streams.value.log + val base = publishRepoBase.value + val org = organization.value + val moduleName = normalizedName.value + "_3" + val ver = version.value + val moduleDir = base / org / moduleName / ver + + log.info(s"Checking published files in $moduleDir") + + def listDir(dir: File, indent: String = ""): Unit = { + if (dir.exists) { + dir.listFiles.foreach { f => + log.info(s"$indent${f.getName}") + if (f.isDirectory) listDir(f, indent + " ") + } + } else { + log.info(s"${indent}Directory does not exist: $dir") + } + } + log.info("Contents of publish repo:") + listDir(base) + + val expectedDirs = Seq("jars", "poms", "ivys") + expectedDirs.foreach { dir => + val d = moduleDir / dir + assert(d.exists && d.isDirectory, s"Expected directory $d to exist") + } + + val jarFile = moduleDir / "jars" / s"$moduleName.jar" + assert(jarFile.exists, s"Expected $jarFile to exist") + assert((moduleDir / "jars" / s"$moduleName.jar.md5").exists, s"Expected md5 checksum to exist") + assert((moduleDir / "jars" / s"$moduleName.jar.sha1").exists, s"Expected sha1 checksum to exist") + + val ivyFile = moduleDir / "ivys" / "ivy.xml" + assert(ivyFile.exists, s"Expected $ivyFile to exist") + assert((moduleDir / "ivys" / "ivy.xml.md5").exists, s"Expected ivy.xml md5 checksum to exist") + assert((moduleDir / "ivys" / "ivy.xml.sha1").exists, s"Expected ivy.xml sha1 checksum to exist") + + val ivyContent = IO.read(ivyFile) + assert(ivyContent.contains(s"""organisation="$org""""), s"ivy.xml should contain organisation") + assert(ivyContent.contains(s"""module="$moduleName""""), s"ivy.xml should contain module name") + assert(ivyContent.contains(s"""revision="$ver""""), s"ivy.xml should contain revision") + + log.info("All ivyless publish (HTTP) checks passed!") +} + +val cleanPublishRepo = taskKey[Unit]("Clean the publish repo") +cleanPublishRepo := { + IO.delete(publishRepoBase.value) +} diff --git a/sbt-app/src/sbt-test/dependency-management/ivyless-publish-http/project/HttpPutServer.scala b/sbt-app/src/sbt-test/dependency-management/ivyless-publish-http/project/HttpPutServer.scala new file mode 100644 index 000000000..439ce62b7 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/ivyless-publish-http/project/HttpPutServer.scala @@ -0,0 +1,49 @@ +import java.io._ +import java.net.InetSocketAddress +import java.nio.file.{ Files, Paths } +import com.sun.net.httpserver.{ HttpExchange, HttpHandler, HttpServer } + +/** Minimal HTTP server that accepts PUT and writes to a base directory (for ivyless publish scripted test). */ +object HttpPutServer { + private var server: HttpServer = null + + def start(port: Int, baseDir: java.io.File): Unit = { + if (server != null) stop() + server = HttpServer.create(new InetSocketAddress(port), 0) + server.createContext("/", new HttpHandler { + override def handle(ex: HttpExchange): Unit = { + val method = ex.getRequestMethod + if ("PUT".equalsIgnoreCase(method)) { + val path = ex.getRequestURI.getRawPath + val relativePath = if (path.startsWith("/")) path.substring(1) else path + val targetFile = new File(baseDir, relativePath.replace("/", File.separator)) + targetFile.getParentFile.mkdirs() + val in = ex.getRequestBody + val out = new FileOutputStream(targetFile) + try { + 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 { + ex.sendResponseHeaders(405, -1) + ex.close() + } + } + }) + server.setExecutor(null) + server.start() + } + + def stop(): Unit = { + if (server != null) { + server.stop(0) + server = null + } + } +} diff --git a/sbt-app/src/sbt-test/dependency-management/ivyless-publish-http/project/plugins.sbt b/sbt-app/src/sbt-test/dependency-management/ivyless-publish-http/project/plugins.sbt new file mode 100644 index 000000000..adc5e970b --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/ivyless-publish-http/project/plugins.sbt @@ -0,0 +1 @@ +// empty plugins file diff --git a/sbt-app/src/sbt-test/dependency-management/ivyless-publish-http/src/main/scala/Lib.scala b/sbt-app/src/sbt-test/dependency-management/ivyless-publish-http/src/main/scala/Lib.scala new file mode 100644 index 000000000..798d21706 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/ivyless-publish-http/src/main/scala/Lib.scala @@ -0,0 +1,5 @@ +package com.example + +object Lib { + def hello: String = "Hello from lib1!" +} diff --git a/sbt-app/src/sbt-test/dependency-management/ivyless-publish-http/test b/sbt-app/src/sbt-test/dependency-management/ivyless-publish-http/test new file mode 100644 index 000000000..e761a95bd --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/ivyless-publish-http/test @@ -0,0 +1,7 @@ +# Test ivyless publish to HTTP server (issue #8639) +# HTTP server accepts PUT to repo directory; publish uses ivyless publisher +# and produces the same layout as ivyless publishLocal + +> cleanPublishRepo +> publishToHttp +> checkIvylessPublish diff --git a/sbt-app/src/sbt-test/dependency-management/ivyless-publish/build.sbt b/sbt-app/src/sbt-test/dependency-management/ivyless-publish/build.sbt new file mode 100644 index 000000000..b9d305605 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/ivyless-publish/build.sbt @@ -0,0 +1,70 @@ +ThisBuild / csrCacheDirectory := (ThisBuild / baseDirectory).value / "coursier-cache" + +name := "lib1" +organization := "com.example" +version := "0.1.0-SNAPSHOT" +scalaVersion := "3.8.1" + +// Publish to a file repo (tests ivyless publish without HTTP server) +val publishRepoBase = settingKey[File]("Base directory for publish repo") +publishRepoBase := baseDirectory.value / "repo" + +publishTo := Some(Resolver.file("test-repo", publishRepoBase.value)(Resolver.ivyStylePatterns)) + +useIvy := false + +Compile / packageDoc / publishArtifact := false +Compile / packageSrc / publishArtifact := false + +val checkIvylessPublish = taskKey[Unit]("Check that ivyless publish produced the expected files") +checkIvylessPublish := { + val log = streams.value.log + val base = publishRepoBase.value + val org = organization.value + val moduleName = normalizedName.value + "_3" + val ver = version.value + val moduleDir = base / org / moduleName / ver + + log.info(s"Checking published files in $moduleDir") + + def listDir(dir: File, indent: String = ""): Unit = { + if (dir.exists) { + dir.listFiles.foreach { f => + log.info(s"$indent${f.getName}") + if (f.isDirectory) listDir(f, indent + " ") + } + } else { + log.info(s"${indent}Directory does not exist: $dir") + } + } + log.info("Contents of publish repo:") + listDir(base) + + val expectedDirs = Seq("jars", "poms", "ivys") + expectedDirs.foreach { dir => + val d = moduleDir / dir + assert(d.exists && d.isDirectory, s"Expected directory $d to exist") + } + + val jarFile = moduleDir / "jars" / s"$moduleName.jar" + assert(jarFile.exists, s"Expected $jarFile to exist") + assert((moduleDir / "jars" / s"$moduleName.jar.md5").exists, s"Expected md5 checksum to exist") + assert((moduleDir / "jars" / s"$moduleName.jar.sha1").exists, s"Expected sha1 checksum to exist") + + val ivyFile = moduleDir / "ivys" / "ivy.xml" + assert(ivyFile.exists, s"Expected $ivyFile to exist") + assert((moduleDir / "ivys" / "ivy.xml.md5").exists, s"Expected ivy.xml md5 checksum to exist") + assert((moduleDir / "ivys" / "ivy.xml.sha1").exists, s"Expected ivy.xml sha1 checksum to exist") + + val ivyContent = IO.read(ivyFile) + assert(ivyContent.contains(s"""organisation="$org""""), s"ivy.xml should contain organisation") + assert(ivyContent.contains(s"""module="$moduleName""""), s"ivy.xml should contain module name") + assert(ivyContent.contains(s"""revision="$ver""""), s"ivy.xml should contain revision") + + log.info("All ivyless publish checks passed!") +} + +val cleanPublishRepo = taskKey[Unit]("Clean the publish repo") +cleanPublishRepo := { + IO.delete(publishRepoBase.value) +} diff --git a/sbt-app/src/sbt-test/dependency-management/ivyless-publish/project/plugins.sbt b/sbt-app/src/sbt-test/dependency-management/ivyless-publish/project/plugins.sbt new file mode 100644 index 000000000..adc5e970b --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/ivyless-publish/project/plugins.sbt @@ -0,0 +1 @@ +// empty plugins file diff --git a/sbt-app/src/sbt-test/dependency-management/ivyless-publish/src/main/scala/Lib.scala b/sbt-app/src/sbt-test/dependency-management/ivyless-publish/src/main/scala/Lib.scala new file mode 100644 index 000000000..798d21706 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/ivyless-publish/src/main/scala/Lib.scala @@ -0,0 +1,5 @@ +package com.example + +object Lib { + def hello: String = "Hello from lib1!" +} diff --git a/sbt-app/src/sbt-test/dependency-management/ivyless-publish/test b/sbt-app/src/sbt-test/dependency-management/ivyless-publish/test new file mode 100644 index 000000000..584742c86 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/ivyless-publish/test @@ -0,0 +1,7 @@ +# Test ivyless publish (remote/file repo) +# When useIvy is false and publishTo is Resolver.file, publish uses ivyless publisher +# and produces the same layout as ivyless publishLocal + +> cleanPublishRepo +> publish +> checkIvylessPublish