[2.x] refactor: Remove Ivy from update path and decouple lm-coursier from lm-ivy (#8832)

- **Remove `ivyModule` from `updateTask0`**: Replace `IvySbt#Module` with `moduleSettings` + `DependencyResolution.moduleDescriptor()`, eliminating the Ivy dependency in the update path.
- **Replace direct Ivy usage in `Load.scala` and `TemplateCommandUtil`**: Use Coursier's `DependencyResolution` API for plugin bootstrapping and template resolution instead of constructing `IvySbt` instances directly.
- **Break `lm-coursier`'s dependency on `lm-ivy`**: Remove `IvySbt#Module` pattern match from `CoursierDependencyResolution`, replace `IBiblioResolver` usage in `Resolvers` with reflection, and switch build dependencies from `lmIvy` to `lmCore`.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dream 2026-03-02 17:46:15 -05:00 committed by GitHub
parent dfa5e31571
commit 0d01cc0b10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 58 additions and 73 deletions

View File

@ -1239,7 +1239,7 @@ lazy val lmCoursierDefinitions = project
conflictWarning := ConflictWarning.disable,
Utils.noPublish,
)
.dependsOn(lmIvy % "provided")
.dependsOn(lmCore % "provided")
lazy val lmCoursierDependencies = Def.settings(
libraryDependencies ++= Seq(
@ -1265,12 +1265,7 @@ lazy val lmCoursier = project
contrabandSettings,
Compile / sourceGenerators += Utils.dataclassGen(lmCoursierDefinitions).taskValue,
)
.dependsOn(
// We depend on lmIvy rather than just lmCore to handle the ModuleDescriptor
// passed to DependencyResolutionInterface.update, which is an IvySbt#Module
// (seems DependencyResolutionInterface.moduleDescriptor is ignored).
lmIvy
)
.dependsOn(lmCore)
lazy val lmCoursierShaded = project
.in(file("lm-coursier/target/shaded-module"))
@ -1340,7 +1335,7 @@ lazy val lmCoursierShaded = project
oldStrategy(x)
}
)
.dependsOn(lmIvy % "provided")
.dependsOn(lmCore % "provided")
lazy val lmCoursierShadedPublishing = project
.in(file("lm-coursier/target/shaded-publishing-module"))

View File

@ -25,7 +25,6 @@ import lmcoursier.internal.{
UpdateRun
}
import lmcoursier.syntax.*
import sbt.internal.librarymanagement.IvySbt
import sbt.librarymanagement.*
import sbt.util.Logger
import coursier.core.{ BomDependency, Dependency, Publication }
@ -139,15 +138,12 @@ class CoursierDependencyResolution(
val module0 = module match {
case c: CoursierModuleDescriptor =>
// seems not to happen, not sure what DependencyResolutionInterface.moduleDescriptor is for
c.descriptor
case i: IvySbt#Module =>
i.moduleSettings match {
case other =>
other.moduleSettings match {
case d: ModuleDescriptorConfiguration => d
case other => sys.error(s"unrecognized module settings: $other")
case s => sys.error(s"unrecognized module settings: $s")
}
case _ =>
sys.error(s"unrecognized ModuleDescriptor type: $module")
}
val so = conf.scalaOrganization

View File

@ -14,9 +14,11 @@ private[lmcoursier] final case class CoursierModuleDescriptor(
def scalaModuleInfo: Option[ScalaModuleInfo] =
descriptor.scalaModuleInfo
def moduleSettings: CoursierModuleSettings =
CoursierModuleSettings()
def moduleSettings: ModuleDescriptorConfiguration =
descriptor
lazy val extraInputHash: Long =
conf.##
// Exclude log/logger fields they contain Logger instances with
// non-deterministic hashCodes that would break update caching.
conf.withLog(None).withLogger(None).##
}

View File

@ -1,5 +0,0 @@
package lmcoursier.internal
import sbt.librarymanagement.ModuleSettings
private[lmcoursier] case class CoursierModuleSettings() extends ModuleSettings

View File

@ -7,7 +7,6 @@ import coursier.cache.CacheUrl
import coursier.core.{ Authentication, Repository }
import coursier.ivy.IvyRepository
import coursier.maven.SbtMavenRepository
import org.apache.ivy.plugins.resolver.IBiblioResolver
import sbt.librarymanagement.*
import sbt.util.Logger
@ -133,29 +132,42 @@ object Resolvers {
private object IBiblioRepository {
// Use reflection to avoid a compile-time dependency on lm-ivy / Apache Ivy.
// At runtime the class will be present on the classpath via the main module.
private val ibiblioClass: Option[Class[?]] =
try Some(Class.forName("org.apache.ivy.plugins.resolver.IBiblioResolver"))
catch { case _: ClassNotFoundException => None }
private def stringVector(v: java.util.List[?]): Vector[String] =
Option(v).map(_.asScala.toVector).getOrElse(Vector.empty).collect { case s: String =>
s
}
private def patterns(resolver: IBiblioResolver): Patterns = Patterns(
ivyPatterns = stringVector(resolver.getIvyPatterns),
artifactPatterns = stringVector(resolver.getArtifactPatterns),
isMavenCompatible = resolver.isM2compatible,
descriptorOptional = !resolver.isUseMavenMetadata,
skipConsistencyCheck = !resolver.isCheckconsistency
)
private def patternsViaReflection(resolver: AnyRef): Patterns =
val cls = resolver.getClass
Patterns(
ivyPatterns = stringVector(
cls.getMethod("getIvyPatterns").invoke(resolver).asInstanceOf[java.util.List[?]]
),
artifactPatterns = stringVector(
cls.getMethod("getArtifactPatterns").invoke(resolver).asInstanceOf[java.util.List[?]]
),
isMavenCompatible = cls.getMethod("isM2compatible").invoke(resolver).asInstanceOf[Boolean],
descriptorOptional =
!cls.getMethod("isUseMavenMetadata").invoke(resolver).asInstanceOf[Boolean],
skipConsistencyCheck =
!cls.getMethod("isCheckconsistency").invoke(resolver).asInstanceOf[Boolean],
)
def unapply(r: Resolver): Option[Patterns] =
r match {
case raw: RawRepository =>
raw.resolver match {
case b: IBiblioResolver =>
Some(patterns(b))
ibiblioClass match
case Some(cls) if cls.isInstance(raw.resolver) =>
Some(patternsViaReflection(raw.resolver))
.filter(patternMatchGuard)
case _ =>
None
}
case _ =>
None
}

View File

@ -3856,7 +3856,7 @@ object Classpaths {
Option[FiniteDuration],
Boolean,
ProjectRef,
IvySbt#Module,
ModuleSettings,
String,
Boolean,
Seq[UpdateReport],
@ -3886,7 +3886,7 @@ object Classpaths {
forceUpdatePeriod.toTaskable,
sbtPlugin.toTaskable,
thisProjectRef.toTaskable,
ivyModule.toTaskable,
moduleSettings.toTaskable,
scalaOrganization.toTaskable,
(update / skip).toTaskable,
transitiveUpdate.toTaskable,
@ -3916,7 +3916,7 @@ object Classpaths {
fup,
isPlugin,
thisRef,
im,
ms,
so,
sk,
tu,
@ -3979,10 +3979,8 @@ object Classpaths {
else Def.displayRelativeReference(extracted.currentRef, thisRef)
LibraryManagement.cachedUpdate(
// LM API
lm = lm,
// Ivy-free ModuleDescriptor
module = im,
module = lm.moduleDescriptor(ms.asInstanceOf[ModuleDescriptorConfiguration]),
cacheStoreFactory = cacheStoreFactory,
label = label,
updateConf,

View File

@ -14,7 +14,6 @@ import java.io.File
import sbt.io.*, syntax.*
import sbt.util.*
import sbt.internal.librarymanagement.ivy.{ IvyConfiguration, IvyDependencyResolution }
import sbt.internal.util.{ ConsoleAppender, Terminal as ITerminal }
import sbt.internal.util.complete.{ DefaultParsers, Parser }, DefaultParsers.*
import xsbti.AppConfiguration
@ -43,7 +42,7 @@ private[sbt] object TemplateCommandUtil {
val infos = s0.get(templateResolverInfos).getOrElse(Nil).toList
val log = s0.globalLogging.full
val extracted = Project.extract(s0)
val (s1, ivyConf) = extracted.runTask(Keys.ivyConfiguration, s0)
val (s1, lm) = extracted.runTask(Keys.dependencyResolution, s0)
val scalaModuleInfo = extracted.get(Keys.updateSbtClassifiers / Keys.scalaModuleInfo)
val templateDescriptions = extracted.get(Keys.templateDescriptions)
val args0 = inputArg.toList ++
@ -54,7 +53,7 @@ private[sbt] object TemplateCommandUtil {
def terminate = TerminateAction :: s1.copy(remainingCommands = Nil)
def reload = "reboot" :: s1.copy(remainingCommands = Nil)
if (args0.nonEmpty) {
run(infos, args0, s0.configuration, ivyConf, globalBase, scalaModuleInfo, log)
run(infos, args0, s0.configuration, lm, globalBase, scalaModuleInfo, log)
terminate
} else {
fortifyArgs(templateDescriptions.toList) match {
@ -63,7 +62,7 @@ private[sbt] object TemplateCommandUtil {
extracted.runInputTask(Keys.templateRunLocal, " " + arg, s0)
reload
case args =>
run(infos, args, s0.configuration, ivyConf, globalBase, scalaModuleInfo, log)
run(infos, args, s0.configuration, lm, globalBase, scalaModuleInfo, log)
terminate
}
}
@ -73,13 +72,13 @@ private[sbt] object TemplateCommandUtil {
infos: List[TemplateResolverInfo],
arguments: List[String],
config: AppConfiguration,
ivyConf: IvyConfiguration,
lm: DependencyResolution,
globalBase: File,
scalaModuleInfo: Option[ScalaModuleInfo],
log: Logger
): Unit =
infos find { info =>
val loader = infoLoader(info, config, ivyConf, globalBase, scalaModuleInfo, log)
val loader = infoLoader(info, config, lm, globalBase, scalaModuleInfo, log)
val hit = tryTemplate(info, arguments, loader)
if (hit) {
runTemplate(info, arguments, loader)
@ -115,12 +114,12 @@ private[sbt] object TemplateCommandUtil {
private def infoLoader(
info: TemplateResolverInfo,
config: AppConfiguration,
ivyConf: IvyConfiguration,
lm: DependencyResolution,
globalBase: File,
scalaModuleInfo: Option[ScalaModuleInfo],
log: Logger
): ClassLoader = {
val cp = classpathForInfo(info, ivyConf, globalBase, scalaModuleInfo, log)
val cp = classpathForInfo(info, lm, globalBase, scalaModuleInfo, log)
ClasspathUtil.toLoader(cp, config.provider.loader)
}
@ -146,12 +145,11 @@ private[sbt] object TemplateCommandUtil {
// Cache files under ~/.sbt/sbt_version/templates/org_name_version
private def classpathForInfo(
info: TemplateResolverInfo,
ivyConf: IvyConfiguration,
lm: DependencyResolution,
globalBase: File,
scalaModuleInfo: Option[ScalaModuleInfo],
log: Logger
): List[Path] = {
val lm = IvyDependencyResolution(ivyConf)
val templatesBaseDirectory = new File(globalBase, "templates")
val templateId = s"${info.module.organization}_${info.module.name}_${info.module.revision}"
val templateDirectory = new File(templatesBaseDirectory, templateId)

View File

@ -20,14 +20,15 @@ import sbt.SlashSyntax0.*
import sbt.internal.BuildStreams.*
import sbt.internal.inc.classpath.ClasspathUtil
import sbt.internal.inc.{ MappedFileConverter, ScalaInstance, ZincLmUtil, ZincUtil }
import sbt.internal.librarymanagement.ivy.{ InlineIvyConfiguration, IvyDependencyResolution }
import lmcoursier.{ CoursierConfiguration, CoursierDependencyResolution }
import lmcoursier.syntax.*
import sbt.internal.util.Attributed.data
import sbt.internal.util.Types.const
import sbt.internal.util.Attributed
import sbt.internal.util.appmacro.ContextUtil
import sbt.internal.server.BuildServerEvalReporter
import sbt.io.{ GlobFilter, IO }
import sbt.librarymanagement.{ Configuration, Configurations, IvyPaths, Resolver, ScalaArtifacts }
import sbt.librarymanagement.{ Configuration, Configurations, Resolver, ScalaArtifacts }
import sbt.nio.Settings
import sbt.util.{ Logger, Show }
import xsbti.{ FileConverter, HashedVirtualFileRef, VirtualFile }
@ -87,14 +88,10 @@ private[sbt] object Load {
val classpath = Attributed.blankSeq(
cp0.map(_.toPath).map(p => converter.toVirtualFile(p): HashedVirtualFileRef)
)
val ivyConfiguration =
InlineIvyConfiguration()
.withPaths(
IvyPaths(baseDirectory.toString, bootIvyHome(state.configuration).map(_.toString))
)
.withResolvers(Resolver.combineDefaultResolvers(Vector.empty))
.withLog(log)
val dependencyResolution = IvyDependencyResolution(ivyConfiguration)
val csrConfig = CoursierConfiguration()
.withResolvers(Resolver.combineDefaultResolvers(Vector.empty).toVector)
.withLog(log)
val dependencyResolution = CoursierDependencyResolution(csrConfig)
val si = ScalaInstance(scalaProvider.version, scalaProvider.launcher)
val zincDir = BuildPaths.getZincDirectory(state, globalBase)
val classpathOptions = ClasspathOptionsUtil.noboot(si.version)
@ -140,13 +137,6 @@ private[sbt] object Load {
)
}
private def bootIvyHome(app: xsbti.AppConfiguration): Option[File] =
try {
Option(app.provider.scalaProvider.launcher.ivyHome)
} catch {
case _: NoSuchMethodError => None
}
def injectGlobal(state: State): Seq[Setting[?]] =
((GlobalScope / appConfiguration) :== state.configuration) +:
LogManager.settingsLogger(state) +:

View File

@ -2,9 +2,9 @@ lazy val checkPom = taskKey[Unit]("check pom to ensure no <type> sections are ge
lazy val root = (project in file(".")).
settings(
scalaVersion := "2.10.6",
libraryDependencies += { ("org.scala-tools.sbinary" %% "sbinary" % "0.4.1").withSources().withJavadoc() },
libraryDependencies += { ("org.scala-sbt" % "io" % "0.13.8").intransitive() },
scalaVersion := "2.13.16",
libraryDependencies += { ("com.typesafe" % "config" % "1.4.3").withSources().withJavadoc() },
libraryDependencies += { ("org.slf4j" % "slf4j-api" % "2.0.16").intransitive() },
checkPom := {
val converter = fileConverter.value
val pomFile = makePom.value
@ -16,8 +16,7 @@ lazy val root = (project in file(".")).
val ur = update.value
val dir = (update / streams).value.cacheDirectory / "out"
val lines = IO.readLines(dir)
val hasError = lines exists { line => line contains "Found intransitive dependency "}
val hasError = lines.exists(line => line.contains("Found intransitive dependency "))
assert(hasError, s"Failed to detect intransitive dependencies, got: ${lines.mkString("\n")}")
},
resolvers += Resolver.typesafeIvyRepo("releases")
)