diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db693926b..82dcd06a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/build.sbt b/build.sbt index a3b7003ee..897acbcd0 100644 --- a/build.sbt +++ b/build.sbt @@ -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, diff --git a/lm-core/src/main/scala/sbt/internal/librarymanagement/UpdateClassifiersUtil.scala b/lm-core/src/main/scala/sbt/internal/librarymanagement/UpdateClassifiersUtil.scala index 455e74619..239a49c03 100644 --- a/lm-core/src/main/scala/sbt/internal/librarymanagement/UpdateClassifiersUtil.scala +++ b/lm-core/src/main/scala/sbt/internal/librarymanagement/UpdateClassifiersUtil.scala @@ -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]) + } diff --git a/lm-core/src/main/scala/sbt/librarymanagement/CredentialUtils.scala b/lm-core/src/main/scala/sbt/librarymanagement/CredentialUtils.scala new file mode 100644 index 000000000..162f62156 --- /dev/null +++ b/lm-core/src/main/scala/sbt/librarymanagement/CredentialUtils.scala @@ -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 diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 81d47ed54..83e7242dc 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -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 = diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 2f2046339..3fc94743c 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -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]("").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) diff --git a/main/src/main/scala/sbt/coursierint/CoursierInputsTasks.scala b/main/src/main/scala/sbt/coursierint/CoursierInputsTasks.scala index 7af0d36ee..2bc721185 100644 --- a/main/src/main/scala/sbt/coursierint/CoursierInputsTasks.scala +++ b/main/src/main/scala/sbt/coursierint/CoursierInputsTasks.scala @@ -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 diff --git a/main/src/main/scala/sbt/coursierint/LMCoursier.scala b/main/src/main/scala/sbt/coursierint/LMCoursier.scala index 685481bf8..26e21ffc6 100644 --- a/main/src/main/scala/sbt/coursierint/LMCoursier.scala +++ b/main/src/main/scala/sbt/coursierint/LMCoursier.scala @@ -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) => diff --git a/main/src/main/scala/sbt/internal/GlobalPlugin.scala b/main/src/main/scala/sbt/internal/GlobalPlugin.scala index e0dfe3ad3..bb3c63fa8 100644 --- a/main/src/main/scala/sbt/internal/GlobalPlugin.scala +++ b/main/src/main/scala/sbt/internal/GlobalPlugin.scala @@ -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 diff --git a/main/src/main/scala/sbt/internal/LibraryManagement.scala b/main/src/main/scala/sbt/internal/LibraryManagement.scala index e079c0efd..57e542aaf 100644 --- a/main/src/main/scala/sbt/internal/LibraryManagement.scala +++ b/main/src/main/scala/sbt/internal/LibraryManagement.scala @@ -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) } diff --git a/main/src/main/scala/sbt/internal/PomGenerator.scala b/main/src/main/scala/sbt/internal/PomGenerator.scala new file mode 100644 index 000000000..579d37af7 --- /dev/null +++ b/main/src/main/scala/sbt/internal/PomGenerator.scala @@ -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) + + + 4.0.0 + {makeModuleID(crossMid)} + {info.map(i => {i.nameFormal}).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)} + + + 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 + ({mid.organization} + {mid.name} + {mid.revision} + {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) => {y} + case _ => NodeSeq.Empty + + private def makeOrganization(info: ModuleInfo): NodeSeq = + + {info.organizationName} + {info.organizationHomepage.map(h => {h}).getOrElse(NodeSeq.Empty)} + + + private def makeScmInfo(info: ModuleInfo): NodeSeq = + info.scmInfo match + case Some(s) => + + {s.browseUrl} + {s.connection} + { + s.devConnection + .map(d => {d}) + .getOrElse(NodeSeq.Empty) + } + + case _ => NodeSeq.Empty + + private def makeDeveloperInfo(info: ModuleInfo): NodeSeq = + if info.developers.nonEmpty then + + { + info.developers.map: dev => + + {dev.id} + {dev.name} + {dev.url} + { + if dev.email != null && dev.email.nonEmpty then {dev.email} + else NodeSeq.Empty + } + + } + + else NodeSeq.Empty + + private def makeLicenses(info: ModuleInfo): NodeSeq = + if info.licenses.nonEmpty then + + { + info.licenses.map: lic => + + {lic.spdxId} + {lic.uri} + repo + + } + + 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 + + { + props.toSeq + .sortBy(_._1) + .map: (k, v) => + Elem(null, k, scala.xml.Null, scala.xml.TopScope, false, scala.xml.Text(v)) + } + + + 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)) => + + {r.name} + {r.name} + {r.root} + + if repos.isEmpty then NodeSeq.Empty + else {repos} + + private def makeDependencyManagement(deps: Vector[ModuleID]): NodeSeq = + if deps.isEmpty then NodeSeq.Empty + else + + + { + deps.map: dep => + + {dep.organization} + {dep.name} + {dep.revision} + pom + import + + } + + + + private def makeDependencies(deps: Vector[ModuleID]): NodeSeq = + if deps.isEmpty then NodeSeq.Empty + else + {deps.map(makeDependencyElem)} + + + 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 {mavenVersion} + val result: Elem = + + {dep.organization} + {dep.name} + {versionNode} + {scopeElem(scope)} + {optionalElem(optional)} + {classifierElem(dep)} + {exclusions(dep)} + + 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) => {s} + + private def optionalElem(opt: Boolean): NodeSeq = + if opt then true else NodeSeq.Empty + + private def classifierElem(dep: ModuleID): NodeSeq = + dep.explicitArtifacts.headOption.flatMap(_.classifier) match + case Some(c) => {c} + 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( + {g} + {a} + ) + else None + } + if elems.isEmpty then NodeSeq.Empty + else {elems} +end PomGenerator diff --git a/main/src/main/scala/sbt/internal/librarymanagement/Publishing.scala b/main/src/main/scala/sbt/internal/librarymanagement/Publishing.scala index b5eccf0c0..3e067de98 100644 --- a/main/src/main/scala/sbt/internal/librarymanagement/Publishing.scala +++ b/main/src/main/scala/sbt/internal/librarymanagement/Publishing.scala @@ -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) diff --git a/main/src/main/scala/sbt/plugins/DependencyTreeSettings.scala b/main/src/main/scala/sbt/plugins/DependencyTreeSettings.scala index 68ebf7fd8..5e14a4751 100644 --- a/main/src/main/scala/sbt/plugins/DependencyTreeSettings.scala +++ b/main/src/main/scala/sbt/plugins/DependencyTreeSettings.scala @@ -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( diff --git a/sbt-app/src/sbt-test/dependency-graph/cachedResolution/build.sbt b/sbt-app/src/sbt-test/dependency-graph/cachedResolution/build.sbt deleted file mode 100644 index 039f672d1..000000000 --- a/sbt-app/src/sbt-test/dependency-graph/cachedResolution/build.sbt +++ /dev/null @@ -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))) - () -} diff --git a/sbt-app/src/sbt-test/dependency-graph/cachedResolution/test b/sbt-app/src/sbt-test/dependency-graph/cachedResolution/test deleted file mode 100644 index a5912a391..000000000 --- a/sbt-app/src/sbt-test/dependency-graph/cachedResolution/test +++ /dev/null @@ -1 +0,0 @@ -> check \ No newline at end of file diff --git a/sbt-app/src/sbt-test/project/build-deps/a/A.scala b/sbt-app/src/sbt-test/ivy/build-deps/a/A.scala similarity index 100% rename from sbt-app/src/sbt-test/project/build-deps/a/A.scala rename to sbt-app/src/sbt-test/ivy/build-deps/a/A.scala diff --git a/sbt-app/src/sbt-test/project/build-deps/b/B.scala b/sbt-app/src/sbt-test/ivy/build-deps/b/B.scala similarity index 100% rename from sbt-app/src/sbt-test/project/build-deps/b/B.scala rename to sbt-app/src/sbt-test/ivy/build-deps/b/B.scala diff --git a/sbt-app/src/sbt-test/project/build-deps/build.sbt b/sbt-app/src/sbt-test/ivy/build-deps/build.sbt similarity index 75% rename from sbt-app/src/sbt-test/project/build-deps/build.sbt rename to sbt-app/src/sbt-test/ivy/build-deps/build.sbt index 6777c4eb3..853e3673c 100644 --- a/sbt-app/src/sbt-test/project/build-deps/build.sbt +++ b/sbt-app/src/sbt-test/ivy/build-deps/build.sbt @@ -1,3 +1,4 @@ +ThisBuild / useIvy := true lazy val root = (project in file(".")) lazy val a = project lazy val b = project diff --git a/sbt-app/src/sbt-test/project/build-deps/changes/b.sbt b/sbt-app/src/sbt-test/ivy/build-deps/changes/b.sbt similarity index 100% rename from sbt-app/src/sbt-test/project/build-deps/changes/b.sbt rename to sbt-app/src/sbt-test/ivy/build-deps/changes/b.sbt diff --git a/sbt-app/src/sbt-test/ivy/build-deps/project/plugins.sbt b/sbt-app/src/sbt-test/ivy/build-deps/project/plugins.sbt new file mode 100644 index 000000000..abe6a97a4 --- /dev/null +++ b/sbt-app/src/sbt-test/ivy/build-deps/project/plugins.sbt @@ -0,0 +1,4 @@ +libraryDependencies += { + val sbtV = sbtVersion.value + ("org.scala-sbt" % s"sbt-ivy_sbt${sbtV}_${scalaBinaryVersion.value}" % sbtV).intransitive() +} diff --git a/sbt-app/src/sbt-test/project/build-deps/test b/sbt-app/src/sbt-test/ivy/build-deps/test similarity index 100% rename from sbt-app/src/sbt-test/project/build-deps/test rename to sbt-app/src/sbt-test/ivy/build-deps/test diff --git a/sbt-app/src/sbt-test/dependency-management/deliver-artifacts/a/A.java b/sbt-app/src/sbt-test/ivy/deliver-artifacts/a/A.java similarity index 100% rename from sbt-app/src/sbt-test/dependency-management/deliver-artifacts/a/A.java rename to sbt-app/src/sbt-test/ivy/deliver-artifacts/a/A.java diff --git a/sbt-app/src/sbt-test/dependency-management/deliver-artifacts/b/B.java b/sbt-app/src/sbt-test/ivy/deliver-artifacts/b/B.java similarity index 100% rename from sbt-app/src/sbt-test/dependency-management/deliver-artifacts/b/B.java rename to sbt-app/src/sbt-test/ivy/deliver-artifacts/b/B.java diff --git a/sbt-app/src/sbt-test/dependency-management/deliver-artifacts/build.sbt b/sbt-app/src/sbt-test/ivy/deliver-artifacts/build.sbt similarity index 100% rename from sbt-app/src/sbt-test/dependency-management/deliver-artifacts/build.sbt rename to sbt-app/src/sbt-test/ivy/deliver-artifacts/build.sbt diff --git a/sbt-app/src/sbt-test/ivy/deliver-artifacts/project/plugins.sbt b/sbt-app/src/sbt-test/ivy/deliver-artifacts/project/plugins.sbt new file mode 100644 index 000000000..abe6a97a4 --- /dev/null +++ b/sbt-app/src/sbt-test/ivy/deliver-artifacts/project/plugins.sbt @@ -0,0 +1,4 @@ +libraryDependencies += { + val sbtV = sbtVersion.value + ("org.scala-sbt" % s"sbt-ivy_sbt${sbtV}_${scalaBinaryVersion.value}" % sbtV).intransitive() +} diff --git a/sbt-app/src/sbt-test/dependency-management/deliver-artifacts/test b/sbt-app/src/sbt-test/ivy/deliver-artifacts/test similarity index 100% rename from sbt-app/src/sbt-test/dependency-management/deliver-artifacts/test rename to sbt-app/src/sbt-test/ivy/deliver-artifacts/test diff --git a/sbt-app/src/sbt-test/dependency-management/exclude-dependencies/build.sbt b/sbt-app/src/sbt-test/ivy/exclude-dependencies/build.sbt similarity index 96% rename from sbt-app/src/sbt-test/dependency-management/exclude-dependencies/build.sbt rename to sbt-app/src/sbt-test/ivy/exclude-dependencies/build.sbt index 88ef1fd52..6654c4859 100644 --- a/sbt-app/src/sbt-test/dependency-management/exclude-dependencies/build.sbt +++ b/sbt-app/src/sbt-test/ivy/exclude-dependencies/build.sbt @@ -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" diff --git a/sbt-app/src/sbt-test/ivy/exclude-dependencies/project/plugins.sbt b/sbt-app/src/sbt-test/ivy/exclude-dependencies/project/plugins.sbt new file mode 100644 index 000000000..abe6a97a4 --- /dev/null +++ b/sbt-app/src/sbt-test/ivy/exclude-dependencies/project/plugins.sbt @@ -0,0 +1,4 @@ +libraryDependencies += { + val sbtV = sbtVersion.value + ("org.scala-sbt" % s"sbt-ivy_sbt${sbtV}_${scalaBinaryVersion.value}" % sbtV).intransitive() +} diff --git a/sbt-app/src/sbt-test/dependency-management/exclude-dependencies/test b/sbt-app/src/sbt-test/ivy/exclude-dependencies/test similarity index 100% rename from sbt-app/src/sbt-test/dependency-management/exclude-dependencies/test rename to sbt-app/src/sbt-test/ivy/exclude-dependencies/test diff --git a/sbt-app/src/sbt-test/dependency-management/make-ivy-xml/build.sbt b/sbt-app/src/sbt-test/ivy/make-ivy-xml/build.sbt similarity index 100% rename from sbt-app/src/sbt-test/dependency-management/make-ivy-xml/build.sbt rename to sbt-app/src/sbt-test/ivy/make-ivy-xml/build.sbt diff --git a/sbt-app/src/sbt-test/ivy/make-ivy-xml/project/plugins.sbt b/sbt-app/src/sbt-test/ivy/make-ivy-xml/project/plugins.sbt new file mode 100644 index 000000000..abe6a97a4 --- /dev/null +++ b/sbt-app/src/sbt-test/ivy/make-ivy-xml/project/plugins.sbt @@ -0,0 +1,4 @@ +libraryDependencies += { + val sbtV = sbtVersion.value + ("org.scala-sbt" % s"sbt-ivy_sbt${sbtV}_${scalaBinaryVersion.value}" % sbtV).intransitive() +} diff --git a/sbt-app/src/sbt-test/dependency-management/make-ivy-xml/test b/sbt-app/src/sbt-test/ivy/make-ivy-xml/test similarity index 100% rename from sbt-app/src/sbt-test/dependency-management/make-ivy-xml/test rename to sbt-app/src/sbt-test/ivy/make-ivy-xml/test diff --git a/sbt-app/src/sbt-test/dependency-management/provided-multi/changes/A.scala b/sbt-app/src/sbt-test/ivy/provided-multi/changes/A.scala similarity index 100% rename from sbt-app/src/sbt-test/dependency-management/provided-multi/changes/A.scala rename to sbt-app/src/sbt-test/ivy/provided-multi/changes/A.scala diff --git a/sbt-app/src/sbt-test/dependency-management/provided-multi/changes/B.scala b/sbt-app/src/sbt-test/ivy/provided-multi/changes/B.scala similarity index 100% rename from sbt-app/src/sbt-test/dependency-management/provided-multi/changes/B.scala rename to sbt-app/src/sbt-test/ivy/provided-multi/changes/B.scala diff --git a/sbt-app/src/sbt-test/dependency-management/provided-multi/changes/p.sbt b/sbt-app/src/sbt-test/ivy/provided-multi/changes/p.sbt similarity index 96% rename from sbt-app/src/sbt-test/dependency-management/provided-multi/changes/p.sbt rename to sbt-app/src/sbt-test/ivy/provided-multi/changes/p.sbt index 9f5e783d2..15f3c2626 100644 --- a/sbt-app/src/sbt-test/dependency-management/provided-multi/changes/p.sbt +++ b/sbt-app/src/sbt-test/ivy/provided-multi/changes/p.sbt @@ -1,4 +1,5 @@ ThisBuild / scalaVersion := "2.12.21" +ThisBuild / useIvy := true def configIvyScala = scalaModuleInfo ~= (_ map (_ withCheckExplicit false)) diff --git a/sbt-app/src/sbt-test/ivy/provided-multi/project/plugins.sbt b/sbt-app/src/sbt-test/ivy/provided-multi/project/plugins.sbt new file mode 100644 index 000000000..abe6a97a4 --- /dev/null +++ b/sbt-app/src/sbt-test/ivy/provided-multi/project/plugins.sbt @@ -0,0 +1,4 @@ +libraryDependencies += { + val sbtV = sbtVersion.value + ("org.scala-sbt" % s"sbt-ivy_sbt${sbtV}_${scalaBinaryVersion.value}" % sbtV).intransitive() +} diff --git a/sbt-app/src/sbt-test/dependency-management/provided-multi/test b/sbt-app/src/sbt-test/ivy/provided-multi/test similarity index 100% rename from sbt-app/src/sbt-test/dependency-management/provided-multi/test rename to sbt-app/src/sbt-test/ivy/provided-multi/test diff --git a/main/src/main/scala/sbt/internal/librarymanagement/IvyXml.scala b/sbt-ivy/src/main/scala/sbt/internal/librarymanagement/IvyXml.scala similarity index 92% rename from main/src/main/scala/sbt/internal/librarymanagement/IvyXml.scala rename to sbt-ivy/src/main/scala/sbt/internal/librarymanagement/IvyXml.scala index 4c61b40a5..3d195b2f9 100644 --- a/main/src/main/scala/sbt/internal/librarymanagement/IvyXml.scala +++ b/sbt-ivy/src/main/scala/sbt/internal/librarymanagement/IvyXml.scala @@ -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( diff --git a/sbt-ivy/src/main/scala/sbt/plugins/IvyDependencyPlugin.scala b/sbt-ivy/src/main/scala/sbt/plugins/IvyDependencyPlugin.scala new file mode 100644 index 000000000..6cb1234b3 --- /dev/null +++ b/sbt-ivy/src/main/scala/sbt/plugins/IvyDependencyPlugin.scala @@ -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