Compilation with analysis independent of Scala version of sbt.

Basic test for this.
This commit is contained in:
Mark Harrah 2009-08-23 22:21:15 -04:00
parent efb1604f0e
commit 50d350abd0
24 changed files with 426 additions and 149 deletions

View File

@ -9,42 +9,50 @@ import java.net.URLClassLoader
* these call Scala code for a different Scala version, they must be compiled for the version of Scala being used.
* It is essential that the provided 'scalaVersion' be a 1:1 mapping to the actual version of Scala being used for compilation
* (-SNAPSHOT is not acceptable). Otherwise, binary compatibility issues will ensue!*/
class Compiler(scalaLoader: ClassLoader, val scalaVersion: String, private[xsbt] val manager: ComponentManager)
{
// this is the instance used to compile the analysis
lazy val componentCompiler = new ComponentCompiler(this)
/** A basic interface to the compiler. It is called in the same virtual machine, but no dependency analysis is done. This
* is used, for example, to compile the interface/plugin code.*/
object raw
{
def apply(arguments: Seq[String])
{
// reflection is required for binary compatibility
// The following import ensures there is a compile error if the class name changes,
// but it should not be otherwise directly referenced
import scala.tools.nsc.Main
val mainClass = Class.forName("scala.tools.nsc.Main", true, scalaLoader)
val main = mainClass.asInstanceOf[{def process(args: Array[String]): Unit }]
main.process(arguments.toArray)
}
}
/** Interface to the compiler that uses the dependency analysis plugin.*/
object analysis
/** A basic interface to the compiler. It is called in the same virtual machine, but no dependency analysis is done. This
* is used, for example, to compile the interface/plugin code.*/
class RawCompiler(scalaLoader: ClassLoader)
{
def apply(arguments: Seq[String])
{
/** The jar containing the compiled plugin and the compiler interface code. This will be passed to scalac as a compiler plugin
* and used to load the class that actually interfaces with Global.*/
private lazy val interfaceJar = componentCompiler(ComponentCompiler.compilerInterfaceID)
def apply(arguments: Seq[String], callback: AnalysisCallback, maximumErrors: Int, log: Logger)
{
val argsWithPlugin = ("-Xplugin:" + interfaceJar.getAbsolutePath) :: arguments.toList
val interfaceLoader = new URLClassLoader(Array(interfaceJar.toURI.toURL), scalaLoader)
val interface = Class.forName("xsbt.CompilerInterface", true, interfaceLoader).newInstance
val runnable = interface.asInstanceOf[{ def run(args: Array[String], callback: AnalysisCallback, maximumErrors: Int, log: Logger): Unit }]
// these arguments are safe to pass across the ClassLoader boundary because the types are defined in Java
// so they will be binary compatible across all versions of Scala
runnable.run(argsWithPlugin.toArray, callback, maximumErrors, log)
}
def forceInitialization() {interfaceJar }
// reflection is required for binary compatibility
// The following import ensures there is a compile error if the class name changes,
// but it should not be otherwise directly referenced
import scala.tools.nsc.Main
val mainClass = Class.forName("scala.tools.nsc.Main", true, scalaLoader)
val process = mainClass.getMethod("process", classOf[Array[String]])
val realArray: Array[String] = arguments.toArray
assert(realArray.getClass eq classOf[Array[String]])
process.invoke(null, realArray)
}
}
/** Interface to the compiler that uses the dependency analysis plugin.*/
class AnalyzeCompile(scalaVersion: String, scalaLoader: ClassLoader, manager: ComponentManager) extends NotNull
{
def this(scalaVersion: String, provider: xsbti.ScalaProvider, manager: ComponentManager) =
this(scalaVersion, provider.getScalaLoader(scalaVersion), manager)
/** The jar containing the compiled plugin and the compiler interface code. This will be passed to scalac as a compiler plugin
* and used to load the class that actually interfaces with Global.*/
def apply(arguments: Seq[String], callback: AnalysisCallback, maximumErrors: Int, log: Logger)
{
// this is the instance used to compile the analysis
val componentCompiler = new ComponentCompiler(scalaVersion, new RawCompiler(scalaLoader), manager)
val interfaceJar = componentCompiler(ComponentCompiler.compilerInterfaceID)
val argsWithPlugin = ("-Xplugin:" + interfaceJar.getAbsolutePath) :: arguments.toList
val dual = createDualLoader(scalaLoader, getClass.getClassLoader) // this goes to scalaLoader for scala classes and sbtLoader for xsbti classes
val interfaceLoader = new URLClassLoader(Array(interfaceJar.toURI.toURL), dual)
val interface = Class.forName("xsbt.CompilerInterface", true, interfaceLoader).newInstance
val runnable = interface.asInstanceOf[{ def run(args: Array[String], callback: AnalysisCallback, maximumErrors: Int, log: Logger): Unit }]
// these arguments are safe to pass across the ClassLoader boundary because the types are defined in Java
// so they will be binary compatible across all versions of Scala
runnable.run(argsWithPlugin.toArray, callback, maximumErrors, log)
}
private def createDualLoader(scalaLoader: ClassLoader, sbtLoader: ClassLoader): ClassLoader =
{
val xsbtiFilter = (name: String) => name.startsWith("xsbti.")
val notXsbtiFilter = (name: String) => !xsbtiFilter(name)
new DualLoader(scalaLoader, notXsbtiFilter, x => true, sbtLoader, xsbtiFilter, x => false)
}
}

View File

@ -5,42 +5,43 @@ import java.io.File
object ComponentCompiler
{
val xsbtiID = "xsbti"
val compilerInterfaceID = "compilerInterface"
val srcExtension = "-src"
val binSeparator = "-bin_"
val compilerInterfaceID = "compiler-interface"
val compilerInterfaceSrcID = compilerInterfaceID + srcExtension
}
class ComponentCompiler(compiler: Compiler)
class ComponentCompiler(scalaVersion: String, compiler: RawCompiler, manager: ComponentManager)
{
import compiler.{manager, scalaVersion}
import ComponentCompiler._
lazy val xsbtiJars = manager.files(xsbtiID)
import FileUtilities.{copy, createDirectory, zip, jars, unzip, withTemporaryDirectory}
def apply(id: String): File =
{
val binID = id + "-bin_" + scalaVersion
val binID = binaryID(id, scalaVersion)
try { manager.file(binID) }
catch { case e: Exception => compileAndInstall(id, binID) }
catch { case e: InvalidComponent => compileAndInstall(id, binID) }
}
private def binaryID(id: String, scalaVersion: String) = id + binSeparator + scalaVersion
private def compileAndInstall(id: String, binID: String): File =
{
val srcID = id + "-src_" + scalaVersion
val srcID = id + srcExtension
val binaryDirectory = manager.location(binID)
createDirectory(binaryDirectory)
val targetJar = new File(binaryDirectory, id + ".jar")
compileSources(manager.files(srcID), compiler, targetJar, id)
compileSources(manager.files(srcID), targetJar, id)
manager.cache(binID)
targetJar
}
/** Extract sources from source jars, compile them with the xsbti interfaces on the classpath, and package the compiled classes and
* any resources from the source jars into a final jar.*/
private def compileSources(sourceJars: Iterable[File], compiler: Compiler, targetJar: File, id: String)
private def compileSources(sourceJars: Iterable[File], targetJar: File, id: String)
{
withTemporaryDirectory { dir =>
val extractedSources = (Set[File]() /: sourceJars) { (extracted, sourceJar)=> extracted ++ unzip(sourceJar, dir) }
val (sourceFiles, resources) = extractedSources.partition(_.getName.endsWith(".scala"))
withTemporaryDirectory { outputDirectory =>
val xsbtiJars = manager.files(xsbtiID)
val arguments = Seq("-d", outputDirectory.getAbsolutePath, "-cp", xsbtiJars.mkString(File.pathSeparator)) ++ sourceFiles.toSeq.map(_.getAbsolutePath)
compiler.raw(arguments)
compiler(arguments)
copy(resources, outputDirectory, PathMapper.relativeTo(dir))
zip(Seq(outputDirectory), targetJar, true, PathMapper.relativeTo(outputDirectory))
}

View File

@ -39,24 +39,4 @@ object CallbackTest
TestCompile(newArgs, superclassNames) { case (callback, log) => f(callback, outputDir, log) }
}
}
}
object WithFiles
{
/** Takes the relative path -> content pairs and writes the content to a file in a temporary directory. The written file
* path is the relative path resolved against the temporary directory path. The provided function is called with the resolved file paths
* in the same order as the inputs. */
def apply[T](sources: (File, String)*)(f: Seq[File] => T): T =
{
withTemporaryDirectory { dir =>
val sourceFiles =
for((file, content) <- sources) yield
{
assert(!file.isAbsolute)
val to = new File(dir, file.getPath)
write(to, content)
to
}
f(sourceFiles)
}
}
}

View File

@ -0,0 +1,50 @@
package xsbt
import java.io.File
import FileUtilities.withTemporaryDirectory
import org.specs._
// compile w/ analysis a bit hard to test properly right now:
// requires compile project to depend on +publish-local, which is not possible in sbt (addressed in xsbt, but that doesn't help here!)
object CompileTest extends Specification
{
"Analysis compiler" should {
"compile basic sources" in {
TestIvyLogger { log =>
LoadHelpers.withLaunch { launch =>
val scalaVersion = "2.7.2"
val sbtVersion = xsbti.Versions.Sbt
val manager = new ComponentManager(launch.getSbtHome(sbtVersion, scalaVersion), log)
prepare(manager, ComponentCompiler.compilerInterfaceSrcID, "CompilerInterface.scala")
prepare(manager, ComponentCompiler.xsbtiID, classOf[xsbti.AnalysisCallback])
testCompileAnalysis(new AnalyzeCompile(scalaVersion, launch, manager), log)
}
}
}
}
private def testCompileAnalysis(compiler: AnalyzeCompile, log: xsbti.Logger)
{
WithFiles( new File("Test.scala") -> "object Test" ) { sources =>
withTemporaryDirectory { temp =>
val arguments = "-d" :: temp.getAbsolutePath :: sources.map(_.getAbsolutePath).toList
val callback = new xsbti.TestCallback(Array())
compiler(arguments, callback, 10, log)
(callback.beganSources) must haveTheSameElementsAs(sources)
}
}
}
private def prepare(manager: ComponentManager, id: String, resource: Class[_]): Unit =
{
val src = FileUtilities.classLocationFile(resource)
prepare(manager, id, src)
}
private def prepare(manager: ComponentManager, id: String, resource: String): Unit =
{
val src = getClass.getClassLoader.getResource(resource)
if(src eq null)
error("Resource not found: " + resource)
prepare(manager, id, FileUtilities.asFile(src))
}
private def prepare(manager: ComponentManager, id: String, file: File): Unit =
FileUtilities.copy(Seq(file), manager.location(id), PathMapper.flat)
}

View File

@ -3,7 +3,7 @@
*/
package xsbt.boot
import BootConfiguration.SbtMainClass
import BootConfiguration.{SbtMainClass, SbtModuleName}
import java.io.File
// The entry point to the launcher
@ -12,7 +12,7 @@ object Boot
def main(args: Array[String])
{
CheckProxy()
try { (new Launch(new File("."), SbtMainClass)).boot(args) }
try { Launch(args) }
catch
{
case b: BootException => errorAndExit(b)

View File

@ -15,10 +15,11 @@ private object BootConfiguration
// these are the module identifiers to resolve/retrieve
val ScalaOrg = "org.scala-lang"
val SbtOrg = "sbt"
val SbtOrg = "org.scala-tools.sbt"
val CompilerModuleName = "scala-compiler"
val LibraryModuleName = "scala-library"
val SbtModuleName = "simple-build-tool"
val SbtModuleName = "xsbt"
val MainSbtComponentID = "default"
/** The Ivy conflict manager to use for updating.*/
val ConflictManagerName = "strict"
@ -38,7 +39,7 @@ private object BootConfiguration
* and the project definition*/
val IvyPackage = "org.apache.ivy."
/** The class name prefix used to hide the sbt launcher classes from sbt and the project definition.
* Note that access to xsbti.boot classes are allowed.*/
* Note that access to xsbti classes are allowed.*/
val SbtBootPackage = "xsbt.boot."
/** The loader will check that these classes can be loaded and will assume that their presence indicates
* sbt and its dependencies have been downloaded.*/
@ -69,7 +70,7 @@ private object BootConfiguration
/** The Ivy pattern to use for resolving sbt and its dependencies from the Google code project.*/
def sbtResolverPattern(scalaVersion: String) = sbtRootBase + "[revision]/[type]s/[artifact].[ext]"
/** The name of the directory to retrieve sbt and its dependencies to.*/
def sbtDirectoryName(sbtVersion: String) = SbtOrg + "-" + sbtVersion
def sbtDirectoryName(sbtVersion: String) = SbtModuleName + "-" + sbtVersion
/** The name of the directory in the boot directory to put all jars for the given version of scala in.*/
def baseDirectoryName(scalaVersion: String) = "scala-" + scalaVersion
}

View File

@ -7,7 +7,7 @@ import BootConfiguration.{IvyPackage, SbtBootPackage, ScalaPackage}
/** A custom class loader to ensure the main part of sbt doesn't load any Scala or
* Ivy classes from the jar containing the loader. */
private[boot] final class BootFilteredLoader extends ClassLoader with NotNull
private[boot] final class BootFilteredLoader(parent: ClassLoader) extends ClassLoader(parent) with NotNull
{
@throws(classOf[ClassNotFoundException])
override final def loadClass(className: String, resolve: Boolean): Class[_] =
@ -17,4 +17,6 @@ private[boot] final class BootFilteredLoader extends ClassLoader with NotNull
else
super.loadClass(className, resolve)
}
override def getResources(name: String) = null
override def getResource(name: String) = null
}

View File

@ -11,7 +11,7 @@
import java.io.{File, FileFilter}
import java.net.URLClassLoader
import xsbti.boot.{Exit => IExit, Launcher, MainResult, Reboot, SbtConfiguration, SbtMain}
import xsbti.{Exit => IExit, Launcher, MainResult, Reboot, SbtConfiguration, SbtMain}
// contains constants and paths
import BootConfiguration._
@ -20,6 +20,9 @@ import UpdateTarget.{UpdateScala, UpdateSbt}
class Launch(projectRootDirectory: File, mainClassName: String) extends Launcher with NotNull
{
def this(projectRootDirectory: File) = this(projectRootDirectory, SbtMainClass)
def this() = this(new File("."))
import Launch._
final def boot(args: Array[String])
{
@ -43,9 +46,20 @@ class Launch(projectRootDirectory: File, mainClassName: String) extends Launcher
* this loader from being seen by the loaded sbt/project.*/
def load(args: Array[String]): MainResult =
{
val (definitionScalaVersion, useSbtVersion) = ProjectProperties.forcePrompt(PropertiesFile)//, forcePrompt : _*)
val scalaLoader = getScalaLoader(definitionScalaVersion)
val sbtLoader = createSbtLoader(useSbtVersion, definitionScalaVersion, scalaLoader)
val (scalaVersion, sbtVersion) = ProjectProperties.forcePrompt(PropertiesFile)//, forcePrompt : _*)
load(args, sbtVersion, mainClassName, scalaVersion)
}
def componentLocation(sbtVersion: String, id: String, scalaVersion: String): File = new File(getSbtHome(sbtVersion, scalaVersion), id)
def getSbtHome(sbtVersion: String, scalaVersion: String): File =
{
val baseDirectory = new File(BootDirectory, baseDirectoryName(scalaVersion))
new File(baseDirectory, sbtDirectoryName(sbtVersion))
}
def getScalaHome(scalaVersion: String) = new File(new File(BootDirectory, baseDirectoryName(scalaVersion)), ScalaDirectoryName)
def load(args: Array[String], useSbtVersion: String, mainClassName: String, definitionScalaVersion: String): MainResult =
{
val sbtLoader = update(definitionScalaVersion, useSbtVersion)
val configuration = new SbtConfiguration
{
def arguments = args
@ -53,15 +67,21 @@ class Launch(projectRootDirectory: File, mainClassName: String) extends Launcher
def sbtVersion = useSbtVersion
def launcher: Launcher = Launch.this
}
run(sbtLoader, configuration)
run(sbtLoader, mainClassName, configuration)
}
def run(sbtLoader: ClassLoader, configuration: SbtConfiguration): MainResult =
def update(scalaVersion: String, sbtVersion: String): ClassLoader =
{
val scalaLoader = getScalaLoader(scalaVersion)
createSbtLoader(sbtVersion, scalaVersion, scalaLoader)
}
def run(sbtLoader: ClassLoader, mainClassName: String, configuration: SbtConfiguration): MainResult =
{
val sbtMain = Class.forName(mainClassName, true, sbtLoader)
val main = sbtMain.newInstance.asInstanceOf[SbtMain]
main.run(configuration)
}
final val ProjectDirectory = new File(projectRootDirectory, ProjectDirectoryName)
final val BootDirectory = new File(ProjectDirectory, BootDirectoryName)
final val PropertiesFile = new File(ProjectDirectory, BuildPropertiesName)
@ -87,12 +107,11 @@ class Launch(projectRootDirectory: File, mainClassName: String) extends Launcher
Some(new Exit(1))
}
}
private val scalaLoaderCache = new scala.collection.jcl.WeakHashMap[String, ClassLoader]
def launcher(directory: File, mainClassName: String): Launcher = new Launch(directory, mainClassName)
def getScalaLoader(scalaVersion: String) = scalaLoaderCache.getOrElseUpdate(scalaVersion, createScalaLoader(scalaVersion))
def getScalaHome(scalaVersion: String) = new File(new File(BootDirectory, baseDirectoryName(scalaVersion)), ScalaDirectoryName)
def createScalaLoader(scalaVersion: String): ClassLoader =
{
val baseDirectory = new File(BootDirectory, baseDirectoryName(scalaVersion))
@ -108,27 +127,28 @@ class Launch(projectRootDirectory: File, mainClassName: String) extends Launcher
else
scalaLoader
}
def createSbtLoader(sbtVersion: String, scalaVersion: String, scalaLoader: ClassLoader): ClassLoader =
def createSbtLoader(sbtVersion: String, scalaVersion: String): ClassLoader = createSbtLoader(sbtVersion, scalaVersion, getScalaLoader(scalaVersion))
def createSbtLoader(sbtVersion: String, scalaVersion: String, parentLoader: ClassLoader): ClassLoader =
{
val baseDirectory = new File(BootDirectory, baseDirectoryName(scalaVersion))
val sbtDirectory = new File(baseDirectory, sbtDirectoryName(sbtVersion))
val sbtLoader = newSbtLoader(sbtDirectory, scalaLoader)
val mainComponentLocation = componentLocation(sbtVersion, MainSbtComponentID, scalaVersion)
val sbtLoader = newSbtLoader(mainComponentLocation, parentLoader)
if(needsUpdate(sbtLoader, TestLoadSbtClasses))
{
(new Update(baseDirectory, sbtVersion, scalaVersion))(UpdateSbt)
val sbtLoader = newSbtLoader(sbtDirectory, scalaLoader)
val sbtLoader = newSbtLoader(mainComponentLocation, parentLoader)
failIfMissing(sbtLoader, TestLoadSbtClasses, "sbt " + sbtVersion)
sbtLoader
}
else
sbtLoader
}
private def newScalaLoader(dir: File) = newLoader(dir, new BootFilteredLoader)
private def newSbtLoader(dir: File, scalaLoader: ClassLoader) = newLoader(dir, scalaLoader)
private def newScalaLoader(dir: File) = newLoader(dir, new BootFilteredLoader(getClass.getClassLoader))
private def newSbtLoader(dir: File, parentLoader: ClassLoader) = newLoader(dir, parentLoader)
}
private object Launch
{
def apply(args: Array[String]) = (new Launch).boot(args)
def isYes(so: Option[String]) = isValue("y", "yes")(so)
def isScratch(so: Option[String]) = isValue("s", "scratch")(so)
def isValue(values: String*)(so: Option[String]) =

View File

@ -1,4 +1,4 @@
package xsbti.boot;
package xsbti;
public interface Exit extends MainResult
{
public int code();

View File

@ -0,0 +1,21 @@
package xsbti;
import java.io.File;
public interface Launcher extends ScalaProvider, SbtProvider
{
public static final int InterfaceVersion = 1;
public void boot(String[] args);
public MainResult checkAndLoad(String[] args);
public MainResult load(String[] args);
public MainResult load(String[] args, String useSbtVersion, String mainClassName, String definitionScalaVersion);
public ClassLoader update(String scalaVersion, String sbtVersion);
public MainResult run(ClassLoader sbtLoader, String mainClassName, SbtConfiguration configuration);
public File ProjectDirectory();
public File BootDirectory();
public File PropertiesFile();
public Launcher launcher(File base, String mainClassName);
}

View File

@ -1,4 +1,4 @@
package xsbti.boot;
package xsbti;
public interface MainResult {}

View File

@ -1,4 +1,4 @@
package xsbti.boot;
package xsbti;
public interface Reboot extends MainResult
{
public String[] arguments();

View File

@ -1,4 +1,4 @@
package xsbti.boot;
package xsbti;
public interface SbtConfiguration
{

View File

@ -1,4 +1,4 @@
package xsbti.boot;
package xsbti;
public interface SbtMain
{

View File

@ -0,0 +1,12 @@
package xsbti;
import java.io.File;
public interface SbtProvider
{
public ClassLoader createSbtLoader(String sbtVersion, String scalaVersion);
public ClassLoader createSbtLoader(String sbtVersion, String scalaVersion, ClassLoader parentLoader);
public File getSbtHome(String sbtVersion, String scalaVersion);
public File componentLocation(String sbtVersion, String id, String scalaVersion);
}

View File

@ -1,4 +1,4 @@
package xsbti.boot;
package xsbti;
import java.io.File;

View File

@ -1,19 +0,0 @@
package xsbti.boot;
import java.io.File;
public interface Launcher extends ScalaProvider
{
public static final int InterfaceVersion = 1;
public void boot(String[] args);
public MainResult checkAndLoad(String[] args);
public MainResult load(String[] args);
public MainResult run(ClassLoader sbtLoader, SbtConfiguration configuration);
public File ProjectDirectory();
public File BootDirectory();
public File PropertiesFile();
public Launcher launcher(File base, String mainClassName);
}

View File

@ -1,28 +1,84 @@
package xsbt
import java.util.Properties
import xsbti.boot._
import xsbti._
import org.specs._
import LoadHelpers._
final class Main // needed so that when we test Launch, it doesn't think sbt was improperly downloaded (it looks for xsbt.Main to verify the right jar was downloaded)
object ScalaProviderTest extends Specification
{
"Launch" should {
"Provide ClassLoader for Scala 2.7.2" in { checkScalaLoader("2.7.2", "2.7.2") }
"Provide ClassLoader for Scala 2.7.3" in { checkScalaLoader("2.7.3", "2.7.3.final") }
"Provide ClassLoader for Scala 2.7.4" in { checkScalaLoader("2.7.4", "2.7.4.final") }
"Provide ClassLoader for Scala 2.7.5" in { checkScalaLoader("2.7.5", "2.7.5.final") }
def provide = addToSusVerb("provide")
"Launch" should provide {
"ClassLoader for Scala 2.7.2" in { checkScalaLoader("2.7.2") }
"ClassLoader for Scala 2.7.3" in { checkScalaLoader("2.7.3") }
"ClassLoader for Scala 2.7.4" in { checkScalaLoader("2.7.4") }
"ClassLoader for Scala 2.7.5" in { checkScalaLoader("2.7.5") }
}
private def checkScalaLoader(version: String, versionValue: String): Unit = withLaunch(checkLauncher(version, versionValue))
private def checkLauncher(version: String, versionValue: String)(launcher: Launcher): Unit =
"Launch" should {
"Successfully load (stub) main sbt from local repository and run it with correct arguments" in {
checkLoad(Array("test"), "xsbt.test.ArgumentTest").asInstanceOf[Exit].code must be(0)
checkLoad(Array(), "xsbt.test.ArgumentTest") must throwA[RuntimeException]
}
"Successfully load (stub) main sbt from local repository and run it with correct sbt version" in {
checkLoad(Array(), "xsbt.test.SbtVersionTest").asInstanceOf[Exit].code must be(0)
}
}
private def checkLoad(args: Array[String], mainClassName: String): MainResult =
withLaunch { _.load(args, test.MainTest.SbtTestVersion, mainClassName, mapScalaVersion(getScalaVersion)) }
private def checkScalaLoader(version: String): Unit = withLaunch(checkLauncher(version, scalaVersionMap(version)))
private def checkLauncher(version: String, versionValue: String)(launcher: ScalaProvider): Unit =
{
val loader = launcher.getScalaLoader(version)
Class.forName("scala.ScalaObject", false, loader)
// ensure that this loader can load Scala classes by trying scala.ScalaObject.
tryScala(loader)
getScalaVersion(loader) must beEqualTo(versionValue)
}
private def tryScala(loader: ClassLoader): Unit = Class.forName("scala.ScalaObject", false, loader).getClassLoader must be(loader)
}
object LoadHelpers
{
def withLaunch[T](f: Launcher => T): T =
FileUtilities.withTemporaryDirectory { temp => f(new xsbt.boot.Launch(temp)) }
def mapScalaVersion(versionNumber: String) = scalaVersionMap.find(_._2 == versionNumber).getOrElse {
error("Scala version number " + versionNumber + " from library.properties has no mapping")}._1
val scalaVersionMap = Map("2.7.2" -> "2.7.2") ++ Seq("2.7.3", "2.7.4", "2.7.5").map(v => (v, v + ".final"))
def getScalaVersion: String = getScalaVersion(getClass.getClassLoader)
def getScalaVersion(loader: ClassLoader): String =
{
val propertiesStream = loader.getResourceAsStream("library.properties")
val properties = new Properties
properties.load(propertiesStream)
properties.getProperty("version.number") must beEqualTo(versionValue)
properties.getProperty("version.number")
}
private def withLaunch[T](f: Launcher => T): T = withLaunch("")(f)
private def withLaunch[T](mainClass: String)(f: Launcher => T): T =
FileUtilities.withTemporaryDirectory { temp => f(new xsbt.boot.Launch(temp, mainClass)) }
}
}
package test
{
object MainTest
{
val SbtTestVersion = "test-0.7" // keep in sync with LauncherProject in the XSbt project definition
}
import MainTest.SbtTestVersion
final class MainException(message: String) extends RuntimeException(message)
final class ArgumentTest extends SbtMain
{
def run(configuration: SbtConfiguration) =
if(configuration.arguments.length == 0)
throw new MainException("Arguments were empty")
else
new xsbt.boot.Exit(0)
}
class SbtVersionTest extends SbtMain
{
def run(configuration: SbtConfiguration) =
if(configuration.sbtVersion == SbtTestVersion)
new xsbt.boot.Exit(0)
else
throw new MainException("sbt version was " + configuration.sbtVersion + ", expected: " + SbtTestVersion)
}
}

5
notes
View File

@ -19,9 +19,8 @@ Task engine
- use Exceptions instead of Option/Either
- every component gets its own subproject
- can use any version of compiler/Scala that is source compatible
- requires CrossLogger that can interface across ClassLoader boundaries with reflection
- Logger passed by implicit parameter
- build using normal cross-build conventions
- build xsbt using normal cross-build conventions
- compiler: raw interface (no dependency analysis) or with dependency analysis
- compiler: can specify scala-library.jar and scala-compiler.jar + version instead of retrieving the ClassLoader
- minimal dependence on main xsbt logger from subcomponents: use thin interface for subcomponents and implement interface in separate files in main xsbt
@ -33,7 +32,7 @@ Dependency Management
TODO:
compiler analysis callback does not check classes against output directory. This must now be done in callback itself:
compiler analysis plugin does not check classes against output directory. This must now be done in the callback itself. The old code was:
Path.relativize(outputPath, pf.file) match
{

View File

@ -2,36 +2,43 @@ import sbt._
class XSbt(info: ProjectInfo) extends ParentProject(info)
{
val testDeps = project("test-dependencies", "Dependencies", new TestDependencies(_))
val launchInterfaceSub = project(launchPath / "interface", "Launcher Interface", new InterfaceProject(_), testDeps)
val launchInterfaceSub = project(launchPath / "interface", "Launcher Interface", new InterfaceProject(_))
val launchSub = project(launchPath, "Launcher", new LaunchProject(_), launchInterfaceSub)
val interfaceSub = project("interface", "Interface", new InterfaceProject(_))
val controlSub = project(utilPath / "control", "Control", new Base(_))
val collectionSub = project(utilPath / "collection", "Collections", new Base(_))
val ioSub = project(utilPath / "io", "IO", new Base(_), controlSub, testDeps)
val ioSub = project(utilPath / "io", "IO", new IOProject(_), controlSub)
val classpathSub = project(utilPath / "classpath", "Classpath", new Base(_))
val compilerInterfaceSub = project(compilePath / "interface", "Compiler Interface", new CompilerInterfaceProject(_), interfaceSub)
val compileInterfaceSub = project(compilePath / "interface", "Compiler Interface Src", new CompilerInterfaceProject(_), interfaceSub)
val ivySub = project("ivy", "Ivy", new IvyProject(_), interfaceSub)
val logSub = project(utilPath / "log", "Logging", new Base(_))
val taskSub = project("tasks", "Tasks", new TaskProject(_), controlSub, collectionSub, testDeps)
val taskSub = project("tasks", "Tasks", new TaskProject(_), controlSub, collectionSub)
val cacheSub = project("cache", "Cache", new CacheProject(_), taskSub, ioSub)
val compilerSub = project(compilePath, "Compile", new Base(_), interfaceSub, ivySub, ioSub, compilerInterfaceSub)
val compilerSub = project(compilePath, "Compile", new CompileProject(_),
launchInterfaceSub, interfaceSub, ivySub, ioSub, classpathSub, compileInterfaceSub)
def launchPath = path("launch")
def utilPath = path("util")
def compilePath = path("compile")
class LaunchProject(info: ProjectInfo) extends Base(info) with TestWithIO
class LaunchProject(info: ProjectInfo) extends Base(info) with TestWithIO with TestDependencies
{
val ivy = "org.apache.ivy" % "ivy" % "2.0.0"
// to test the retrieving and loading of the main sbt, we package and publish the test classes to the local repository
override def defaultMainArtifact = Artifact(idWithVersion)
override def projectID = ModuleID(organization, idWithVersion, "test-" + version)
override def packageAction = packageTask(packageTestPaths, outputPath / (idWithVersion + "-" + projectID.revision +".jar"), packageOptions).dependsOn(rawTestCompile)
override def deliverProjectDependencies = Nil
def idWithVersion = "xsbt_" + ScalaVersion.currentString
lazy val rawTestCompile = super.testCompileAction
override def testCompileAction = publishLocal dependsOn(rawTestCompile)
}
class TestDependencies(info: ProjectInfo) extends DefaultProject(info)
trait TestDependencies extends Project
{
val sc = "org.scala-tools.testing" % "scalacheck" % "1.5" % "test->default"
val sp = "org.scala-tools.testing" % "specs" % "1.5.0" % "test->default"
@ -39,28 +46,36 @@ class XSbt(info: ProjectInfo) extends ParentProject(info)
}
override def parallelExecution = true
class TaskProject(info: ProjectInfo) extends Base(info)
class IOProject(info: ProjectInfo) extends Base(info) with TestDependencies
class TaskProject(info: ProjectInfo) extends Base(info) with TestDependencies
class CacheProject(info: ProjectInfo) extends Base(info)
{
//override def compileOptions = super.compileOptions ++ List(Unchecked,ExplainTypes, CompileOption("-Xlog-implicits"))
}
class Base(info: ProjectInfo) extends DefaultProject(info)
class Base(info: ProjectInfo) extends DefaultProject(info) with ManagedBase
{
override def scratch = true
}
class CompileProject(info: ProjectInfo) extends Base(info)
{
override def testCompileAction = super.testCompileAction dependsOn(launchSub.testCompile, compileInterfaceSub.`package`, interfaceSub.`package`)
override def deliverProjectDependencies = Set(super.deliverProjectDependencies.toSeq : _*) - launchInterfaceSub.projectID
override def testClasspath = super.testClasspath +++ launchSub.testClasspath +++ compileInterfaceSub.jarPath +++ interfaceSub.jarPath
override def compileOptions = super.compileOptions ++ Seq(CompileOption("-Xno-varargs-conversion"))
}
class IvyProject(info: ProjectInfo) extends Base(info) with TestWithIO
{
val ivy = "org.apache.ivy" % "ivy" % "2.0.0"
}
class InterfaceProject(info: ProjectInfo) extends DefaultProject(info)
class InterfaceProject(info: ProjectInfo) extends DefaultProject(info) with ManagedBase
{
override def mainSources = descendents(mainSourceRoots, "*.java")
override def compileOrder = CompileOrder.JavaThenScala
}
class CompilerInterfaceProject(info: ProjectInfo) extends Base(info) with SourceProject
{
// these set up the test so that the classes and resources are both in the output resource directory
// the main output path is removed so that the plugin (xsbt.Analyzer) is found in the output resource directory so that
// these set up the test environment so that the classes and resources are both in the output resource directory
// the main compile path is removed so that the plugin (xsbt.Analyzer) is found in the output resource directory so that
// the tests can configure that directory as -Xpluginsdir (which requires the scalac-plugin.xml and the classes to be together)
override def testCompileAction = super.testCompileAction dependsOn(packageForTest, ioSub.testCompile)
override def mainResources = super.mainResources +++ "scalac-plugin.xml"
@ -77,5 +92,15 @@ class XSbt(info: ProjectInfo) extends ParentProject(info)
}
trait SourceProject extends BasicScalaProject
{
override def packagePaths = packageSourcePaths
override final def crossScalaVersions = Set.empty
override def packagePaths = mainResources +++ mainSources
}
trait ManagedBase extends BasicScalaProject
{
override def deliverScalaDependencies = Nil
override def crossScalaVersions = Set("2.7.5")
override def managedStyle = ManagedStyle.Ivy
override def useDefaultConfigurations = false
val defaultConf = Configurations.Default
val testConf = Configurations.Test
}

View File

@ -0,0 +1,80 @@
package xsbt
import java.net.URL
import java.util.Enumeration
class DifferentLoaders(message: String, val loaderA: ClassLoader, val loaderB: ClassLoader) extends ClassNotFoundException(message)
class DualLoader(parentA: ClassLoader, aOnlyClasses: String => Boolean, aOnlyResources: String => Boolean,
parentB: ClassLoader, bOnlyClasses: String => Boolean, bOnlyResources: String => Boolean) extends ClassLoader
{
def this(parentA: ClassLoader, aOnly: String => Boolean, parentB: ClassLoader, bOnly: String => Boolean) =
this(parentA, aOnly, aOnly, parentB, bOnly, bOnly)
override final def loadClass(className: String, resolve: Boolean): Class[_] =
{
val c =
if(aOnlyClasses(className))
parentA.loadClass(className)
else if(bOnlyClasses(className))
parentB.loadClass(className)
else
{
val classA = parentA.loadClass(className)
val classB = parentB.loadClass(className)
if(classA.getClassLoader eq classB.getClassLoader)
classA
else
throw new DifferentLoaders("Parent class loaders returned different classes for '" + className + "'", classA.getClassLoader, classB.getClassLoader)
}
if(resolve)
resolveClass(c)
c
}
override def getResource(name: String): URL =
{
if(aOnlyResources(name))
parentA.getResource(name)
else if(bOnlyResources(name))
parentB.getResource(name)
else
{
val urlA = parentA.getResource(name)
val urlB = parentB.getResource(name)
if(urlA eq null)
urlB
else
urlA
}
}
override def getResources(name: String): Enumeration[URL] =
{
if(aOnlyResources(name))
parentA.getResources(name)
else if(bOnlyResources(name))
parentB.getResources(name)
else
{
val urlsA = parentA.getResources(name)
val urlsB = parentB.getResources(name)
if(urlsA eq null)
urlsB
else if(urlsB eq null)
urlsA
else
new DualEnumeration(urlsA, urlsB)
}
}
}
final class DualEnumeration[T](a: Enumeration[T], b: Enumeration[T]) extends Enumeration[T]
{
// invariant: current.hasMoreElements or current eq b
private[this] var current = if(a.hasMoreElements) a else b
def hasMoreElements = current.hasMoreElements
def nextElement =
{
val element = current.nextElement
if(!current.hasMoreElements)
current = b
element
}
}

View File

@ -7,7 +7,7 @@ import OpenResource._
import ErrorHandling.translate
import java.io.{File, FileInputStream, InputStream, OutputStream}
import java.net.{URISyntaxException, URL}
import java.net.{URI, URISyntaxException, URL}
import java.nio.charset.Charset
import java.util.jar.{Attributes, JarEntry, JarFile, JarInputStream, JarOutputStream, Manifest}
import java.util.zip.{GZIPOutputStream, ZipEntry, ZipFile, ZipInputStream, ZipOutputStream}
@ -38,6 +38,20 @@ object FileUtilities
def toFile(url: URL) =
try { new File(url.toURI) }
catch { case _: URISyntaxException => new File(url.getPath) }
/** Converts the given URL to a File. If the URL is for an entry in a jar, the File for the jar is returned. */
def asFile(url: URL): File =
{
url.getProtocol match
{
case "file" => toFile(url)
case "jar" =>
val path = url.getPath
val end = path.indexOf('!')
new File(new URI(if(end == -1) path else path.substring(0, end)))
case _ => error("Invalid protocol " + url.getProtocol)
}
}
// "base.extension" -> (base, extension)

View File

@ -14,6 +14,8 @@ object PathMapper
{
val basic = new FMapper(_.getPath)
def relativeTo(base: File) = new FMapper(file => FileUtilities.relativize(base, file).getOrElse(file.getPath))
val flat = new FMapper(_.getName)
def apply(f: File => String) = new FMapper(f)
}
class FMapper(f: File => String) extends PathMapper
{

View File

@ -0,0 +1,25 @@
package xsbt
import java.io.File
import FileUtilities.{withTemporaryDirectory, write}
object WithFiles
{
/** Takes the relative path -> content pairs and writes the content to a file in a temporary directory. The written file
* path is the relative path resolved against the temporary directory path. The provided function is called with the resolved file paths
* in the same order as the inputs. */
def apply[T](sources: (File, String)*)(f: Seq[File] => T): T =
{
withTemporaryDirectory { dir =>
val sourceFiles =
for((file, content) <- sources) yield
{
assert(!file.isAbsolute)
val to = new File(dir, file.getPath)
write(to, content)
to
}
f(sourceFiles)
}
}
}