From 0d01cc0b107b968b783bef433df1d2de73373a69 Mon Sep 17 00:00:00 2001 From: Dream <42954461+eureka928@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:46:15 -0500 Subject: [PATCH] [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 --- build.sbt | 11 ++---- .../CoursierDependencyResolution.scala | 10 ++---- .../internal/CoursierModuleDescriptor.scala | 8 +++-- .../internal/CoursierModuleSettings.scala | 5 --- .../scala/lmcoursier/internal/Resolvers.scala | 36 ++++++++++++------- main/src/main/scala/sbt/Defaults.scala | 10 +++--- .../main/scala/sbt/TemplateCommandUtil.scala | 18 +++++----- main/src/main/scala/sbt/internal/Load.scala | 24 ++++--------- .../dependency-management/pom-type/build.sbt | 9 +++-- 9 files changed, 58 insertions(+), 73 deletions(-) delete mode 100644 lm-coursier/src/main/scala/lmcoursier/internal/CoursierModuleSettings.scala diff --git a/build.sbt b/build.sbt index c125f8a95..68a8f000a 100644 --- a/build.sbt +++ b/build.sbt @@ -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")) diff --git a/lm-coursier/src/main/scala/lmcoursier/CoursierDependencyResolution.scala b/lm-coursier/src/main/scala/lmcoursier/CoursierDependencyResolution.scala index 13d32316e..95f040de4 100644 --- a/lm-coursier/src/main/scala/lmcoursier/CoursierDependencyResolution.scala +++ b/lm-coursier/src/main/scala/lmcoursier/CoursierDependencyResolution.scala @@ -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 diff --git a/lm-coursier/src/main/scala/lmcoursier/internal/CoursierModuleDescriptor.scala b/lm-coursier/src/main/scala/lmcoursier/internal/CoursierModuleDescriptor.scala index 12e8143e9..bd74d63d7 100644 --- a/lm-coursier/src/main/scala/lmcoursier/internal/CoursierModuleDescriptor.scala +++ b/lm-coursier/src/main/scala/lmcoursier/internal/CoursierModuleDescriptor.scala @@ -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).## } diff --git a/lm-coursier/src/main/scala/lmcoursier/internal/CoursierModuleSettings.scala b/lm-coursier/src/main/scala/lmcoursier/internal/CoursierModuleSettings.scala deleted file mode 100644 index badad3183..000000000 --- a/lm-coursier/src/main/scala/lmcoursier/internal/CoursierModuleSettings.scala +++ /dev/null @@ -1,5 +0,0 @@ -package lmcoursier.internal - -import sbt.librarymanagement.ModuleSettings - -private[lmcoursier] case class CoursierModuleSettings() extends ModuleSettings diff --git a/lm-coursier/src/main/scala/lmcoursier/internal/Resolvers.scala b/lm-coursier/src/main/scala/lmcoursier/internal/Resolvers.scala index 936cef804..d85840f66 100644 --- a/lm-coursier/src/main/scala/lmcoursier/internal/Resolvers.scala +++ b/lm-coursier/src/main/scala/lmcoursier/internal/Resolvers.scala @@ -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 } diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index c517e39cd..e9548b93f 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -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, diff --git a/main/src/main/scala/sbt/TemplateCommandUtil.scala b/main/src/main/scala/sbt/TemplateCommandUtil.scala index 0e8c39513..d1431adf7 100644 --- a/main/src/main/scala/sbt/TemplateCommandUtil.scala +++ b/main/src/main/scala/sbt/TemplateCommandUtil.scala @@ -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) diff --git a/main/src/main/scala/sbt/internal/Load.scala b/main/src/main/scala/sbt/internal/Load.scala index b6fb9335a..235d80a9e 100755 --- a/main/src/main/scala/sbt/internal/Load.scala +++ b/main/src/main/scala/sbt/internal/Load.scala @@ -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) +: diff --git a/sbt-app/src/sbt-test/dependency-management/pom-type/build.sbt b/sbt-app/src/sbt-test/dependency-management/pom-type/build.sbt index 2f327c51a..809d99829 100644 --- a/sbt-app/src/sbt-test/dependency-management/pom-type/build.sbt +++ b/sbt-app/src/sbt-test/dependency-management/pom-type/build.sbt @@ -2,9 +2,9 @@ lazy val checkPom = taskKey[Unit]("check pom to ensure no 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") )