From 50d350abd05af7c9ea92dd2451fef5272c715c46 Mon Sep 17 00:00:00 2001 From: Mark Harrah Date: Sun, 23 Aug 2009 22:21:15 -0400 Subject: [PATCH] Compilation with analysis independent of Scala version of sbt. Basic test for this. --- compile/Compiler.scala | 78 +++++++++-------- compile/ComponentCompiler.scala | 25 +++--- .../src/test/scala/TestCompile.scala | 20 ----- compile/src/test/scala/CompileTest.scala | 50 +++++++++++ launch/Boot.scala | 4 +- launch/BootConfiguration.scala | 9 +- launch/FilteredLoader.scala | 4 +- launch/Launch.scala | 54 ++++++++---- .../src/main/java/xsbti/{boot => }/Exit.java | 2 +- .../src/main/java/xsbti/Launcher.java | 21 +++++ .../java/xsbti/{boot => }/MainResult.java | 2 +- .../main/java/xsbti/{boot => }/Reboot.java | 2 +- .../xsbti/{boot => }/SbtConfiguration.java | 2 +- .../main/java/xsbti/{boot => }/SbtMain.java | 2 +- .../src/main/java/xsbti/SbtProvider.java | 12 +++ .../java/xsbti/{boot => }/ScalaProvider.java | 2 +- .../src/main/java/xsbti/boot/Launcher.java | 19 ----- launch/src/test/scala/ScalaProviderTest.scala | 84 +++++++++++++++---- notes | 5 +- project/build/XSbt.scala | 55 ++++++++---- util/classpath/DualLoader.scala | 80 ++++++++++++++++++ util/io/FileUtilities.scala | 16 +++- util/io/PathMapper.scala | 2 + util/io/src/test/scala/WithFiles.scala | 25 ++++++ 24 files changed, 426 insertions(+), 149 deletions(-) create mode 100644 compile/src/test/scala/CompileTest.scala rename launch/interface/src/main/java/xsbti/{boot => }/Exit.java (75%) create mode 100644 launch/interface/src/main/java/xsbti/Launcher.java rename launch/interface/src/main/java/xsbti/{boot => }/MainResult.java (62%) rename launch/interface/src/main/java/xsbti/{boot => }/Reboot.java (78%) rename launch/interface/src/main/java/xsbti/{boot => }/SbtConfiguration.java (88%) rename launch/interface/src/main/java/xsbti/{boot => }/SbtMain.java (80%) create mode 100644 launch/interface/src/main/java/xsbti/SbtProvider.java rename launch/interface/src/main/java/xsbti/{boot => }/ScalaProvider.java (88%) delete mode 100644 launch/interface/src/main/java/xsbti/boot/Launcher.java create mode 100644 util/classpath/DualLoader.scala create mode 100644 util/io/src/test/scala/WithFiles.scala diff --git a/compile/Compiler.scala b/compile/Compiler.scala index 1d00f7206..449ceee46 100644 --- a/compile/Compiler.scala +++ b/compile/Compiler.scala @@ -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) } } \ No newline at end of file diff --git a/compile/ComponentCompiler.scala b/compile/ComponentCompiler.scala index c60c86253..369c0cb85 100644 --- a/compile/ComponentCompiler.scala +++ b/compile/ComponentCompiler.scala @@ -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)) } diff --git a/compile/interface/src/test/scala/TestCompile.scala b/compile/interface/src/test/scala/TestCompile.scala index 013dd4188..fbac3521a 100644 --- a/compile/interface/src/test/scala/TestCompile.scala +++ b/compile/interface/src/test/scala/TestCompile.scala @@ -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) - } - } } \ No newline at end of file diff --git a/compile/src/test/scala/CompileTest.scala b/compile/src/test/scala/CompileTest.scala new file mode 100644 index 000000000..dbb26516a --- /dev/null +++ b/compile/src/test/scala/CompileTest.scala @@ -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) +} \ No newline at end of file diff --git a/launch/Boot.scala b/launch/Boot.scala index fcbfb2aa1..05b94ebc5 100644 --- a/launch/Boot.scala +++ b/launch/Boot.scala @@ -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) diff --git a/launch/BootConfiguration.scala b/launch/BootConfiguration.scala index 6c54a9a21..9527f5d3a 100644 --- a/launch/BootConfiguration.scala +++ b/launch/BootConfiguration.scala @@ -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 } diff --git a/launch/FilteredLoader.scala b/launch/FilteredLoader.scala index 3a3dec645..5dbcfe1e9 100644 --- a/launch/FilteredLoader.scala +++ b/launch/FilteredLoader.scala @@ -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 } \ No newline at end of file diff --git a/launch/Launch.scala b/launch/Launch.scala index dc3b8489e..f7335f0cc 100644 --- a/launch/Launch.scala +++ b/launch/Launch.scala @@ -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]) = diff --git a/launch/interface/src/main/java/xsbti/boot/Exit.java b/launch/interface/src/main/java/xsbti/Exit.java similarity index 75% rename from launch/interface/src/main/java/xsbti/boot/Exit.java rename to launch/interface/src/main/java/xsbti/Exit.java index 316fb8ee0..64e08f174 100644 --- a/launch/interface/src/main/java/xsbti/boot/Exit.java +++ b/launch/interface/src/main/java/xsbti/Exit.java @@ -1,4 +1,4 @@ -package xsbti.boot; +package xsbti; public interface Exit extends MainResult { public int code(); diff --git a/launch/interface/src/main/java/xsbti/Launcher.java b/launch/interface/src/main/java/xsbti/Launcher.java new file mode 100644 index 000000000..fb7925aeb --- /dev/null +++ b/launch/interface/src/main/java/xsbti/Launcher.java @@ -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); +} \ No newline at end of file diff --git a/launch/interface/src/main/java/xsbti/boot/MainResult.java b/launch/interface/src/main/java/xsbti/MainResult.java similarity index 62% rename from launch/interface/src/main/java/xsbti/boot/MainResult.java rename to launch/interface/src/main/java/xsbti/MainResult.java index e894519fa..48ea398bf 100644 --- a/launch/interface/src/main/java/xsbti/boot/MainResult.java +++ b/launch/interface/src/main/java/xsbti/MainResult.java @@ -1,4 +1,4 @@ -package xsbti.boot; +package xsbti; public interface MainResult {} diff --git a/launch/interface/src/main/java/xsbti/boot/Reboot.java b/launch/interface/src/main/java/xsbti/Reboot.java similarity index 78% rename from launch/interface/src/main/java/xsbti/boot/Reboot.java rename to launch/interface/src/main/java/xsbti/Reboot.java index 16c1fdc19..bc8c35709 100644 --- a/launch/interface/src/main/java/xsbti/boot/Reboot.java +++ b/launch/interface/src/main/java/xsbti/Reboot.java @@ -1,4 +1,4 @@ -package xsbti.boot; +package xsbti; public interface Reboot extends MainResult { public String[] arguments(); diff --git a/launch/interface/src/main/java/xsbti/boot/SbtConfiguration.java b/launch/interface/src/main/java/xsbti/SbtConfiguration.java similarity index 88% rename from launch/interface/src/main/java/xsbti/boot/SbtConfiguration.java rename to launch/interface/src/main/java/xsbti/SbtConfiguration.java index fb6859a60..d7d6e1909 100644 --- a/launch/interface/src/main/java/xsbti/boot/SbtConfiguration.java +++ b/launch/interface/src/main/java/xsbti/SbtConfiguration.java @@ -1,4 +1,4 @@ -package xsbti.boot; +package xsbti; public interface SbtConfiguration { diff --git a/launch/interface/src/main/java/xsbti/boot/SbtMain.java b/launch/interface/src/main/java/xsbti/SbtMain.java similarity index 80% rename from launch/interface/src/main/java/xsbti/boot/SbtMain.java rename to launch/interface/src/main/java/xsbti/SbtMain.java index 9e2e4e089..15858be5b 100644 --- a/launch/interface/src/main/java/xsbti/boot/SbtMain.java +++ b/launch/interface/src/main/java/xsbti/SbtMain.java @@ -1,4 +1,4 @@ -package xsbti.boot; +package xsbti; public interface SbtMain { diff --git a/launch/interface/src/main/java/xsbti/SbtProvider.java b/launch/interface/src/main/java/xsbti/SbtProvider.java new file mode 100644 index 000000000..f5d857e5e --- /dev/null +++ b/launch/interface/src/main/java/xsbti/SbtProvider.java @@ -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); +} \ No newline at end of file diff --git a/launch/interface/src/main/java/xsbti/boot/ScalaProvider.java b/launch/interface/src/main/java/xsbti/ScalaProvider.java similarity index 88% rename from launch/interface/src/main/java/xsbti/boot/ScalaProvider.java rename to launch/interface/src/main/java/xsbti/ScalaProvider.java index 3567d691e..bd780eac7 100644 --- a/launch/interface/src/main/java/xsbti/boot/ScalaProvider.java +++ b/launch/interface/src/main/java/xsbti/ScalaProvider.java @@ -1,4 +1,4 @@ -package xsbti.boot; +package xsbti; import java.io.File; diff --git a/launch/interface/src/main/java/xsbti/boot/Launcher.java b/launch/interface/src/main/java/xsbti/boot/Launcher.java deleted file mode 100644 index 7a4c97351..000000000 --- a/launch/interface/src/main/java/xsbti/boot/Launcher.java +++ /dev/null @@ -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); -} \ No newline at end of file diff --git a/launch/src/test/scala/ScalaProviderTest.scala b/launch/src/test/scala/ScalaProviderTest.scala index 32cc3a7b4..a66aaf2ca 100644 --- a/launch/src/test/scala/ScalaProviderTest.scala +++ b/launch/src/test/scala/ScalaProviderTest.scala @@ -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)) } -} \ No newline at end of file +} + +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) + } +} diff --git a/notes b/notes index 667d46c6d..74d4cd11e 100644 --- a/notes +++ b/notes @@ -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 { diff --git a/project/build/XSbt.scala b/project/build/XSbt.scala index ccfa445ef..212e721d0 100644 --- a/project/build/XSbt.scala +++ b/project/build/XSbt.scala @@ -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 } \ No newline at end of file diff --git a/util/classpath/DualLoader.scala b/util/classpath/DualLoader.scala new file mode 100644 index 000000000..ca679045a --- /dev/null +++ b/util/classpath/DualLoader.scala @@ -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 + } +} \ No newline at end of file diff --git a/util/io/FileUtilities.scala b/util/io/FileUtilities.scala index 739a0c2e6..7458c4069 100644 --- a/util/io/FileUtilities.scala +++ b/util/io/FileUtilities.scala @@ -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) diff --git a/util/io/PathMapper.scala b/util/io/PathMapper.scala index 4112701ff..2f8afdc09 100644 --- a/util/io/PathMapper.scala +++ b/util/io/PathMapper.scala @@ -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 { diff --git a/util/io/src/test/scala/WithFiles.scala b/util/io/src/test/scala/WithFiles.scala new file mode 100644 index 000000000..649f96175 --- /dev/null +++ b/util/io/src/test/scala/WithFiles.scala @@ -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) + } + } +} \ No newline at end of file