From 5a0e9b2fe743e1159528314a039a655baf808ffe Mon Sep 17 00:00:00 2001 From: Adrien Piquerez Date: Fri, 23 Dec 2022 16:05:43 +0100 Subject: [PATCH] Try resolve sbt plugin from valid Maven pattern sbt plugins were published to an invalid path with regard to Maven's specifictation. As of sbt 1.9 we produce two POMs, a deprecated one and a new one, that is valid with regard to Maven resolution. When resolving, we first try to resolve the new valid POM and we fallback to the invalid one if needed. In the new POM format, we append the sbt cross-version to all artifactIds of sbt plugins. This is because we want Maven to be able to resolve the plugin and all its dependencies. When parsing it, we remove the cross-version suffix so that the result of parsing the valid POM format is exactly the same as parsing the deprecated POM format. Hence conflict resolution happens as intended. More details can be found at https://github.com/sbt/sbt/pull/7096 --- .../librarymanagement/ConvertResolver.scala | 41 ++++++++++- .../librarymanagement/CustomPomParser.scala | 30 +++++++- .../sbt/internal/librarymanagement/Ivy.scala | 73 +++++++++++++------ .../BaseIvySpecification.scala | 5 +- .../librarymanagement/IvyModuleSpec.scala | 38 ++++++++++ 5 files changed, 162 insertions(+), 25 deletions(-) create mode 100644 ivy/src/test/scala/sbt/internal/librarymanagement/IvyModuleSpec.scala diff --git a/ivy/src/main/scala/sbt/internal/librarymanagement/ConvertResolver.scala b/ivy/src/main/scala/sbt/internal/librarymanagement/ConvertResolver.scala index f682a785e..94e82974a 100644 --- a/ivy/src/main/scala/sbt/internal/librarymanagement/ConvertResolver.scala +++ b/ivy/src/main/scala/sbt/internal/librarymanagement/ConvertResolver.scala @@ -27,10 +27,16 @@ import org.apache.ivy.plugins.resolver.{ import org.apache.ivy.plugins.repository.url.{ URLRepository => URLRepo } import org.apache.ivy.plugins.repository.file.{ FileResource, FileRepository => FileRepo } import java.io.{ File, IOException } +import java.util.Date -import org.apache.ivy.util.{ ChecksumHelper, FileUtil, Message } import org.apache.ivy.core.module.descriptor.{ Artifact => IArtifact } +import org.apache.ivy.core.module.id.ModuleRevisionId +import org.apache.ivy.core.module.descriptor.DefaultArtifact import org.apache.ivy.core.report.DownloadReport +import org.apache.ivy.plugins.resolver.util.{ ResolvedResource, ResourceMDParser } +import org.apache.ivy.util.{ ChecksumHelper, FileUtil, Message } +import scala.collection.JavaConverters._ +import sbt.internal.librarymanagement.mavenint.PomExtraDependencyAttributes import sbt.io.IO import sbt.util.Logger import sbt.librarymanagement._ @@ -173,6 +179,32 @@ private[sbt] object ConvertResolver { setArtifactPatterns(pattern) setIvyPatterns(pattern) } + override protected def findResourceUsingPattern( + mrid: ModuleRevisionId, + pattern: String, + artifact: IArtifact, + rmdparser: ResourceMDParser, + date: Date + ): ResolvedResource = { + val extraAttributes = + mrid.getExtraAttributes.asScala.toMap.asInstanceOf[Map[String, String]] + getSbtPluginCrossVersion(extraAttributes) match { + case Some(sbtCrossVersion) => + // if the module is an sbt plugin + // we first try to resolve the artifact with the sbt cross version suffix + // and we fallback to the one without the suffix + val newArtifact = DefaultArtifact.cloneWithAnotherName( + artifact, + artifact.getName + sbtCrossVersion + ) + val resolved = + super.findResourceUsingPattern(mrid, pattern, newArtifact, rmdparser, date) + if (resolved != null) resolved + else super.findResourceUsingPattern(mrid, pattern, artifact, rmdparser, date) + case None => + super.findResourceUsingPattern(mrid, pattern, artifact, rmdparser, date) + } + } } val resolver = new PluginCapableResolver if (repo.localIfFile) resolver.setRepository(new LocalIfFileRepo) @@ -230,6 +262,13 @@ private[sbt] object ConvertResolver { } } + private def getSbtPluginCrossVersion(extraAttributes: Map[String, String]): Option[String] = { + for { + sbtVersion <- extraAttributes.get(PomExtraDependencyAttributes.SbtVersionKey) + scalaVersion <- extraAttributes.get(PomExtraDependencyAttributes.ScalaVersionKey) + } yield s"_${scalaVersion}_$sbtVersion" + } + private sealed trait DescriptorRequired extends BasicResolver { // Works around implementation restriction to access protected method `get` def getResource(resource: Resource, dest: File): Long diff --git a/ivy/src/main/scala/sbt/internal/librarymanagement/CustomPomParser.scala b/ivy/src/main/scala/sbt/internal/librarymanagement/CustomPomParser.scala index e81ef45cd..3ec3282f6 100644 --- a/ivy/src/main/scala/sbt/internal/librarymanagement/CustomPomParser.scala +++ b/ivy/src/main/scala/sbt/internal/librarymanagement/CustomPomParser.scala @@ -75,6 +75,31 @@ object CustomPomParser { private[this] val unqualifiedKeys = Set(SbtVersionKey, ScalaVersionKey, ExtraAttributesKey, ApiURLKey, VersionSchemeKey) + /** In the new POM format of sbt plugins, the dependency to an sbt plugin + * contains the sbt cross-version _2.12_1.0. The reason is we want Maven to be able + * to resolve the dependency using the pattern: + * /_2.12_1.0//_2.12_1.0-.pom + * In sbt 1.x we use extra-attributes to resolve sbt plugins, so here we must remove + * the sbt cross-version and keep the extra-attributes. + * Parsing a dependency found in the new POM format produces the same module as + * if it is found in the old POM format. It used not to contain the sbt cross-version + * suffix, but that was invalid. + * Hence we can resolve conflicts between new and old POM formats. + * + * To compare the two formats you can look at the POMs in: + * https://repo1.maven.org/maven2/ch/epfl/scala/sbt-plugin-example-diamond_2.12_1.0/0.5.0/ + */ + private def removeSbtCrossVersion( + properties: Map[String, String], + moduleName: String + ): String = { + val sbtCrossVersion = for { + sbtVersion <- properties.get(s"e:$SbtVersionKey") + scalaVersion <- properties.get(s"e:$ScalaVersionKey") + } yield s"_${scalaVersion}_$sbtVersion" + sbtCrossVersion.map(moduleName.stripSuffix).getOrElse(moduleName) + } + // packagings that should be jars, but that Ivy doesn't handle as jars // TODO - move this elsewhere. val JarPackagings = Set("eclipse-plugin", "hk2-jar", "orbit", "scala-jar") @@ -163,9 +188,12 @@ object CustomPomParser { import collection.JavaConverters._ val oldExtra = qualifiedExtra(id) val newExtra = (oldExtra ++ properties).asJava + // remove the sbt plugin cross version from the resolved ModuleRevisionId + // sbt-plugin-example_2.12_1.0 => sbt-plugin-example + val nameWithoutCrossVersion = removeSbtCrossVersion(properties, id.getName) ModuleRevisionId.newInstance( id.getOrganisation, - id.getName, + nameWithoutCrossVersion, id.getBranch, id.getRevision, newExtra diff --git a/ivy/src/main/scala/sbt/internal/librarymanagement/Ivy.scala b/ivy/src/main/scala/sbt/internal/librarymanagement/Ivy.scala index eaab99af1..270d08546 100644 --- a/ivy/src/main/scala/sbt/internal/librarymanagement/Ivy.scala +++ b/ivy/src/main/scala/sbt/internal/librarymanagement/Ivy.scala @@ -219,9 +219,28 @@ final class IvySbt( else IvySbt.cachedResolutionResolveCache.clean() } - final class Module(rawModuleSettings: ModuleSettings) + /** + * In the new POM format of sbt plugins, we append the sbt-cross version _2.12_1.0 to + * the module artifactId, and the artifactIds of its dependencies that are sbt plugins. + * + * The goal is to produce a valid Maven POM, a POM that Maven can resolve: + * Maven will try and succeed to resolve the POM of pattern: + * /_2.12_1.0//_2.12_1.0-.pom + */ + final class Module(rawModuleSettings: ModuleSettings, appendSbtCrossVersion: Boolean) extends sbt.librarymanagement.ModuleDescriptor { self => - val moduleSettings: ModuleSettings = IvySbt.substituteCross(rawModuleSettings) + + def this(rawModuleSettings: ModuleSettings) = + this(rawModuleSettings, appendSbtCrossVersion = false) + + val moduleSettings: ModuleSettings = + rawModuleSettings match { + case ic: InlineConfiguration => + val icWithCross = IvySbt.substituteCross(ic) + if (appendSbtCrossVersion) IvySbt.appendSbtCrossVersion(icWithCross) + else icWithCross + case m => m + } def directDependencies: Vector[ModuleID] = moduleSettings match { @@ -696,32 +715,44 @@ private[sbt] object IvySbt { ) } - private def substituteCross(m: ModuleSettings): ModuleSettings = { - m.scalaModuleInfo match { - case None => m - case Some(is) => substituteCross(m, is.scalaFullVersion, is.scalaBinaryVersion) + private def substituteCross(ic: InlineConfiguration): InlineConfiguration = { + ic.scalaModuleInfo match { + case None => ic + case Some(is) => substituteCross(ic, is.scalaFullVersion, is.scalaBinaryVersion) } } private def substituteCross( - m: ModuleSettings, + ic: InlineConfiguration, scalaFullVersion: String, scalaBinaryVersion: String - ): ModuleSettings = { - m match { - case ic: InlineConfiguration => - val applyCross = CrossVersion(scalaFullVersion, scalaBinaryVersion) - def propagateCrossVersion(moduleID: ModuleID): ModuleID = { - val crossExclusions: Vector[ExclusionRule] = - moduleID.exclusions.map(CrossVersion.substituteCross(_, ic.scalaModuleInfo)) - applyCross(moduleID) - .withExclusions(crossExclusions) - } - ic.withModule(applyCross(ic.module)) - .withDependencies(ic.dependencies.map(propagateCrossVersion)) - .withOverrides(ic.overrides map applyCross) - case _ => m + ): InlineConfiguration = { + val applyCross = CrossVersion(scalaFullVersion, scalaBinaryVersion) + def propagateCrossVersion(moduleID: ModuleID): ModuleID = { + val crossExclusions: Vector[ExclusionRule] = + moduleID.exclusions.map(CrossVersion.substituteCross(_, ic.scalaModuleInfo)) + applyCross(moduleID) + .withExclusions(crossExclusions) } + ic.withModule(applyCross(ic.module)) + .withDependencies(ic.dependencies.map(propagateCrossVersion)) + .withOverrides(ic.overrides map applyCross) + } + + private def appendSbtCrossVersion(ic: InlineConfiguration): InlineConfiguration = + ic.withModule(appendSbtCrossVersion(ic.module)) + .withDependencies(ic.dependencies.map(appendSbtCrossVersion)) + .withOverrides(ic.overrides.map(appendSbtCrossVersion)) + + private def appendSbtCrossVersion(mid: ModuleID): ModuleID = { + val crossVersion = for { + scalaVersion <- mid.extraAttributes.get("e:scalaVersion") + sbtVersion <- mid.extraAttributes.get("e:sbtVersion") + } yield s"_${scalaVersion}_$sbtVersion" + crossVersion + .filter(!mid.name.endsWith(_)) + .map(cv => mid.withName(mid.name + cv)) + .getOrElse(mid) } private def toIvyArtifact( diff --git a/ivy/src/test/scala/sbt/internal/librarymanagement/BaseIvySpecification.scala b/ivy/src/test/scala/sbt/internal/librarymanagement/BaseIvySpecification.scala index f6bcba917..a1bd6d31f 100644 --- a/ivy/src/test/scala/sbt/internal/librarymanagement/BaseIvySpecification.scala +++ b/ivy/src/test/scala/sbt/internal/librarymanagement/BaseIvySpecification.scala @@ -37,7 +37,8 @@ trait BaseIvySpecification extends AbstractEngineSpec { deps: Vector[ModuleID], scalaFullVersion: Option[String], uo: UpdateOptions = UpdateOptions(), - overrideScalaVersion: Boolean = true + overrideScalaVersion: Boolean = true, + appendSbtCrossVersion: Boolean = false ): IvySbt#Module = { val scalaModuleInfo = scalaFullVersion map { fv => ScalaModuleInfo( @@ -55,7 +56,7 @@ trait BaseIvySpecification extends AbstractEngineSpec { .withConfigurations(configurations) .withScalaModuleInfo(scalaModuleInfo) val ivySbt = new IvySbt(mkIvyConfiguration(uo)) - new ivySbt.Module(moduleSetting) + new ivySbt.Module(moduleSetting, appendSbtCrossVersion) } def resolvers: Vector[Resolver] = Vector(Resolver.mavenCentral) diff --git a/ivy/src/test/scala/sbt/internal/librarymanagement/IvyModuleSpec.scala b/ivy/src/test/scala/sbt/internal/librarymanagement/IvyModuleSpec.scala new file mode 100644 index 000000000..ac9474912 --- /dev/null +++ b/ivy/src/test/scala/sbt/internal/librarymanagement/IvyModuleSpec.scala @@ -0,0 +1,38 @@ +package sbt.internal.librarymanagement + +import sbt.internal.librarymanagement.mavenint.PomExtraDependencyAttributes.{ + SbtVersionKey, + ScalaVersionKey +} +import sbt.librarymanagement.{ CrossVersion, ModuleDescriptorConfiguration } + +object IvyModuleSpec extends BaseIvySpecification { + + test("The Scala binary version of a Scala module should be appended to its name") { + val m = module( + defaultModuleId.withCrossVersion(CrossVersion.Binary()), + Vector.empty, + Some("2.13.10") + ) + m.moduleSettings match { + case configuration: ModuleDescriptorConfiguration => + assert(configuration.module.name == "foo_2.13") + case _ => fail() + } + } + + test("The sbt cross-version should be appended to the name of an sbt plugin") { + val m = module( + defaultModuleId.extra(SbtVersionKey -> "1.0", ScalaVersionKey -> "2.12"), + Vector.empty, + Some("2.12.17"), + appendSbtCrossVersion = true + ) + m.moduleSettings match { + case configuration: ModuleDescriptorConfiguration => + assert(configuration.module.name == "foo_2.12_1.0") + case _ => fail() + } + } + +}