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() + } + } + +}