[2.x] refactor: Extract lm-ivy to separate sbt-ivy plugin module (#8873)

Step 5 of #7640 — removes the compile-time dependency on lm-ivy from main/ by creating a standalone sbt-ivy plugin module.

- Create new sbt-ivy/ subproject with IvyDependencyPlugin AutoPlugin that provides all Ivy-specific functionality (ivySbt, ivyModule, ivyConfiguration, publisher, projectDescriptors, deliver/makeIvyXml)
- Move IvyXml.scala from main/ to sbt-ivy/
This commit is contained in:
Dream 2026-03-10 23:30:06 -04:00 committed by GitHub
parent 4f4bc374f6
commit d71fe5b7a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 818 additions and 288 deletions

View File

@ -156,6 +156,7 @@ jobs:
./sbt -v --client "scripted dependency-management/* project-load/* project-matrix/* java/* run/*"
./sbt -v --client "scripted plugins/*"
./sbt -v --client "scripted nio/*"
./sbt -v --client "scripted ivy/*"
- name: Build and test (4)
if: ${{ matrix.jobtype == 4 }}
shell: bash

View File

@ -690,7 +690,7 @@ lazy val buildFileProj = (project in file("buildfile"))
libraryDependencies ++= Seq(scalaCompiler),
mimaSettings,
)
.dependsOn(lmCore, lmIvy)
.dependsOn(lmCore)
.configure(addSbtIO, addSbtCompilerInterface, addSbtZincCompileCore)
// The main integration project for sbt. It brings all of the projects together, configures them, and provides for overriding conventions.
@ -734,11 +734,30 @@ lazy val mainProj = (project in file("main"))
Compile / doc / sources := Nil,
mimaSettings,
mimaBinaryIssueFilters ++= Vector(
// Moved to sbt-ivy module (Step 5 of sbt#7640)
exclude[DirectMissingMethodProblem]("sbt.Classpaths.depMap"),
exclude[DirectMissingMethodProblem]("sbt.Classpaths.ivySbt0"),
exclude[DirectMissingMethodProblem]("sbt.Classpaths.mkIvyConfiguration"),
exclude[MissingClassProblem]("sbt.internal.librarymanagement.IvyXml"),
exclude[MissingClassProblem]("sbt.internal.librarymanagement.IvyXml$"),
),
)
.dependsOn(lmCore, lmIvy, lmCoursierShadedPublishing)
.dependsOn(lmCore, lmCoursierShadedPublishing)
.configure(addSbtIO, addSbtCompilerInterface, addSbtZincCompileCore)
lazy val sbtIvyProj = (project in file("sbt-ivy"))
.dependsOn(sbtProj, lmIvy)
.settings(
testedBaseSettings,
name := "sbt-ivy",
sbtPlugin := true,
pluginCrossBuild / sbtVersion := version.value,
// TODO: Fix doc
Compile / doc / sources := Nil,
mimaPreviousArtifacts := Set.empty, // new module, no previous artifacts
)
.configure(addSbtIO)
// Strictly for bringing implicits and aliases from subsystems into the top-level sbt namespace through a single package object
// technically, we need a dependency on all of mainProj's dependencies, but we don't do that since this is strictly an integration project
// with the sole purpose of providing certain identifiers without qualification (with a package object)
@ -993,6 +1012,7 @@ def allProjects =
mainSettingsProj,
zincLmIntegrationProj,
mainProj,
sbtIvyProj,
sbtProj,
bundledLauncherProj,
sbtClientProj,

View File

@ -2,6 +2,7 @@ package sbt.internal.librarymanagement
import java.io.File
import sbt.librarymanagement.*
import sbt.librarymanagement.syntax.*
object UpdateClassifiersUtil {
@ -76,4 +77,26 @@ object UpdateClassifiersUtil {
.withExplicitArtifacts(arts)
.withInclusions(Vector(InclExclRule.everything))
def extractExcludes(report: UpdateReport): Map[ModuleID, Set[String]] =
report.allMissing
.flatMap { case (_, mod, art) =>
art.classifier.map { c =>
(restrictedCopy(mod, false), c)
}
}
.groupBy(_._1)
.map { (mod, pairs) => (mod, pairs.map(_._2).toSet) }
def addExcluded(
report: UpdateReport,
classifiers: Vector[String],
exclude: Map[ModuleID, Set[String]]
): UpdateReport =
report.addMissing { id =>
classifiedArtifacts(id.name, classifiers.filter(getExcluded(id, exclude)))
}
private def getExcluded(id: ModuleID, exclude: Map[ModuleID, Set[String]]): Set[String] =
exclude.getOrElse(restrictedCopy(id, false), Set.empty[String])
}

View File

@ -0,0 +1,63 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
package librarymanagement
import java.io.File
import sbt.io.IO
/** Credential-loading utilities that work with sbt.librarymanagement.Credentials without Ivy. */
object CredentialUtils:
def forHost(sc: Seq[Credentials], host: String): Option[Credentials.DirectCredentials] =
allDirect(sc).find(_.host == host)
def allDirect(sc: Seq[Credentials]): Seq[Credentials.DirectCredentials] =
sc.map(toDirect)
def toDirect(c: Credentials): Credentials.DirectCredentials = c match
case dc: Credentials.DirectCredentials => dc
case fc: Credentials.FileCredentials =>
loadCredentials(fc.path) match
case Left(err) => sys.error(err)
case Right(dc) => dc
def loadCredentials(path: File): Either[String, Credentials.DirectCredentials] =
if !path.exists then Left("Credentials file " + path + " does not exist")
else
val props = read(path)
for
host <- lookup(props, HostKeys, path)
user <- lookup(props, UserKeys, path)
pass <- lookup(props, PasswordKeys, path)
yield
val realm = props.keysIterator.find(RealmKeys.contains).flatMap(props.get).orNull
new Credentials.DirectCredentials(realm, host, user, pass)
private val RealmKeys = Set("realm")
private val HostKeys = List("host", "hostname")
private val UserKeys = List("user", "user.name", "username")
private val PasswordKeys = List("password", "pwd", "pass", "passwd")
private def lookup(
props: Map[String, String],
keys: List[String],
path: File
): Either[String, String] =
keys
.flatMap(props.get)
.headOption
.toRight(s"${keys.head} not specified in credentials file: $path")
import scala.jdk.CollectionConverters.*
private def read(from: File): Map[String, String] =
val properties = new java.util.Properties
IO.load(properties, from)
properties.asScala.map((k, v) => (k, v.trim)).toMap
end CredentialUtils

View File

@ -14,8 +14,6 @@ import java.util.{ Optional, UUID }
import java.util.concurrent.TimeUnit
import lmcoursier.CoursierDependencyResolution
import lmcoursier.definitions.{ Configuration as CConfiguration }
import org.apache.ivy.core.module.descriptor.ModuleDescriptor
import org.apache.ivy.core.module.id.ModuleRevisionId
import org.scalasbt.ipcsocket.Win32SecurityLevel
import sbt.Def.{ Initialize, ScopedKey, Setting, SettingsDefinition, parsed }
import sbt.Keys.*
@ -33,7 +31,6 @@ import sbt.internal.classpath.AlternativeZincUtil
import sbt.internal.inc.JavaInterfaceUtil.*
import sbt.internal.inc.classpath.ClasspathFilter
import sbt.internal.inc.{ CompileOutput, MappedFileConverter, Stamps, ZincLmUtil, ZincUtil }
import sbt.internal.librarymanagement.ivy.*
import sbt.internal.librarymanagement.mavenint.{
PomExtraDependencyAttributes,
SbtPomExtraProperties
@ -281,7 +278,7 @@ object Defaults extends BuildCommon {
pomPostProcess :== idFun,
pomAllRepositories :== false,
pomIncludeRepository :== Classpaths.defaultRepositoryFilter,
updateOptions := UpdateOptions(),
updateOptions := None, // overridden by IvyDependencyPlugin with UpdateOptions()
forceUpdatePeriod :== None,
platform :== Platform.jvm,
// coursier settings
@ -2851,10 +2848,10 @@ object Classpaths {
val nameWithCross = crossVersion(artifact.value.name)
val version = Keys.version.value
val pomFile = config.file.get.getParentFile / s"$nameWithCross-$version.pom"
val publisher = Keys.publisher.value
val ivySbt = Keys.ivySbt.value
val module = new ivySbt.Module(moduleSettings.value, appendSbtCrossVersion = true)
publisher.makePomFile(module, config.withFile(pomFile), streams.value.log)
val pub = Keys.publisher.value
val module =
pub.moduleDescriptor(moduleSettings.value.asInstanceOf[ModuleDescriptorConfiguration])
pub.makePomFile(module, config.withFile(pomFile), streams.value.log)
converter.toVirtualFile(pomFile.toPath)
def ivyPublishSettings: Seq[Setting[?]] = publishGlobalDefaults ++ Seq(
@ -2863,8 +2860,10 @@ object Classpaths {
makePom := Def.uncached {
val converter = fileConverter.value
val config = makePomConfiguration.value
val publisher = Keys.publisher.value
publisher.makePomFile(ivyModule.value, config, streams.value.log)
val pub = Keys.publisher.value
val module =
pub.moduleDescriptor(moduleSettings.value.asInstanceOf[ModuleDescriptorConfiguration])
pub.makePomFile(module, config, streams.value.log)
converter.toVirtualFile(config.file.get.toPath())
},
(makePom / packagedArtifact) := Def.uncached((makePom / artifact).value -> makePom.value),
@ -3135,7 +3134,7 @@ object Classpaths {
makePom / artifact := Artifact.pom(moduleName.value),
projectID := defaultProjectID.value,
projectID := pluginProjectID.value,
projectDescriptors := Def.uncached(depMap.value),
projectDescriptors := Def.uncached(Map.empty[Any, Any]),
updateConfiguration := {
// Tell the UpdateConfiguration which artifact types are special (for sources and javadocs)
val specialArtifactTypes = sourceArtifactTypes.value.toSet union docArtifactTypes.value.toSet
@ -3158,8 +3157,7 @@ object Classpaths {
else None
},
dependencyResolution := Def.uncached(dependencyResolutionTask.value),
publisher := Def.uncached(IvyPublisher(ivyConfiguration.value)),
ivyConfiguration := Def.uncached(mkIvyConfiguration.value),
ivyConfiguration := Def.uncached((): Any),
ivyConfigurations := {
val confs = thisProject.value.configurations
(confs ++ confs.map(internalConfigurationMap.value) ++ (if (autoCompilerPlugins.value)
@ -3302,8 +3300,11 @@ object Classpaths {
overwrite = isSnapshot.value
)
},
ivySbt := Def.uncached(ivySbt0.value),
ivyModule := Def.uncached { val is = ivySbt.value; new is.Module(moduleSettings.value) },
ivySbt := Def.uncached((): Any),
ivyModule := Def.uncached((): Any),
publisher := Def.uncached(
Classpaths.defaultPublisher(dependencyResolution.value, fullResolvers.value.toVector)
),
allCredentials := Def.uncached(LMCoursier.allCredentialsTask.value),
transitiveUpdate := Def.uncached(transitiveUpdateTask.value),
updateCacheName := {
@ -3380,7 +3381,6 @@ object Classpaths {
CoursierInputsTasks.coursierFallbackDependenciesTask.value
),
) ++
IvyXml.generateIvyXmlSettings() ++
LMCoursier.publicationsSetting(Seq(Compile, Test).map(c => c -> CConfiguration(c.name)))
def jvmBaseSettings: Seq[Setting[?]] = Seq(
@ -3496,11 +3496,6 @@ object Classpaths {
)
else projectID.value
}
private[sbt] lazy val ivySbt0: Initialize[Task[IvySbt]] =
Def.task {
IvyCredentials.register(credentials.value, streams.value.log)
new IvySbt(ivyConfiguration.value)
}
def moduleSettings0: Initialize[Task[ModuleSettings]] = Def.task {
val deps = allDependencies.value.toVector
errorInsecureProtocolInModules(deps, streams.value.log)
@ -3536,21 +3531,6 @@ object Classpaths {
.resolvers
explicit orElse boot getOrElse externalResolvers.value
},
ivyConfiguration := Def.uncached(
InlineIvyConfiguration(
lock = Option(lock(appConfiguration.value)),
log = Option(streams.value.log),
updateOptions = UpdateOptions(),
paths = Option(ivyPaths.value),
resolvers = externalResolvers.value.toVector,
otherResolvers = Vector.empty,
moduleConfigurations = Vector.empty,
checksums = checksums.value.toVector,
managedChecksums = false,
resolutionCacheDir = Some(target.value / "resolution-cache"),
)
),
ivySbt := Def.uncached(ivySbt0.value),
classifiersModule := Def.uncached(classifiersModuleTask.value),
// Redefine scalaVersion and scalaBinaryVersion specifically for the dependency graph used for updateSbtClassifiers task.
// to fix https://github.com/sbt/sbt/issues/2686
@ -3583,7 +3563,6 @@ object Classpaths {
.task {
val lm = dependencyResolution.value
val s = streams.value
val is = ivySbt.value
val mod = classifiersModule.value
val updateConfig0 = updateConfiguration.value
val updateConfig = updateConfig0
@ -3596,7 +3575,9 @@ object Classpaths {
val srcTypes = sourceArtifactTypes.value
val docTypes = docArtifactTypes.value
val log = s.log
val out = is.withIvy(log)(_.getSettings.getDefaultIvyUserDir)
val out = ivyPaths.value.ivyHome
.map(new File(_))
.getOrElse(new File(System.getProperty("user.home"), ".ivy2"))
val uwConfig = (update / unresolvedWarningConfiguration).value
withExcludes(out, mod.classifiers, lock(app)) { excludes =>
// val noExplicitCheck = ivy.map(_.withCheckExplicit(false))
@ -3703,9 +3684,9 @@ object Classpaths {
def deliverTask(config: TaskKey[PublishConfiguration]): Initialize[Task[File]] =
Def.task {
Def.unit(update.value)
if !useIvy.value then sys.error("deliver/makeIvyXml requires useIvy := true")
IvyActions.deliver(ivyModule.value, config.value, streams.value.log)
sys.error(
"deliver/makeIvyXml requires the sbt-ivy plugin. Add IvyDependencyPlugin to your project."
)
}
@deprecated("Use variant without delivery key", "1.1.1")
@ -3736,16 +3717,10 @@ object Classpaths {
val log = streams.value.log
val ref = thisProjectRef.value
logSkipPublish(log, ref)
} else if (!useIvy.value) {
sys.error(
"publishOrSkip requires useIvy := true. Use publish/publishLocal for ivyless publishing."
)
} else {
val conf = config.value
val log = streams.value.log
val module = ivyModule.value
val publisherInterface = publisher.value
publisherInterface.publish(module, conf, log)
sys.error(
"publishOrSkip requires the sbt-ivy plugin. Use publish/publishLocal for ivyless publishing."
)
}
}
.tag(Tags.Publish, Tags.Network)
@ -4062,17 +4037,6 @@ object Classpaths {
f(libraryDependencies.value)
}
/*
// can't cache deliver/publish easily since files involved are hidden behind patterns. publish will be difficult to verify target-side anyway
def cachedPublish(cacheFile: File)(g: (IvySbt#Module, PublishConfiguration) => Unit, module: IvySbt#Module, config: PublishConfiguration) => Unit =
{ case module :+: config :+: HNil =>
/* implicit val publishCache = publishIC
val f = cached(cacheFile) { (conf: IvyConfiguration, settings: ModuleSettings, config: PublishConfiguration) =>*/
g(module, config)
/*}
f(module.owner.configuration :+: module.moduleSettings :+: config :+: HNil)*/
}*/
def defaultRepositoryFilter: MavenRepository => Boolean = repo => !repo.root.startsWith("file:")
def getPublishTo(repo: Option[Resolver]): Resolver =
@ -4195,31 +4159,62 @@ object Classpaths {
depProjId.withConfigurations(dep.configuration).withExplicitArtifacts(Vector.empty)
}
private[sbt] def depMap: Initialize[Task[Map[ModuleRevisionId, ModuleDescriptor]]] =
import sbt.TupleSyntax.*
(buildDependencies.toTaskable, thisProjectRef.toTaskable, settingsData, streams)
.flatMapN { (bd, thisProj, data, s) =>
depMap(bd.classpathTransitiveRefs(thisProj), data, s.log)
}
private[sbt] def depMap(
projects: Seq[ProjectRef],
data: Def.Settings,
log: Logger
): Task[Map[ModuleRevisionId, ModuleDescriptor]] =
val ivyModules = projects.flatMap { proj =>
(proj / ivyModule).get(data)
}.join
ivyModules.mapN { mod =>
mod.map { _.dependencyMapping(log) }.toMap
}
def projectResolverTask: Initialize[Task[Resolver]] =
projectDescriptors.map { m =>
val resolver = new ProjectResolver(ProjectResolver.InterProject, m)
new RawRepository(resolver, resolver.getName)
Def.task {
// Stub resolver Coursier handles inter-project deps via csrInterProjectDependencies.
// Overridden by IvyDependencyPlugin with a real ProjectResolver.
new RawRepository(NoOpResolver, NoOpResolver.name)
}
private object NoOpResolver:
val name = "inter-project"
override def toString: String = name
/** Default publisher that delegates moduleDescriptor to Coursier and generates POM without Ivy. */
private[sbt] def defaultPublisher(
lm: DependencyResolution,
resolvers: Vector[Resolver] = Vector.empty,
): Publisher =
Publisher(new PublisherInterface {
def moduleDescriptor(moduleSetting: ModuleDescriptorConfiguration): ModuleDescriptor =
lm.moduleDescriptor(moduleSetting)
def publish(
module: ModuleDescriptor,
configuration: PublishConfiguration,
log: Logger
): Unit =
sys.error("Ivy-based publish requires the sbt-ivy plugin or useIvy := true")
def makePomFile(
module: ModuleDescriptor,
configuration: MakePomConfiguration,
log: Logger
): java.io.File =
val file = configuration.file.getOrElse(sys.error("makePom file must be specified."))
val ms = module.moduleSettings.asInstanceOf[ModuleDescriptorConfiguration]
val mid = ms.module
val info = configuration.moduleInfo.orElse(Option(ms.moduleInfo))
val deps = module.directDependencies
val extra = configuration.extra.getOrElse(scala.xml.NodeSeq.Empty)
val confs = configuration.configurations
val scalaInfo = ms.scalaModuleInfo
val pomXml =
sbt.internal.PomGenerator.makePom(
mid,
info,
deps,
confs,
extra,
scalaInfo,
resolvers,
configuration.filterRepositories,
configuration.allRepositories,
)
val processed = configuration.process(pomXml)
scala.xml.XML.save(file.getAbsolutePath, processed, "UTF-8", xmlDecl = true)
log.info("Wrote " + file.getAbsolutePath)
file
})
def makeProducts: Initialize[Task[Seq[File]]] = Def.task {
val c = fileConverter.value
val resourceDirs = resourceDirectories.value
@ -4247,23 +4242,6 @@ object Classpaths {
def internalDependencyJarsTask: Initialize[Task[Classpath]] =
ClasspathImpl.internalDependencyJarsTask
lazy val mkIvyConfiguration: Initialize[Task[InlineIvyConfiguration]] =
Def.task {
val (rs, other) = (fullResolvers.value.toVector, otherResolvers.value.toVector)
val s = streams.value
warnResolversConflict(rs ++: other, s.log)
errorInsecureProtocol(rs ++: other, s.log)
InlineIvyConfiguration()
.withPaths(ivyPaths.value)
.withResolvers(rs)
.withOtherResolvers(other)
.withModuleConfigurations(moduleConfigurations.value.toVector)
.withLock(lock(appConfiguration.value))
.withChecksums((update / checksums).value.toVector)
.withResolutionCacheDir(target.value / "resolution-cache")
.withUpdateOptions(updateOptions.value)
.withLog(s.log)
}
def interSort(
projectRef: ProjectRef,
@ -4474,7 +4452,7 @@ object Classpaths {
try {
app.provider.scalaProvider.launcher.checksums.toVector
} catch {
case _: NoSuchMethodError => IvySbt.DefaultChecksums
case _: NoSuchMethodError => Vector("sha1", "md5")
}
def isOverrideRepositories(app: xsbti.AppConfiguration): Boolean =

View File

@ -13,16 +13,13 @@ import java.io.File
import java.net.URI
import lmcoursier.definitions.{ CacheLogger, ModuleMatchers, Reconciliation }
import lmcoursier.{ CoursierConfiguration, FallbackDependency }
import org.apache.ivy.core.module.descriptor.ModuleDescriptor
import org.apache.ivy.core.module.id.ModuleRevisionId
import sbt.Def.*
import sbt.KeyRanks.*
import sbt.internal.InMemoryCacheStore.CacheStoreFactoryFactory
import sbt.internal.*
import sbt.internal.bsp.*
import sbt.internal.inc.ScalaInstance
import sbt.internal.librarymanagement.{ CompatibilityWarningOptions, IvySbt }
import sbt.internal.librarymanagement.ivy.{ IvyConfiguration, UpdateOptions }
import sbt.internal.librarymanagement.CompatibilityWarningOptions
import sbt.internal.server.BuildServerProtocol.BspFullWorkspace
import sbt.internal.server.{ BspCompileTask, BuildServerReporter, ServerHandler }
import sbt.internal.util.{ AttributeKey, ProgressState, SourcePosition }
@ -519,16 +516,16 @@ object Keys {
val internalConfigurationMap = settingKey[Configuration => Configuration]("Maps configurations to the actual configuration used to define the classpath.").withRank(CSetting)
val classpathConfiguration = taskKey[Configuration]("The configuration used to define the classpath.").withRank(CTask)
val ivyConfiguration = taskKey[IvyConfiguration]("General dependency management (Ivy) settings, such as the resolvers and paths to use.").withRank(DTask)
val ivyConfiguration = taskKey[Any]("General dependency management (Ivy) settings, such as the resolvers and paths to use.").withRank(DTask)
val ivyConfigurations = settingKey[Seq[Configuration]]("The defined configurations for dependency management. This may be different from the configurations for Project settings.").withRank(BSetting)
// This setting was created to work around the limitation of derived tasks not being able to use task-scoped task: ivyConfiguration in updateSbtClassifiers
val bootIvyConfiguration = taskKey[IvyConfiguration]("General dependency management (Ivy) settings, configured to retrieve sbt's components.").withRank(DTask)
val bootIvyConfiguration = taskKey[Any]("General dependency management (Ivy) settings, configured to retrieve sbt's components.").withRank(DTask)
val bootDependencyResolution = taskKey[DependencyResolution]("Dependency resolution to retrieve sbt's components.").withRank(CTask)
val scalaCompilerBridgeDependencyResolution = taskKey[DependencyResolution]("Dependency resolution to retrieve the compiler bridge.").withRank(CTask)
val moduleSettings = taskKey[ModuleSettings]("Module settings, which configure dependency management for a specific module, such as a project.").withRank(DTask)
val unmanagedBase = settingKey[File]("The default directory for manually managed libraries.").withRank(ASetting)
val updateConfiguration = settingKey[UpdateConfiguration]("Configuration for resolving and retrieving managed dependencies.").withRank(DSetting)
val updateOptions = settingKey[UpdateOptions]("Options for resolving managed dependencies.").withRank(DSetting)
val updateOptions = settingKey[Any]("Options for resolving managed dependencies.").withRank(DSetting)
@transient
val unresolvedWarningConfiguration = taskKey[UnresolvedWarningConfiguration]("Configuration for unresolved dependency warning.").withRank(DTask)
@ -537,8 +534,8 @@ object Keys {
@transient
val dependencyResolution = taskKey[DependencyResolution]("Provides the sbt interface to dependency resolution.").withRank(CTask)
val publisher = taskKey[Publisher]("Provides the sbt interface to publisher")
val ivySbt = taskKey[IvySbt]("Provides the sbt interface to Ivy.").withRank(CTask)
val ivyModule = taskKey[IvySbt#Module]("Provides the sbt interface to a configured Ivy module.").withRank(CTask)
val ivySbt = taskKey[Any]("Provides the sbt interface to Ivy.").withRank(CTask)
val ivyModule = taskKey[Any]("Provides the sbt interface to a configured Ivy module.").withRank(CTask)
val updateCacheName = taskKey[String]("Defines the directory name used to store the update cache files (inside the streams cacheDirectory).").withRank(DTask)
val update = taskKey[UpdateReport]("Resolves and optionally retrieves dependencies, producing a report.").withRank(ATask)
val updateFull = taskKey[UpdateReport]("Resolves and optionally retrieves dependencies, producing a full report with callers.").withRank(CTask)
@ -638,7 +635,7 @@ object Keys {
@transient
val publishTo = taskKey[Option[Resolver]]("The resolver to publish to.").withRank(ASetting)
val artifacts = settingKey[Seq[Artifact]]("The artifact definitions for the current module. Must be consistent with " + packagedArtifacts.key.label + ".").withRank(BSetting)
val projectDescriptors = taskKey[Map[ModuleRevisionId, ModuleDescriptor]]("Project dependency map for the inter-project resolver.").withRank(DTask)
val projectDescriptors = taskKey[Map[Any, Any]]("Project dependency map for the inter-project resolver.").withRank(DTask)
val autoUpdate = settingKey[Boolean]("<unimplemented>").withRank(Invisible)
val retrieveManaged = settingKey[Boolean]("If true, enables retrieving dependencies to the current build. Otherwise, dependencies are used directly from the cache.").withRank(BSetting)
val retrieveManagedSync = settingKey[Boolean]("If true, enables synchronizing the dependencies retrieved to the current build by removed unneeded files.").withRank(BSetting)

View File

@ -13,26 +13,12 @@ import java.net.URI
import sbt.librarymanagement.{ Credentials as IvyCredentials, * }
import sbt.util.Logger
import sbt.Keys.*
import lmcoursier.definitions.{
Classifier as CClassifier,
Configuration as CConfiguration,
Dependency as CDependency,
Extension as CExtension,
Info as CInfo,
Module as CModule,
ModuleName as CModuleName,
Organization as COrganization,
Project as CProject,
Publication as CPublication,
Type as CType,
Strict as CStrict,
}
import lmcoursier.definitions.{ Project as CProject, Strict as CStrict }
import lmcoursier.credentials.DirectCredentials
import lmcoursier.{ FallbackDependency, FromSbt, Inputs }
import sbt.internal.librarymanagement.mavenint.SbtPomExtraProperties
import sbt.ProjectExtra.transitiveInterDependencies
import sbt.ScopeFilter.Make.*
import scala.jdk.CollectionConverters.*
object CoursierInputsTasks {
private def coursierProject0(
@ -100,61 +86,6 @@ object CoursierInputsTasks {
)
}
private def moduleFromIvy(id: org.apache.ivy.core.module.id.ModuleRevisionId): CModule =
CModule(
COrganization(id.getOrganisation),
CModuleName(id.getName),
id.getExtraAttributes.asScala.map { (k0, v0) =>
k0.asInstanceOf[String] -> v0.asInstanceOf[String]
}.toMap
)
private def dependencyFromIvy(
desc: org.apache.ivy.core.module.descriptor.DependencyDescriptor
): Seq[(CConfiguration, CDependency)] = {
val id = desc.getDependencyRevisionId
val module = moduleFromIvy(id)
val exclusions = desc.getAllExcludeRules.map { rule =>
// we're ignoring rule.getConfigurations and rule.getMatcher here
val modId = rule.getId.getModuleId
// we're ignoring modId.getAttributes here
(COrganization(modId.getOrganisation), CModuleName(modId.getName))
}.toSet
val configurations = desc.getModuleConfigurations.toVector
.flatMap(Inputs.ivyXmlMappings)
def dependency(conf: CConfiguration, pub: CPublication) = CDependency(
module,
id.getRevision,
conf,
exclusions,
pub,
optional = false,
desc.isTransitive
)
val publications: CConfiguration => CPublication = {
val artifacts = desc.getAllDependencyArtifacts
val m = artifacts.toVector.flatMap { art =>
val pub =
CPublication(art.getName, CType(art.getType), CExtension(art.getExt()), CClassifier(""))
art.getConfigurations.map(CConfiguration(_)).toVector.map { conf =>
conf -> pub
}
}.toMap
c => m.getOrElse(c, CPublication("", CType(""), CExtension(""), CClassifier("")))
}
configurations.map { (from, to) =>
from -> dependency(to, publications(to))
}
}
private[sbt] def coursierInterProjectDependenciesTask: Def.Initialize[sbt.Task[Seq[CProject]]] =
(Def
.task {
@ -171,34 +102,9 @@ object CoursierInputsTasks {
private[sbt] def coursierExtraProjectsTask: Def.Initialize[sbt.Task[Seq[CProject]]] = {
Def.task {
val projects = csrInterProjectDependencies.value
val projectModules = projects.map(_.module).toSet
// this includes org.scala-sbt:global-plugins referenced from meta-builds in particular
sbt.Keys.projectDescriptors.value
.map { (k, v) =>
moduleFromIvy(k) -> v
}
.filter { case (module, _) =>
!projectModules(module)
}
.toVector
.map { (module, v) =>
val configurations = v.getConfigurations.map { c =>
CConfiguration(c.getName) -> c.getExtends.map(CConfiguration(_)).toSeq
}.toMap
val deps = v.getDependencies.flatMap(dependencyFromIvy)
CProject(
module,
v.getModuleRevisionId.getRevision,
deps.toSeq,
configurations,
Nil,
None,
Nil,
CInfo("", "", Nil, Nil, None)
)
}
// Coursier handles inter-project dependencies natively via csrInterProjectDependencies.
// The Ivy-typed projectDescriptors are only populated when IvyDependencyPlugin is enabled.
Seq.empty[CProject]
}
}
@ -230,7 +136,7 @@ object CoursierInputsTasks {
.flatMap {
case dc: IvyCredentials.DirectCredentials => List(dc)
case fc: IvyCredentials.FileCredentials =>
sbt.internal.librarymanagement.ivy.IvyCredentials.loadCredentials(fc.path) match {
sbt.librarymanagement.CredentialUtils.loadCredentials(fc.path) match {
case Left(err) =>
log.warn(s"$err, ignoring it")
Nil

View File

@ -278,7 +278,7 @@ object LMCoursier {
case dc: IvyCredentials.DirectCredentials =>
Right[String, IvyCredentials.DirectCredentials](dc)
case fc: IvyCredentials.FileCredentials =>
sbt.internal.librarymanagement.ivy.IvyCredentials.loadCredentials(fc.path)
sbt.librarymanagement.CredentialUtils.loadCredentials(fc.path)
}) match {
case Left(err) => st.log.warn(err)
case Right(d) =>

View File

@ -24,8 +24,6 @@ import sbt.ProjectExtra.{ extract, runUnloadHooks, setProject }
import sbt.SlashSyntax0.*
import sbt.librarymanagement.LibraryManagementCodec.given
import java.io.File
import org.apache.ivy.core.module.{ descriptor, id }
import descriptor.ModuleDescriptor, id.ModuleRevisionId
object GlobalPlugin {
// constructs a sequence of settings that may be appended to a project's settings to
@ -79,9 +77,7 @@ object GlobalPlugin {
val taskInit = Def.task {
val intcp = (Runtime / internalDependencyClasspath).value
val prods = (Runtime / exportedProducts).value
val depMap =
if useIvy.value then projectDescriptors.value + ivyModule.value.dependencyMapping(state.log)
else projectDescriptors.value
val depMap = projectDescriptors.value
GlobalPluginData(
projectID.value,
@ -125,7 +121,7 @@ object GlobalPlugin {
final case class GlobalPluginData(
projectID: ModuleID,
dependencies: Seq[ModuleID],
descriptors: Map[ModuleRevisionId, ModuleDescriptor],
descriptors: Map[Any, Any],
resolvers: Vector[Resolver],
fullClasspath: Classpath,
internalClasspath: Classpath

View File

@ -310,7 +310,7 @@ private[sbt] object LibraryManagement {
Seq[ScopedKey[?]],
ScopedKey[?],
Option[FiniteDuration],
IvySbt#Module,
ModuleSettings,
String,
ProjectRef,
Boolean,
@ -318,7 +318,6 @@ private[sbt] object LibraryManagement {
UnresolvedWarningConfiguration,
Boolean,
CompatibilityWarningOptions,
IvySbt,
GetClassifiersModule,
File,
xsbti.AppConfiguration,
@ -334,7 +333,7 @@ private[sbt] object LibraryManagement {
Keys.executionRoots,
Keys.resolvedScoped.toTaskable,
Keys.forceUpdatePeriod.toTaskable,
Keys.ivyModule.toTaskable,
Keys.moduleSettings.toTaskable,
Keys.updateCacheName.toTaskable,
Keys.thisProjectRef.toTaskable,
(Keys.update / Keys.skip).toTaskable,
@ -342,7 +341,6 @@ private[sbt] object LibraryManagement {
(Keys.update / Keys.unresolvedWarningConfiguration).toTaskable,
Keys.publishMavenStyle.toTaskable,
Keys.compatibilityWarningOptions.toTaskable,
Keys.ivySbt,
Keys.classifiersModule,
Keys.dependencyCacheDirectory,
Keys.appConfiguration.toTaskable,
@ -358,7 +356,7 @@ private[sbt] object LibraryManagement {
er,
rs,
fup,
im,
ms,
ucn,
thisRef,
sk,
@ -366,7 +364,6 @@ private[sbt] object LibraryManagement {
uwConfig,
mavenStyle,
cwo,
ivySbt0,
mod,
dcd,
app,
@ -400,10 +397,8 @@ private[sbt] object LibraryManagement {
conf1.withLogicalClock(LogicalClock(state0.hashCode))
}
cachedUpdate(
// LM API
lm = lm,
// Ivy-free ModuleDescriptor
module = im,
module = lm.moduleDescriptor(ms.asInstanceOf[ModuleDescriptorConfiguration]),
s.cacheStoreFactory.sub(ucn),
Reference.display(thisRef),
updateConf,
@ -446,12 +441,12 @@ private[sbt] object LibraryManagement {
)
val report = f(excludes)
val allExcludes: Map[ModuleID, Vector[ConfigRef]] = excludes ++
IvyActions
UpdateClassifiersUtil
.extractExcludes(report)
.view
.mapValues(cs => cs.map(c => ConfigRef(c)).toVector)
store.write(allExcludes)
IvyActions
UpdateClassifiersUtil
.addExcluded(
report,
classifiers.toVector,
@ -986,7 +981,7 @@ private[sbt] object LibraryManagement {
Def.task {
val log = streams.value.log
val conf = publishConfiguration.value
val module = ivyModule.value
val module = ivyModule.value.asInstanceOf[ModuleDescriptor]
val publisherInterface = publisher.value
publisherInterface.publish(module, conf, log)
}
@ -1114,7 +1109,7 @@ private[sbt] object LibraryManagement {
Def.task {
val log = streams.value.log
val conf = publishLocalConfiguration.value
val module = ivyModule.value
val module = ivyModule.value.asInstanceOf[ModuleDescriptor]
val publisherInterface = publisher.value
publisherInterface.publish(module, conf, log)
}
@ -1157,7 +1152,7 @@ private[sbt] object LibraryManagement {
Def.task {
val log = streams.value.log
val conf = publishM2Configuration.value
val module = ivyModule.value
val module = ivyModule.value.asInstanceOf[ModuleDescriptor]
val publisherInterface = publisher.value
publisherInterface.publish(module, conf, log)
}

View File

@ -0,0 +1,285 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
package internal
import sbt.librarymanagement.*
import scala.xml.{ Elem, Node, NodeSeq }
/**
* Generates Maven POM XML from sbt's own types, without requiring Ivy.
* This is used by the default publisher when the sbt-ivy plugin is not loaded.
*/
private[sbt] object PomGenerator:
def makePom(
mid: ModuleID,
info: Option[ModuleInfo],
deps: Vector[ModuleID],
configurations: Option[Vector[Configuration]],
extra: NodeSeq,
scalaModuleInfo: Option[ScalaModuleInfo] = None,
resolvers: Vector[Resolver] = Vector.empty,
filterRepositories: MavenRepository => Boolean = _ => true,
allRepositories: Boolean = false,
): Node =
val crossMid = crossVersionDep(mid, scalaModuleInfo)
val keepConfs: Set[String] =
configurations.map(_.map(_.name).toSet).getOrElse(Set.empty)
val crossVersioned = deps.map(crossVersionDep(_, scalaModuleInfo))
val filteredDeps =
if keepConfs.isEmpty then crossVersioned
else crossVersioned.filter(d => d.configurations.forall(c => confIntersects(c, keepConfs)))
val (bomDeps, regularDeps) = filteredDeps.partition: d =>
d.explicitArtifacts.nonEmpty && d.explicitArtifacts.forall(_.`type` == Artifact.PomType)
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
{makeModuleID(crossMid)}
{info.map(i => <name>{i.nameFormal}</name>).getOrElse(NodeSeq.Empty)}
{info.map(makeStartYear).getOrElse(NodeSeq.Empty)}
{info.map(makeOrganization).getOrElse(NodeSeq.Empty)}
{info.map(makeScmInfo).getOrElse(NodeSeq.Empty)}
{info.map(makeDeveloperInfo).getOrElse(NodeSeq.Empty)}
{info.map(makeLicenses).getOrElse(NodeSeq.Empty)}
{makeProperties(crossMid)}
{extra}
{makeRepositories(resolvers, filterRepositories, allRepositories)}
{makeDependencyManagement(bomDeps)}
{makeDependencies(regularDeps)}
</project>
private def crossVersionDep(dep: ModuleID, scalaInfo: Option[ScalaModuleInfo]): ModuleID =
val crossFn = CrossVersion(dep, scalaInfo)
val crossDep = crossFn match
case Some(fn) => dep.withName(fn(dep.name)).withCrossVersion(CrossVersion.disabled)
case None => dep
if crossDep.exclusions.isEmpty || scalaInfo.isEmpty then crossDep
else
val si = scalaInfo.get
val crossedExclusions = crossDep.exclusions.map: excl =>
CrossVersion(excl.crossVersion, si.scalaFullVersion, si.scalaBinaryVersion) match
case Some(fn) =>
excl.withName(fn(excl.name)).withCrossVersion(CrossVersion.disabled)
case None => excl
crossDep.withExclusions(crossedExclusions)
private def confIntersects(confStr: String, keepConfs: Set[String]): Boolean =
confStr
.split(';')
.exists: mapping =>
val from = mapping.split("->").head.trim
from.split(',').exists(c => keepConfs.contains(c.trim) || c.trim == "*")
private def makeModuleID(mid: ModuleID): NodeSeq =
val packaging =
if mid.explicitArtifacts.isEmpty then "jar"
else
val types = mid.explicitArtifacts.map(_.`type`).filterNot(IgnoreTypes)
if types.isEmpty then Artifact.PomType
else if types.contains(Artifact.DefaultType) then Artifact.DefaultType
else types.head
(<groupId>{mid.organization}</groupId>
<artifactId>{mid.name}</artifactId>
<version>{mid.revision}</version>
<packaging>{packaging}</packaging>)
private val IgnoreTypes: Set[String] =
Set(Artifact.SourceType, Artifact.DocType, Artifact.PomType)
private def makeStartYear(info: ModuleInfo): NodeSeq =
info.startYear match
case Some(y) => <inceptionYear>{y}</inceptionYear>
case _ => NodeSeq.Empty
private def makeOrganization(info: ModuleInfo): NodeSeq =
<organization>
<name>{info.organizationName}</name>
{info.organizationHomepage.map(h => <url>{h}</url>).getOrElse(NodeSeq.Empty)}
</organization>
private def makeScmInfo(info: ModuleInfo): NodeSeq =
info.scmInfo match
case Some(s) =>
<scm>
<url>{s.browseUrl}</url>
<connection>{s.connection}</connection>
{
s.devConnection
.map(d => <developerConnection>{d}</developerConnection>)
.getOrElse(NodeSeq.Empty)
}
</scm>
case _ => NodeSeq.Empty
private def makeDeveloperInfo(info: ModuleInfo): NodeSeq =
if info.developers.nonEmpty then
<developers>
{
info.developers.map: dev =>
<developer>
<id>{dev.id}</id>
<name>{dev.name}</name>
<url>{dev.url}</url>
{
if dev.email != null && dev.email.nonEmpty then <email>{dev.email}</email>
else NodeSeq.Empty
}
</developer>
}
</developers>
else NodeSeq.Empty
private def makeLicenses(info: ModuleInfo): NodeSeq =
if info.licenses.nonEmpty then
<licenses>
{
info.licenses.map: lic =>
<license>
<name>{lic.spdxId}</name>
<url>{lic.uri}</url>
<distribution>repo</distribution>
</license>
}
</licenses>
else NodeSeq.Empty
private def makeProperties(mid: ModuleID): NodeSeq =
val props = mid.extraAttributes
.collect:
case (k, v) if k.startsWith("e:info.") => (k.stripPrefix("e:"), v)
case (k, v) if k.startsWith("info.") => (k, v)
if props.isEmpty then NodeSeq.Empty
else
<properties>
{
props.toSeq
.sortBy(_._1)
.map: (k, v) =>
Elem(null, k, scala.xml.Null, scala.xml.TopScope, false, scala.xml.Text(v))
}
</properties>
private def makeRepositories(
resolvers: Vector[Resolver],
filter: MavenRepository => Boolean,
allRepositories: Boolean,
): NodeSeq =
val repos = resolvers.collect:
case r: MavenRepository if r.name != "public" && (allRepositories || filter(r)) =>
<repository>
<id>{r.name}</id>
<name>{r.name}</name>
<url>{r.root}</url>
</repository>
if repos.isEmpty then NodeSeq.Empty
else <repositories>{repos}</repositories>
private def makeDependencyManagement(deps: Vector[ModuleID]): NodeSeq =
if deps.isEmpty then NodeSeq.Empty
else
<dependencyManagement>
<dependencies>
{
deps.map: dep =>
<dependency>
<groupId>{dep.organization}</groupId>
<artifactId>{dep.name}</artifactId>
<version>{dep.revision}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
}
</dependencies>
</dependencyManagement>
private def makeDependencies(deps: Vector[ModuleID]): NodeSeq =
if deps.isEmpty then NodeSeq.Empty
else <dependencies>
{deps.map(makeDependencyElem)}
</dependencies>
private def makeDependencyElem(dep: ModuleID): Elem =
val (scope, optional) = getScopeAndOptional(dep.configurations)
val mavenVersion = convertVersion(dep.revision)
val versionNode: NodeSeq =
if mavenVersion == null || mavenVersion == "*" || mavenVersion.isEmpty then NodeSeq.Empty
else <version>{mavenVersion}</version>
val result: Elem =
<dependency>
<groupId>{dep.organization}</groupId>
<artifactId>{dep.name}</artifactId>
{versionNode}
{scopeElem(scope)}
{optionalElem(optional)}
{classifierElem(dep)}
{exclusions(dep)}
</dependency>
result
private def getScopeAndOptional(configurations: Option[String]): (Option[String], Boolean) =
configurations match
case None => (None, false)
case Some(confStr) =>
val confs =
confStr.split(';').flatMap(_.split("->").head.trim.split(',')).map(_.trim).toSet
val optional = confs.contains(Configurations.Optional.name)
val notOptional = confs - Configurations.Optional.name
val scope = Configurations.defaultMavenConfigurations
.find(c => notOptional.contains(c.name))
.map(_.name)
(scope, optional)
/** Convert Ivy-style dynamic versions to Maven range format. */
private def convertVersion(version: String): String =
if version == null then null
else if version.endsWith("+") then
val base = version.stripSuffix("+").stripSuffix(".")
val parts = base.split('.')
if parts.nonEmpty then
val last =
try parts.last.toInt + 1
catch case _: NumberFormatException => return version
val upper = (parts.init :+ last.toString).mkString(".")
s"[$base,$upper)"
else version
else if version == "latest.integration" || version == "latest.release" then ""
else version
private def scopeElem(scope: Option[String]): NodeSeq =
scope match
case None | Some("compile") => NodeSeq.Empty
case Some(s) => <scope>{s}</scope>
private def optionalElem(opt: Boolean): NodeSeq =
if opt then <optional>true</optional> else NodeSeq.Empty
private def classifierElem(dep: ModuleID): NodeSeq =
dep.explicitArtifacts.headOption.flatMap(_.classifier) match
case Some(c) => <classifier>{c}</classifier>
case None => NodeSeq.Empty
private def exclusions(dep: ModuleID): NodeSeq =
if dep.exclusions.isEmpty then NodeSeq.Empty
else
val elems = dep.exclusions.flatMap { excl =>
val g = excl.organization
val a = excl.name
if g.nonEmpty && g != "*" && a.nonEmpty && a != "*" then Some(<exclusion>
<groupId>{g}</groupId>
<artifactId>{a}</artifactId>
</exclusion>)
else None
}
if elems.isEmpty then NodeSeq.Empty
else <exclusions>{elems}</exclusions>
end PomGenerator

View File

@ -15,7 +15,7 @@ import sbt.internal.util.MessageOnlyException
import sbt.io.IO
import sbt.io.Path.contentOf
import sbt.librarymanagement.Credentials
import sbt.internal.librarymanagement.ivy.IvyCredentials
import sbt.librarymanagement.CredentialUtils
import sona.{ PublishingType, Sona }
import scala.concurrent.duration.FiniteDuration
@ -65,7 +65,7 @@ see https://www.scala-sbt.org/1.x/docs/Using-Sonatype.html for details.""")
}
private def fromCreds(creds: Seq[Credentials], uploadRequestTimeout: FiniteDuration): Sona = {
val cred = IvyCredentials
val cred = CredentialUtils
.forHost(creds, Sona.host)
.getOrElse(throw new MessageOnlyException(s"no credentials are found for ${Sona.host}"))
Sona.oauthClient(cred.userName, cred.passwd, uploadRequestTimeout)

View File

@ -18,7 +18,6 @@ import sbt.ProjectExtra.*
import sbt.internal.graph.*
import sbt.internal.graph.backend.SbtUpdateReport
import sbt.internal.graph.rendering.{ DagreHTML, TreeView }
import sbt.internal.librarymanagement.*
import sbt.internal.util.complete.{ Parser, Parsers }
import sbt.internal.util.complete.DefaultParsers.*
import sbt.io.IO
@ -110,29 +109,12 @@ OPTIONS
*/
def coreSettings =
Seq(
// disable the cached resolution engine (exposing a scoped `ivyModule` used directly by `updateTask`), as it
// generates artificial module descriptors which are internal to sbt, making it hard to reconstruct the
// dependency tree
dependencyTreeIgnoreMissingUpdate / updateOptions := updateOptions.value
.withCachedResolution(false),
dependencyTreeIgnoreMissingUpdate / ivyConfiguration := Def.uncached {
// inTask will make sure the new definition will pick up `updateOptions in dependencyTreeIgnoreMissingUpdate`
Project.inTask(dependencyTreeIgnoreMissingUpdate, Classpaths.mkIvyConfiguration).value
},
dependencyTreeIgnoreMissingUpdate / ivyModule := Def.uncached {
// concatenating & inlining ivySbt & ivyModule default task implementations, as `SbtAccess.inTask` does
// NOT correctly force the scope when applied to `TaskKey.toTask` instances (as opposed to raw
// implementations like `Classpaths.mkIvyConfiguration` or `Classpaths.updateTask`)
val is = new IvySbt((dependencyTreeIgnoreMissingUpdate / ivyConfiguration).value)
new is.Module(moduleSettings.value)
},
// don't fail on missing dependencies or eviction errors
dependencyTreeIgnoreMissingUpdate / updateConfiguration := updateConfiguration.value
.withMissingOk(true),
dependencyTreeIgnoreMissingUpdate / evictionErrorLevel := Level.Warn,
dependencyTreeIgnoreMissingUpdate / assumedEvictionErrorLevel := Level.Warn,
dependencyTreeIgnoreMissingUpdate := Def.uncached {
// inTask will make sure the new definition will pick up `ivyModule/updateConfiguration in ignoreMissingUpdate`
Project.inTask(dependencyTreeIgnoreMissingUpdate, Classpaths.updateTask).value
},
)
@ -390,8 +372,8 @@ OPTIONS
type HasModule = {
val module: ModuleID
}
def crossName(ivyModule: IvySbt#Module) =
ivyModule.moduleSettings match {
def crossName(module: Any) =
module match {
case ic: ModuleDescriptorConfiguration => ic.module.name
case _ =>
throw new IllegalStateException(

View File

@ -1,17 +0,0 @@
scalaVersion := "2.12.21"
libraryDependencies += "org.slf4j" % "slf4j-api" % "1.7.28"
updateOptions := updateOptions.value.withCachedResolution(true)
TaskKey[Unit]("check") := {
val report = (Test / updateFull).value
val graph = (Test / dependencyTree).toTask(" --quiet").value
def sanitize(str: String): String = str.split('\n').drop(1).mkString("\n")
val expectedGraph =
"""default:cachedresolution_2.12:0.1.0-SNAPSHOT
| +-org.slf4j:slf4j-api:1.7.28
| """.stripMargin
require(sanitize(graph) == sanitize(expectedGraph), "Graph for report %s was '\n%s' but should have been '\n%s'" format (report, sanitize(graph), sanitize(expectedGraph)))
()
}

View File

@ -1,3 +1,4 @@
ThisBuild / useIvy := true
lazy val root = (project in file("."))
lazy val a = project
lazy val b = project

View File

@ -0,0 +1,4 @@
libraryDependencies += {
val sbtV = sbtVersion.value
("org.scala-sbt" % s"sbt-ivy_sbt${sbtV}_${scalaBinaryVersion.value}" % sbtV).intransitive()
}

View File

@ -0,0 +1,4 @@
libraryDependencies += {
val sbtV = sbtVersion.value
("org.scala-sbt" % s"sbt-ivy_sbt${sbtV}_${scalaBinaryVersion.value}" % sbtV).intransitive()
}

View File

@ -2,6 +2,8 @@ import scala.xml.{ Node, _ }
import scala.xml.Utility.trim
import sbt.internal.librarymanagement.{ IvySbt, MakePom }
ThisBuild / useIvy := true
lazy val check = taskKey[Unit]("check")
val dispatch = "net.databinder.dispatch" %% "dispatch-core" % "0.11.2"
@ -36,7 +38,7 @@ lazy val root = (project in file(".")).
sys.error("dispatch-core_2.11-0.11.1.jar found when it should NOT be included: " + bcp.toString)
}
val bPomXml = makePomXml(streams.value.log, (b / makePomConfiguration).value, (b / ivyModule).value)
val bPomXml = makePomXml(streams.value.log, (b / makePomConfiguration).value, (b / ivyModule).value.asInstanceOf[IvySbt#Module])
val repatchTwitterXml = bPomXml \ "dependencies" \ "dependency" find { d =>
(d \ "groupId").text == "com.eed3si9n" && (d \ "artifactId").text == "repatch-twitter-core_2.11"

View File

@ -0,0 +1,4 @@
libraryDependencies += {
val sbtV = sbtVersion.value
("org.scala-sbt" % s"sbt-ivy_sbt${sbtV}_${scalaBinaryVersion.value}" % sbtV).intransitive()
}

View File

@ -0,0 +1,4 @@
libraryDependencies += {
val sbtV = sbtVersion.value
("org.scala-sbt" % s"sbt-ivy_sbt${sbtV}_${scalaBinaryVersion.value}" % sbtV).intransitive()
}

View File

@ -1,4 +1,5 @@
ThisBuild / scalaVersion := "2.12.21"
ThisBuild / useIvy := true
def configIvyScala =
scalaModuleInfo ~= (_ map (_ withCheckExplicit false))

View File

@ -0,0 +1,4 @@
libraryDependencies += {
val sbtV = sbtVersion.value
("org.scala-sbt" % s"sbt-ivy_sbt${sbtV}_${scalaBinaryVersion.value}" % sbtV).intransitive()
}

View File

@ -210,21 +210,25 @@ object IvyXml {
shadedConfigOpt: Option[Configuration]
): Setting[Task[T]] =
task := Def.uncached(task.dependsOnTask {
Def.task {
val currentProject = {
val proj = csrProject.value
val publications = csrPublications.value
proj.withPublications(publications)
Def.ifS(Def.task { sbt.Keys.useIvy.value })(
Def.task {
val currentProject = {
val proj = csrProject.value
val publications = csrPublications.value
proj.withPublications(publications)
}
val resolved = sbt.Keys.resolvedDependencies.value
IvyXml.writeFiles(
currentProject,
shadedConfigOpt,
sbt.Keys.ivySbt.value.asInstanceOf[IvySbt],
sbt.Keys.streams.value.log,
resolved
)
}
val resolved = sbt.Keys.resolvedDependencies.value
IvyXml.writeFiles(
currentProject,
shadedConfigOpt,
sbt.Keys.ivySbt.value,
sbt.Keys.streams.value.log,
resolved
)
}
)(
Def.task { () }
)
}.value)
private lazy val needsIvyXmlLocal = Seq(publishLocalConfiguration) ++ getPubConf(

View File

@ -0,0 +1,274 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
package plugins
import java.io.File
import org.apache.ivy.core.module.descriptor.{ DependencyDescriptor, ModuleDescriptor }
import org.apache.ivy.core.module.id.ModuleRevisionId
import sbt.Def.{ Initialize, Setting }
import sbt.Keys.*
import sbt.ProjectExtra.*
import sbt.internal.LibraryManagement
import sbt.internal.librarymanagement.{ IvyActions, IvySbt, IvyXml, ProjectResolver }
import sbt.internal.librarymanagement.ivy.*
import sbt.io.syntax.*
import sbt.librarymanagement.*
import sbt.std.TaskExtra.*
import lmcoursier.definitions.{
Classifier as CClassifier,
Configuration as CConfiguration,
Dependency as CDependency,
Extension as CExtension,
Info as CInfo,
Module as CModule,
ModuleName as CModuleName,
Organization as COrganization,
Project as CProject,
Publication as CPublication,
Type as CType,
}
import lmcoursier.Inputs
import scala.jdk.CollectionConverters.*
/**
* AutoPlugin that provides all Ivy-specific functionality.
* This plugin overrides the stub defaults in main/ with real Ivy implementations.
*/
object IvyDependencyPlugin extends AutoPlugin:
override def requires = IvyPlugin
override def trigger = allRequirements
override lazy val globalSettings: Seq[Setting[?]] = Seq(
updateOptions := UpdateOptions(),
)
override lazy val projectSettings: Seq[Setting[?]] = Seq(
ivyConfiguration := Def.uncached(
Def
.ifS(Def.task { useIvy.value })(
Def.task { mkIvyConfiguration.value: Any }
)(
Def.task { (): Any }
)
.value
),
publisher := Def.uncached(
Def
.ifS(Def.task { useIvy.value })(
Def.task {
IvyPublisher(ivyConfiguration.value.asInstanceOf[IvyConfiguration])
}
)(
Def.task {
Classpaths.defaultPublisher(dependencyResolution.value, fullResolvers.value.toVector)
}
)
.value
),
ivySbt := Def.uncached(
Def
.ifS(Def.task { useIvy.value })(
Def.task { ivySbt0.value: Any }
)(
Def.task { (): Any }
)
.value
),
ivyModule := Def.uncached(
Def
.ifS(Def.task { useIvy.value })(
Def.task {
val is = ivySbt.value.asInstanceOf[IvySbt]
new is.Module(moduleSettings.value): Any
}
)(
Def.task { (): Any }
)
.value
),
projectDescriptors := Def.uncached(
Def
.ifS(Def.task { useIvy.value })(
Def.task { depMap.value.asInstanceOf[Map[Any, Any]] }
)(
Def.task { Map.empty[Any, Any] }
)
.value
),
projectResolver := Def.uncached(
Def
.ifS(Def.task { useIvy.value })(
projectResolverTask
)(
Classpaths.projectResolverTask
)
.value
),
csrExtraProjects := Def.uncached(
Def
.ifS(Def.task { useIvy.value })(
coursierExtraProjectsTask
)(
Def.task { Nil }
)
.value
),
) ++ IvyXml.generateIvyXmlSettings() ++ ivyPublishOrSkipSettings
private lazy val ivySbt0: Initialize[Task[IvySbt]] =
Def.task {
IvyCredentials.register(credentials.value, streams.value.log)
new IvySbt(ivyConfiguration.value.asInstanceOf[IvyConfiguration])
}
private lazy val mkIvyConfiguration: Initialize[Task[InlineIvyConfiguration]] =
Def.task {
val (rs, other) = (fullResolvers.value.toVector, otherResolvers.value.toVector)
val s = streams.value
Classpaths.warnResolversConflict(rs ++: other, s.log)
Classpaths.errorInsecureProtocol(rs ++: other, s.log)
InlineIvyConfiguration()
.withPaths(ivyPaths.value)
.withResolvers(rs)
.withOtherResolvers(other)
.withModuleConfigurations(moduleConfigurations.value.toVector)
.withLock(LibraryManagement.lock(appConfiguration.value))
.withChecksums((update / checksums).value.toVector)
.withResolutionCacheDir(target.value / "resolution-cache")
.withUpdateOptions(updateOptions.value.asInstanceOf[UpdateOptions])
.withLog(s.log)
}
private def depMap: Initialize[Task[Map[ModuleRevisionId, ModuleDescriptor]]] =
import sbt.TupleSyntax.*
(buildDependencies.toTaskable, thisProjectRef.toTaskable, settingsData, streams)
.flatMapN { (bd, thisProj, data, s) =>
depMap(bd.classpathTransitiveRefs(thisProj), data, s.log)
}
private def depMap(
projects: Seq[ProjectRef],
data: Def.Settings,
log: sbt.util.Logger
): Task[Map[ModuleRevisionId, ModuleDescriptor]] =
val ivyModules = projects.flatMap { proj =>
(proj / ivyModule).get(data)
}.join
ivyModules.mapN { mod =>
mod.map { m => m.asInstanceOf[IvySbt#Module].dependencyMapping(log) }.toMap
}
private def projectResolverTask: Initialize[Task[Resolver]] =
projectDescriptors.map { m =>
val typedMap = m.asInstanceOf[Map[ModuleRevisionId, ModuleDescriptor]]
val resolver = new ProjectResolver(ProjectResolver.InterProject, typedMap)
new RawRepository(resolver, resolver.getName)
}
private def ivyPublishOrSkipSettings: Seq[Setting[?]] =
Seq(
deliver := deliverTask(makeIvyXmlConfiguration).value,
deliverLocal := deliverTask(makeIvyXmlLocalConfiguration).value,
makeIvyXml := deliverTask(makeIvyXmlConfiguration).value,
)
private def deliverTask(config: TaskKey[PublishConfiguration]): Initialize[Task[File]] =
Def.task {
Def.unit(update.value)
if !useIvy.value then sys.error("deliver/makeIvyXml requires useIvy := true")
IvyActions.deliver(
ivyModule.value.asInstanceOf[IvySbt#Module],
config.value,
streams.value.log
)
}
private lazy val coursierExtraProjectsTask: Initialize[Task[Seq[CProject]]] =
Def.task {
val projects = csrInterProjectDependencies.value
val projectModules = projects.map(_.module).toSet
projectDescriptors.value
.map { (k, v) =>
val id = k.asInstanceOf[ModuleRevisionId]
val desc = v.asInstanceOf[ModuleDescriptor]
moduleFromIvy(id) -> desc
}
.filter { case (module, _) =>
!projectModules(module)
}
.toVector
.map { (module, v) =>
val configurations = v.getConfigurations.map { c =>
CConfiguration(c.getName) -> c.getExtends.map(CConfiguration(_)).toSeq
}.toMap
val deps = v.getDependencies.flatMap(dependencyFromIvy)
CProject(
module,
v.getModuleRevisionId.getRevision,
deps.toSeq,
configurations,
Nil,
None,
Nil,
CInfo("", "", Nil, Nil, None)
)
}
}
private def moduleFromIvy(id: ModuleRevisionId): CModule =
CModule(
COrganization(id.getOrganisation),
CModuleName(id.getName),
id.getExtraAttributes.asScala.map { (k0, v0) =>
k0.asInstanceOf[String] -> v0.asInstanceOf[String]
}.toMap
)
private def dependencyFromIvy(
desc: DependencyDescriptor
): Seq[(CConfiguration, CDependency)] =
val id = desc.getDependencyRevisionId
val module = moduleFromIvy(id)
val exclusions = desc.getAllExcludeRules.map { rule =>
val modId = rule.getId.getModuleId
(COrganization(modId.getOrganisation), CModuleName(modId.getName))
}.toSet
val configurations = desc.getModuleConfigurations.toVector
.flatMap(Inputs.ivyXmlMappings)
def dependency(conf: CConfiguration, pub: CPublication) = CDependency(
module,
id.getRevision,
conf,
exclusions,
pub,
optional = false,
desc.isTransitive
)
val publications: CConfiguration => CPublication =
val artifacts = desc.getAllDependencyArtifacts
val m = artifacts.toVector.flatMap { art =>
val pub = CPublication(
art.getName,
CType(art.getType),
CExtension(art.getExt()),
CClassifier("")
)
art.getConfigurations.map(CConfiguration(_)).toVector.map { conf =>
conf -> pub
}
}.toMap
c => m.getOrElse(c, CPublication("", CType(""), CExtension(""), CClassifier("")))
configurations.map { (from, to) =>
from -> dependency(to, publications(to))
}
end IvyDependencyPlugin