mirror of https://github.com/sbt/sbt.git
commit
6207378a49
|
|
@ -3603,7 +3603,13 @@ object Classpaths {
|
|||
o %% "librarymanagement-ivy",
|
||||
o %% "util-logging",
|
||||
o %% "util-position",
|
||||
o %% "io"
|
||||
o %% "io",
|
||||
"com.eed3si9n" %% "sjson-new-core",
|
||||
"com.eed3si9n" %% "gigahorse-core",
|
||||
"com.typesafe" %% "ssl-config-core",
|
||||
"org.reactivestreams" % "reactive-streams",
|
||||
"org.slf4j" % "slf4j-api",
|
||||
"com.typesafe" % "config",
|
||||
)
|
||||
if (isMeta && !force) excludes.toVector ++ sbtModulesExcludes
|
||||
else excludes
|
||||
|
|
|
|||
|
|
@ -12,11 +12,13 @@ package internal
|
|||
import java.io.{ File, IOException }
|
||||
import java.net.{ URI, URL }
|
||||
import java.util.concurrent.Callable
|
||||
import java.util.regex.Matcher
|
||||
|
||||
import gigahorse.AuthScheme
|
||||
import gigahorse.support.apachehttp.Gigahorse
|
||||
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 }
|
||||
|
|
@ -560,6 +562,11 @@ 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]
|
||||
|
|
@ -576,8 +583,9 @@ private[sbt] object LibraryManagement {
|
|||
val moduleName = project.module.name.value
|
||||
val version = project.version
|
||||
|
||||
// Base directory: localRepoBase / org / module / version
|
||||
val moduleDir = localRepoBase / org / moduleName / 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")
|
||||
|
||||
|
|
@ -638,6 +646,7 @@ private[sbt] object LibraryManagement {
|
|||
*/
|
||||
private def substituteIvyArtifactPattern(
|
||||
pattern: String,
|
||||
project: CsrProject,
|
||||
org: String,
|
||||
moduleName: String,
|
||||
version: String,
|
||||
|
|
@ -655,8 +664,18 @@ private[sbt] object LibraryManagement {
|
|||
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_[^)]+/\\)", "")
|
||||
// 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
|
||||
}
|
||||
|
|
@ -750,6 +769,7 @@ private[sbt] object LibraryManagement {
|
|||
val artifactName = moduleName
|
||||
val pathPattern = substituteIvyArtifactPattern(
|
||||
artifactPattern,
|
||||
project,
|
||||
org,
|
||||
moduleName,
|
||||
version,
|
||||
|
|
@ -771,6 +791,7 @@ private[sbt] object LibraryManagement {
|
|||
val ivyXmlContent = lmcoursier.IvyXml(project, Nil, Nil)
|
||||
val ivyPathPattern = substituteIvyArtifactPattern(
|
||||
ivyPattern,
|
||||
project,
|
||||
org,
|
||||
moduleName,
|
||||
version,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
ThisBuild / csrCacheDirectory := (ThisBuild / baseDirectory).value / "coursier-cache"
|
||||
|
||||
val publishRepoBase = settingKey[File]("Base directory for publish repo (HTTP server writes here)")
|
||||
|
||||
val publishPort = 3032
|
||||
|
||||
lazy val root = (project in file("."))
|
||||
.enablePlugins(SbtPlugin)
|
||||
.settings(
|
||||
name := "sbt-test-plugin",
|
||||
organization := "com.example",
|
||||
version := "0.1.0-SNAPSHOT",
|
||||
scalaVersion := "2.12.21",
|
||||
pluginCrossBuild / sbtVersion := "1.5.8",
|
||||
publishRepoBase := baseDirectory.value / "repo",
|
||||
publishTo := Some(
|
||||
Resolver
|
||||
.url("test-repo", new java.net.URI(s"http://localhost:$publishPort/").toURL)(using
|
||||
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 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 checkPublished = taskKey[Unit]("Check ivyless publish wrote the plugin cross-suffix PUT path")
|
||||
checkPublished := {
|
||||
val log = streams.value.log
|
||||
val base = publishRepoBase.value
|
||||
val org = organization.value
|
||||
val moduleName = normalizedName.value
|
||||
val ver = version.value
|
||||
|
||||
val moduleDir = base / org / moduleName / "scala_2.12" / "sbt_1.0" / 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)
|
||||
|
||||
assert(moduleDir.isDirectory, s"Expected plugin cross-suffix directory $moduleDir to 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")
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
val nonCrossDir = base / org / moduleName / ver
|
||||
assert(!nonCrossDir.exists, s"Non-cross directory $nonCrossDir should not exist; cross suffixes are missing")
|
||||
|
||||
log.info("All ivyless publish (HTTP plugin) checks passed!")
|
||||
}
|
||||
|
||||
val cleanPublishRepo = taskKey[Unit]("Clean the publish repo")
|
||||
cleanPublishRepo := {
|
||||
IO.delete(publishRepoBase.value)
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import java.io._
|
||||
import java.net.InetSocketAddress
|
||||
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 {
|
||||
in.transferTo(out)
|
||||
} 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
// empty plugins file
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.example
|
||||
|
||||
object Plugin {
|
||||
def hello: String = "Hello from sbt 1 plugin!"
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Test ivyless publish to HTTP Ivy-style repo for an sbt 1 plugin (issue #9383)
|
||||
# When useIvy is false, the Ivy-pattern PUT path must include the
|
||||
# scala_2.12/sbt_1.0 cross suffixes for sbt 1 plugins.
|
||||
|
||||
> cleanPublishRepo
|
||||
> publishToHttp
|
||||
> checkPublished
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
ThisBuild / csrCacheDirectory := (ThisBuild / baseDirectory).value / "coursier-cache"
|
||||
|
||||
val ivyLocalBase = settingKey[File]("Local Ivy repository base")
|
||||
|
||||
lazy val root = (project in file("."))
|
||||
.enablePlugins(SbtPlugin)
|
||||
.settings(
|
||||
name := "sbt-test-plugin",
|
||||
organization := "com.example",
|
||||
version := "0.1.0-SNAPSHOT",
|
||||
scalaVersion := "2.12.21",
|
||||
pluginCrossBuild / sbtVersion := "1.5.8",
|
||||
ivyLocalBase := baseDirectory.value / "local-ivy-repo",
|
||||
ivyPaths := IvyPaths(baseDirectory.value.toString, Some(ivyLocalBase.value.toString)),
|
||||
useIvy := false,
|
||||
Compile / packageDoc / publishArtifact := false,
|
||||
Compile / packageSrc / publishArtifact := false,
|
||||
)
|
||||
|
||||
val cleanLocalIvy = taskKey[Unit]("Clean the local ivy repo")
|
||||
cleanLocalIvy := {
|
||||
IO.delete(ivyLocalBase.value / "local")
|
||||
}
|
||||
|
||||
val checkPublished = taskKey[Unit]("Check ivyless publishLocal wrote the plugin cross-suffix path")
|
||||
checkPublished := {
|
||||
val log = streams.value.log
|
||||
val base = ivyLocalBase.value / "local"
|
||||
val org = organization.value
|
||||
val moduleName = normalizedName.value
|
||||
val ver = version.value
|
||||
|
||||
val moduleDir = base / org / moduleName / "scala_2.12" / "sbt_1.0" / 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 ivyLocalBase:")
|
||||
listDir(ivyLocalBase.value)
|
||||
|
||||
assert(moduleDir.isDirectory, s"Expected plugin cross-suffix directory $moduleDir to 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")
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
val nonCrossDir = base / org / moduleName / ver
|
||||
assert(!nonCrossDir.exists, s"Non-cross directory $nonCrossDir should not exist; cross suffixes are missing")
|
||||
|
||||
log.info("All ivyless publishLocal plugin checks passed!")
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
// empty plugins file
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.example
|
||||
|
||||
object Plugin {
|
||||
def hello: String = "Hello from sbt 1 plugin!"
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Test ivyless publishLocal for an sbt 1 plugin (issue #9383)
|
||||
# When useIvy is false, publishLocal must write artifacts under the
|
||||
# scala_2.12/sbt_1.0 cross-suffix path, matching the Ivy PluginPattern.
|
||||
|
||||
> cleanLocalIvy
|
||||
> publishLocal
|
||||
> checkPublished
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
ThisBuild / scalaVersion := "3.8.4"
|
||||
ThisBuild / organization := "com.example"
|
||||
|
||||
libraryDependencies += "com.typesafe" % "config" % "1.4.5"
|
||||
|
||||
val checkMetabuildClasspath = taskKey[Unit]("Checks the effective metabuild classpath")
|
||||
val checkProjectClasspath = taskKey[Unit]("Checks the project classpath")
|
||||
|
||||
checkMetabuildClasspath := Def.uncached {
|
||||
val buildUnit = loadedBuild.value.units(thisProjectRef.value.build).unit
|
||||
val classpath = buildUnit.plugins.pluginData.dependencyClasspath
|
||||
val managedModules = classpath
|
||||
.flatMap(_.get(Keys.moduleIDStr))
|
||||
.map(Classpaths.moduleIdJsonKeyFormat.read)
|
||||
.map(module => module.organization -> module.name)
|
||||
.toSet
|
||||
val launcherOwnedModules = Set(
|
||||
"com.eed3si9n" -> "sjson-new-core_3",
|
||||
"com.eed3si9n" -> "gigahorse-core_3",
|
||||
"com.typesafe" -> "ssl-config-core_3",
|
||||
"org.reactivestreams" -> "reactive-streams",
|
||||
"org.slf4j" -> "slf4j-api",
|
||||
"com.typesafe" -> "config",
|
||||
)
|
||||
val pluginModules = Set(
|
||||
"ch.epfl.scala" -> "sbt-github-dependency-submission_sbt2_3",
|
||||
"com.eed3si9n" -> "gigahorse-asynchttpclient_3",
|
||||
"com.eed3si9n" -> "shaded-asynchttpclient",
|
||||
)
|
||||
val converter = buildUnit.converter
|
||||
val launcherJars = classpath
|
||||
.filter(_.get(Keys.moduleIDStr).isEmpty)
|
||||
.map(entry => converter.toPath(entry.data).getFileName.toString)
|
||||
val launcherJarNames = Set(
|
||||
"sjson-new-core_3",
|
||||
"gigahorse-core_3",
|
||||
"ssl-config-core_3",
|
||||
"reactive-streams",
|
||||
"slf4j-api",
|
||||
"config",
|
||||
)
|
||||
|
||||
assert(
|
||||
managedModules.intersect(launcherOwnedModules).isEmpty,
|
||||
s"found plugin-managed sbt modules: ${managedModules.intersect(launcherOwnedModules)}",
|
||||
)
|
||||
assert(
|
||||
pluginModules.subsetOf(managedModules),
|
||||
s"missing plugin modules: ${pluginModules.diff(managedModules)}",
|
||||
)
|
||||
assert(
|
||||
launcherJarNames.forall(name => launcherJars.count(_.startsWith(s"$name-")) == 1),
|
||||
s"expected one launcher jar for each sbt module: $launcherJars",
|
||||
)
|
||||
}
|
||||
|
||||
checkProjectClasspath := Def.uncached {
|
||||
val modules = (Compile / externalDependencyClasspath).value
|
||||
.flatMap(_.get(Keys.moduleIDStr))
|
||||
.map(Classpaths.moduleIdJsonKeyFormat.read)
|
||||
assert(
|
||||
modules.exists(module =>
|
||||
module.organization == "com.typesafe" &&
|
||||
module.name == "config" &&
|
||||
module.revision == "1.4.5"
|
||||
),
|
||||
s"missing project dependency: $modules",
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import sbt.*
|
||||
import sbt.Keys.*
|
||||
|
||||
object Example {
|
||||
lazy val settings = Seq(javaOptions += "-Dfoo=bar")
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
addSbtPlugin("ch.epfl.scala" % "sbt-github-dependency-submission" % "3.2.3")
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# Issue 8207: a metabuild plugin must not downgrade sjson-new core.
|
||||
# Loading this build exercises the metabuild compile, which summons
|
||||
# the JsonFormat for String via the caching macro on javaOptions.
|
||||
# When the plugin downgrades sjson-new core on the metabuild
|
||||
# classpath, project load fails with a missing given. A successful
|
||||
# load is the regression assertion.
|
||||
> projects
|
||||
> checkMetabuildClasspath
|
||||
> checkProjectClasspath
|
||||
> githubGenerateSnapshot {}
|
||||
Loading…
Reference in New Issue