mirror of https://github.com/sbt/sbt.git
Compilation with analysis independent of Scala version of sbt.
Basic test for this.
This commit is contained in:
parent
efb1604f0e
commit
50d350abd0
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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]) =
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package xsbti.boot;
|
||||
package xsbti;
|
||||
public interface Exit extends MainResult
|
||||
{
|
||||
public int code();
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package xsbti.boot;
|
||||
package xsbti;
|
||||
|
||||
public interface MainResult {}
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package xsbti.boot;
|
||||
package xsbti;
|
||||
public interface Reboot extends MainResult
|
||||
{
|
||||
public String[] arguments();
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package xsbti.boot;
|
||||
package xsbti;
|
||||
|
||||
public interface SbtConfiguration
|
||||
{
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package xsbti.boot;
|
||||
package xsbti;
|
||||
|
||||
public interface SbtMain
|
||||
{
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package xsbti.boot;
|
||||
package xsbti;
|
||||
|
||||
import java.io.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);
|
||||
}
|
||||
|
|
@ -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
5
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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue