From c4eb6705886a7462668e3a1647386557df299f8d Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Wed, 15 Jul 2015 10:01:11 +0200 Subject: [PATCH] Find most specific version of compiler interface sources This commit introduces a mechanism that allows sbt to find the most specific version of the compiler interface sources that exists using Ivy. For instance, when asked for a compiler interface for Scala 2.11.8-M2, sbt will look for sources for: - 2.11.8-M2 ; - 2.11.8 ; - 2.11 ; - the default sources. This commit also modifies the build definition by removing the precompiled projects and configuring the compiler-interface project so that it publishes its source artifacts in a Maven-friendly format. --- .../sbt/compiler/ComponentCompiler.scala | 143 +++++++++++++++++- 1 file changed, 142 insertions(+), 1 deletion(-) diff --git a/compile/ivy/src/main/scala/sbt/compiler/ComponentCompiler.scala b/compile/ivy/src/main/scala/sbt/compiler/ComponentCompiler.scala index 24d4f0530..3192e42c6 100644 --- a/compile/ivy/src/main/scala/sbt/compiler/ComponentCompiler.scala +++ b/compile/ivy/src/main/scala/sbt/compiler/ComponentCompiler.scala @@ -5,6 +5,7 @@ package sbt package compiler import java.io.File +import scala.util.Try object ComponentCompiler { val xsbtiID = "xsbti" @@ -14,6 +15,7 @@ object ComponentCompiler { val compilerInterfaceSrcID = compilerInterfaceID + srcExtension val javaVersion = System.getProperty("java.class.version") + @deprecated("Use `interfaceProvider(ComponentManager, IvyConfiguration)`.", "0.13.10") def interfaceProvider(manager: ComponentManager): CompilerInterfaceProvider = new CompilerInterfaceProvider { def apply(scalaInstance: xsbti.compile.ScalaInstance, log: Logger): File = { @@ -23,12 +25,23 @@ object ComponentCompiler { componentCompiler(compilerInterfaceID) } } + + def interfaceProvider(manager: ComponentManager, ivyConfiguration: IvyConfiguration): CompilerInterfaceProvider = new CompilerInterfaceProvider { + def apply(scalaInstance: xsbti.compile.ScalaInstance, log: Logger): File = + { + // this is the instance used to compile the interface component + val componentCompiler = new IvyComponentCompiler(new RawCompiler(scalaInstance, ClasspathOptions.auto, log), manager, ivyConfiguration, log) + log.debug("Getting " + compilerInterfaceID + " from component compiler for Scala " + scalaInstance.version) + componentCompiler(compilerInterfaceID) + } + } } /** * This class provides source components compiled with the provided RawCompiler. * The compiled classes are cached using the provided component manager according * to the actualVersion field of the RawCompiler. */ +@deprecated("Replaced by IvyComponentCompiler.", "0.13.10") class ComponentCompiler(compiler: RawCompiler, manager: ComponentManager) { import ComponentCompiler._ def apply(id: String): File = @@ -64,4 +77,132 @@ class ComponentCompiler(compiler: RawCompiler, manager: ComponentManager) { manager.define(binID, Seq(targetJar)) } } -} \ No newline at end of file +} + +/** + * Component compiler which is able to find the most specific version available of + * the compiler interface sources using Ivy. + * The compiled classes are cached using the provided component manager according + * to the actualVersion field of the RawCompiler. + */ +private[compiler] class IvyComponentCompiler(compiler: RawCompiler, manager: ComponentManager, ivyConfiguration: IvyConfiguration, log: Logger) { + import ComponentCompiler._ + + private val sbtOrgTemp = JsonUtil.sbtOrgTemp + private val modulePrefixTemp = "temp-module-" + private val ivySbt: IvySbt = new IvySbt(ivyConfiguration) + private val sbtVersion = ComponentManager.version + + def apply(id: String): File = { + val binID = binaryID(id) + manager.file(binID)(new IfMissing.Define(true, compileAndInstall(id, binID))) + } + + private def binaryID(id: String): String = { + val base = id + binSeparator + compiler.scalaInstance.actualVersion + base + "__" + javaVersion + } + + private def compileAndInstall(id: String, binID: String): Unit = { + def interfaceSources(moduleVersions: Vector[VersionNumber]): Iterable[File] = + moduleVersions match { + case Vector() => + def getAndDefineDefaultSources() = + update(getModule(id))(_.getName endsWith "-sources.jar") map { sourcesJar => + manager.define(id, sourcesJar) + sourcesJar + } getOrElse (throw new InvalidComponent(s"Couldn't retrieve default sources: module '$id'")) + + log.debug(s"Fetching default sources: module '$id'") + manager.files(id)(new IfMissing.Fallback(getAndDefineDefaultSources())) + + case version +: rest => + val moduleName = s"${id}_$version" + def getAndDefineVersionSpecificSources() = + update(getModule(moduleName))(_.getName endsWith "-sources.jar") map { sourcesJar => + manager.define(moduleName, sourcesJar) + sourcesJar + } getOrElse interfaceSources(rest) + + log.debug(s"Fetching version-specific sources: module '$moduleName'") + manager.files(moduleName)(new IfMissing.Fallback(getAndDefineVersionSpecificSources())) + } + IO.withTemporaryDirectory { binaryDirectory => + + val targetJar = new File(binaryDirectory, s"$binID.jar") + val xsbtiJars = manager.files(xsbtiID)(IfMissing.Fail) + + val sourceModuleVersions = VersionNumber(compiler.scalaInstance.actualVersion).cascadingVersions + AnalyzingCompiler.compileSources(interfaceSources(sourceModuleVersions), targetJar, xsbtiJars, id, compiler, log) + + manager.define(binID, Seq(targetJar)) + + } + } + + /** + * Returns a dummy module that depends on "org.scala-sbt" % `id` % `sbtVersion`. + * Note: Sbt's implementation of Ivy requires us to do this, because only the dependencies + * of the specified module will be downloaded. + */ + private def getModule(id: String): ivySbt.Module = { + val sha1 = Hash.toHex(Hash(id)) + val dummyID = ModuleID(sbtOrgTemp, modulePrefixTemp + sha1, sbtVersion, Some("component")) + val moduleID = ModuleID(xsbti.ArtifactInfo.SbtOrganization, id, sbtVersion, Some("component")).sources() + getModule(dummyID, Seq(moduleID)) + } + + private def getModule(moduleID: ModuleID, deps: Seq[ModuleID], uo: UpdateOptions = UpdateOptions()): ivySbt.Module = { + val moduleSetting = InlineConfiguration( + module = moduleID, + moduleInfo = ModuleInfo(moduleID.name), + dependencies = deps, + configurations = Seq(Configurations.Component), + ivyScala = None) + + new ivySbt.Module(moduleSetting) + } + + private def dependenciesNames(module: ivySbt.Module): String = module.moduleSettings match { + // `module` is a dummy module, we will only fetch its dependencies. + case ic: InlineConfiguration => + ic.dependencies map { + case mID: ModuleID => + import mID._ + s"$organization % $name % $revision" + } mkString ", " + case _ => + s"unknown" + } + + private def update(module: ivySbt.Module)(predicate: File => Boolean): Option[Seq[File]] = { + + val retrieveDirectory = new File(ivyConfiguration.baseDirectory, "component") + val retrieveConfiguration = new RetrieveConfiguration(retrieveDirectory, Resolver.defaultRetrievePattern, false) + val updateConfiguration = new UpdateConfiguration(Some(retrieveConfiguration), true, UpdateLogging.DownloadOnly) + + log.info(s"Attempting to fetch ${dependenciesNames(module)}. This operation may fail.") + IvyActions.updateEither(module, updateConfiguration, UnresolvedWarningConfiguration(), LogicalClock.unknown, None, log) match { + case Left(unresolvedWarning) => + log.debug("Couldn't retrieve module ${dependenciesNames(module)}.") + None + + case Right(updateReport) => + val allFiles = + for { + conf <- updateReport.configurations + m <- conf.modules + (_, f) <- m.artifacts + } yield f + + log.debug(s"Files retrieved for ${dependenciesNames(module)}:") + log.debug(allFiles mkString ", ") + + allFiles filter predicate match { + case Seq() => None + case files => Some(files) + } + + } + } +}