import sbt._ import java.io.File import scala.xml.NodeSeq /** Support for compiling sbt across multiple versions of Scala. The scala compiler is run in a * separate JVM and no partial compilation is done.*/ abstract class CrossCompileProject extends BasicScalaProject with MavenStyleScalaPaths { /** Used for 2.8.0-SNAPSHOT*/ val scalaToolsSnapshots = "Scala Tools Snapshots" at "http://scala-tools.org/repo-snapshots" /* The base configuration names for the versions of Scala*/ private val version2_7_2 = "2.7.2" private val version2_7_3 = "2.7.3" private val version2_7_4 = "2.7.4" private val version2_7_5 = "2.7.5" private val version2_8_0 = "2.8.0.r18093-b20090623200909" private val base = "base" /* The configurations for the versions of Scala.*/ private val conf_2_7_2 = config(version2_7_2) private val conf_2_7_3 = config(version2_7_3) private val conf_2_7_4 = config(version2_7_4) private val conf_2_7_5 = config(version2_7_5) private val conf_2_8_0 = config(version2_8_0) private val conf_base = config(base) // the list of all configurations cross-compile supports private val allConfigurations = conf_2_7_2 :: conf_2_7_3 :: conf_2_7_4 :: conf_2_7_5 :: conf_2_8_0 :: Nil // the list of configurations to actually build against private val buildConfigurations = conf_2_7_2 :: conf_2_7_3 :: conf_2_7_4 :: conf_2_7_5 :: Nil//allConfigurations not currently used because of issues with 2.8.0 // the configuration to use for normal development (when cross-building is not done) private def developmentVersion = buildConfigurations.first /* Methods to derive the configuration name from the base name 'v'.*/ private def optional(v: Configuration) = config("optional-" + v.toString) private def scalac(v: Configuration) = config("scalac-" + v.toString) private def sbt(v: Configuration) = config("sbt_" + v.toString) private def depConf(v: Configuration) = v.toString + "->default" // =========== Cross-compilation across scala versions =========== // The dependencies that should go in each configuration are: // base Required dependencies that are the same across all scala versions. // Required dependencies to use with Scala // optional-base Optional dependencies that are the same for all scala versions // optional- Optional dependencies to use with Scala // compile Used for normal development, it should extend a specific and optional- // scalac- The scala compiler for Scala // There should be a jar publication for each version of scala. The artifact should be named sbt_. override def ivyXML = ( { variableConfigurations } { publications } { variableDependencies(conf_2_7_2, /*ScalaTest*/"0.9.3", /*Specs*/"1.4.0", false) } { variableDependencies(conf_2_7_3, /*ScalaTest*/"0.9.4", /*Specs*/"1.4.3", true) } { variableDependencies(conf_2_7_4, /*ScalaTest*/"0.9.5", /*Specs*/"1.4.3", true) } { variableDependencies(conf_2_7_5, /*ScalaTest*/"0.9.5", /*Specs*/"1.4.3", true) } { variableDependencies(conf_2_8_0, /*ScalaTest*/"0.9.5", /*Specs*/"1.4.3", true) } ) /** Creates a publication (an 'artifact' element) for each Scala version */ private def publications: NodeSeq = { for(conf <- buildConfigurations) yield } /** Creates the main, optional, and scalac configurations for each Scala version*/ private def variableConfigurations: NodeSeq = { buildConfigurations flatMap { conf => scalaComment(conf) ++ ( ) } } /** Defines the dependencies for the given version of Scala, ScalaTest, and Specs. If uniformTestOrg is true, * the 'org.scala-tools.testing' organization is used. Otherwise, 'org.' is prefixed to the module name. */ private def variableDependencies(scalaVersion: Configuration, scalaTestVersion: String, specsVersion: String, uniformTestOrg: Boolean) = { if(buildConfigurations.contains(scalaVersion)) { scalaComment(scalaVersion) ++ { if(scalaVersion eq conf_2_8_0) Nil else { testDependency("scalatest", scalaTestVersion, uniformTestOrg, scalaVersion) ++ testDependency("specs", specsVersion, uniformTestOrg, scalaVersion) ++ testDependency("scalacheck", "1.5", false, scalaVersion) } } ++ scalaDependency("scala-compiler", scalaVersion) ++ scalaDependency("scala-library", scalaVersion) ++ { if(scalaVersion == conf_2_8_0) else NodeSeq.Empty } } else Nil } private def scalaDependency(name: String, scalaVersion: Configuration) = /** Creates a comment containing the version of Scala*/ private def scalaComment(scalaVersion: Configuration) = scala.xml.Comment("Scala " + scalaVersion) /** Creates a dependency element for a test. See 'testOrg' for a description of uniformTestOrg.*/ private def testDependency(name: String, version: String, uniformTestOrg: Boolean, baseConf: Configuration) = /** Returns the organization for the given test library. If uniform is true, * the 'org.scala-tools.testing' organization is used. Otherwise, 'org.' is prefixed to the module name.*/ private def testOrg(name: String, uniform: Boolean) = if(uniform) "org.scala-tools.testing" else "org." + name /** Disable filtering Scala jars from dependency management, because we need them and are putting them * in custom configurations and are using them in a separate process than sbt runs in.*/ override def filterScalaJars = false /** The lib directory is now only for building using the 'build' script.*/ override def unmanagedClasspath = path("ignore_lib_directory") /** When cross-compiling, replace mainCompilePath with the classes directory for the version being compiled.*/ override def fullUnmanagedClasspath(config: Configuration) = if( (Configurations.Default :: Configurations.defaultMavenConfigurations) contains config) super.fullUnmanagedClasspath(config) else classesPath(config) +++ mainResourcesPath // include the optional- dependencies as well as the ones common across all scala versions def optionalClasspath(version: Configuration) = fullClasspath(optional(version)) +++ super.optionalClasspath private val CompilerMainClass = "scala.tools.nsc.Main" // use a publish configuration that publishes the 'base' + all configurations (base is required because // the configurations extend it) private val conf = new DefaultPublishConfiguration("local", "release") { override def configurations: Option[Iterable[Configuration]] = Some(config(base) :: buildConfigurations) } // the actions for cross-version packaging and publishing lazy val crossPackage = buildConfigurations.map(packageForScala) lazy val crossDeliverLocal = deliverTask(conf, updateOptions) dependsOn(crossPackage : _*) lazy val crossPublishLocal = publishTask(conf, updateOptions) dependsOn(crossDeliverLocal) // Creates a task that produces a packaged sbt compiled against Scala scalaVersion. // The jar is named 'sbt_-.jar' private def packageForScala(scalaVersion: Configuration) = { val classes = classesPath(scalaVersion) ** "*" val jarName = crossJarName(scalaVersion) val packageActionName = crossActionName("package", scalaVersion) val compileAction = compileForScala(scalaVersion) named(crossActionName("compile", scalaVersion)) packageTask(classes +++ mainResources, outputPath, jarName, packageOptions) dependsOn(compileAction) named(packageActionName) } private def crossActionName(base: String, scalaVersion: Configuration) = base + " [ " + scalaVersion.toString + " ] " private def crossJarName(scalaVersion: Configuration) = sbt(scalaVersion) + "-" + version.toString + ".jar" // This creates a task that compiles sbt against the given version of scala. Classes are put in classes-. private def compileForScala(version: Configuration)= task { val classes = classesPath(version) val toClean = (outputPath / crossJarName(version)) +++ (classes ** "*") val setupResult = FileUtilities.clean(toClean.get, true, log) orElse FileUtilities.createDirectory(classes, log) for(err <- setupResult) log.error(err) // the classpath containing the scalac compiler val compilerClasspath = concatPaths(fullClasspath(scalac(version))) // The libraries to compile sbt against val classpath = fullClasspath(version) +++ optionalClasspath(version) val sources: List[String] = pathListStrings(mainSources) val compilerOptions = List("-cp", concatPaths(classpath), "-d", classes.toString) val compilerArguments: List[String] = compilerOptions ::: sources // the compiler classpath has to be appended to the boot classpath to work properly val allArguments = "-Xmx512M" :: ("-Xbootclasspath/a:" + compilerClasspath) :: CompilerMainClass :: compilerArguments log.debug("Running external compiler with command: java " + allArguments.mkString(" ")) val exitValue = Process("java", allArguments) ! log if(exitValue == 0) None else Some("Nonzero exit value (" + exitValue + ") when calling scalac " + version + " with options: \n" + compilerOptions.mkString(" ")) } private def concatPaths(p: PathFinder): String = Path.makeString(p.get) private def pathListStrings(p: PathFinder): List[String] = p.get.map(_.absolutePath).toList private def classesPath(scalaVersion: Configuration) = ("target" / ("classes-" + scalaVersion.toString)) ## }