diff options
author | Paul Phillips <paulp@improving.org> | 2011-08-01 17:46:48 +0000 |
---|---|---|
committer | Paul Phillips <paulp@improving.org> | 2011-08-01 17:46:48 +0000 |
commit | daa26379ceae60b441f49dab49f367ebea027529 (patch) | |
tree | 42ab375e1071ccc625258241f876c1a4d0b75cc7 | |
parent | 8c0fa605fba819d6ad8714a488d404e966d224b3 (diff) | |
download | scala-daa26379ceae60b441f49dab49f367ebea027529.tar.gz scala-daa26379ceae60b441f49dab49f367ebea027529.tar.bz2 scala-daa26379ceae60b441f49dab49f367ebea027529.zip |
Working on jar creation infrastructure.
output generation (but only then, since otherwise we're not creating the
jar):
1) -Xmain-class foo.Bar will give the jar a Main-Class of foo.Bar 2)
Alternatively, if there is only one runnable program, that will be
the Main-Class 3) Always, the jar's manifest will have an entry for
Scala-Compiler-Version.
Not very relatedly, a warning is now issued when a module has a main
method but a runnable program will not be generated. Closes SI-4861.
This represents an opening step toward automatically recognizing
mismatched bytecode situations: coarse, but useful and safe. Review by
mirco.
-rw-r--r-- | src/compiler/scala/tools/nsc/backend/jvm/BytecodeWriters.scala | 28 | ||||
-rw-r--r-- | src/compiler/scala/tools/nsc/backend/jvm/GenJVM.scala | 102 | ||||
-rw-r--r-- | src/compiler/scala/tools/nsc/io/Jar.scala | 84 | ||||
-rw-r--r-- | src/compiler/scala/tools/nsc/io/package.scala | 8 | ||||
-rw-r--r-- | src/compiler/scala/tools/nsc/settings/AdvancedScalaSettings.scala | 1 | ||||
-rw-r--r-- | src/compiler/scala/tools/nsc/settings/ScalaSettings.scala | 1 | ||||
-rw-r--r-- | src/compiler/scala/tools/nsc/util/ClassPath.scala | 2 | ||||
-rw-r--r-- | src/library/scala/util/Properties.scala | 10 | ||||
-rw-r--r-- | test/files/neg/main1.check | 26 | ||||
-rw-r--r-- | test/files/neg/main1.flags | 1 | ||||
-rw-r--r-- | test/files/neg/main1.scala | 45 | ||||
-rw-r--r-- | test/pending/run/jar-version.scala | 11 |
12 files changed, 265 insertions, 54 deletions
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BytecodeWriters.scala b/src/compiler/scala/tools/nsc/backend/jvm/BytecodeWriters.scala index faf86d37f6..228d4c6191 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BytecodeWriters.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BytecodeWriters.scala @@ -7,12 +7,12 @@ package scala.tools.nsc package backend.jvm import ch.epfl.lamp.fjbg._ -import java.io.{ DataOutputStream, OutputStream } -import scala.tools.nsc.io.{ Path, Directory } +import java.io.{ DataOutputStream, OutputStream, File => JFile } +import scala.tools.nsc.io._ import scala.tools.nsc.util.ScalaClassLoader import scala.tools.util.Javap -import java.util.jar.{ JarEntry, JarOutputStream } -import scala.tools.nsc.io.AbstractFile +import java.util.jar.{ JarEntry, JarOutputStream, Attributes } +import Attributes.Name /** For the last mile: turning generated bytecode in memory into * something you can use. Has implementations for writing to class @@ -43,17 +43,23 @@ trait BytecodeWriters { def close(): Unit = () } - class DirectToJarfileWriter(val jarFile: AbstractFile) extends BytecodeWriter { - private val out = new JarOutputStream(jarFile.bufferedOutput) + class DirectToJarfileWriter(jfile: JFile) extends BytecodeWriter { + val jarMainAttrs = ( + if (settings.mainClass.isDefault) Nil + else List(Name.MAIN_CLASS -> settings.mainClass.value) + ) + val writer = new Jar(jfile).jarWriter(jarMainAttrs: _*) + def writeClass(label: String, jclass: JClass, sym: Symbol) { val path = jclass.getName + ".class" - out putNextEntry new JarEntry(path) - val dataStream = new DataOutputStream(out) - try jclass writeTo dataStream - finally dataStream.flush() + val out = writer.newOutputStream(path) + + try jclass writeTo out + finally out.flush() + informProgress("added " + label + path + " to jar") } - override def close() = out.close() + override def close() = writer.close() } trait JavapBytecodeWriter extends BytecodeWriter { diff --git a/src/compiler/scala/tools/nsc/backend/jvm/GenJVM.scala b/src/compiler/scala/tools/nsc/backend/jvm/GenJVM.scala index 70dd8cd920..0036fd8060 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/GenJVM.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/GenJVM.scala @@ -36,7 +36,8 @@ abstract class GenJVM extends SubComponent with GenJVMUtil with GenAndroid with AnyClass, ObjectClass, ThrowsClass, ThrowableClass, ClassfileAnnotationClass, SerializableClass, StringClass, ClassClass, FunctionClass, DeprecatedAttr, SerializableAttr, SerialVersionUIDAttr, VolatileAttr, - TransientAttr, CloneableAttr, RemoteAttr + TransientAttr, CloneableAttr, RemoteAttr, + hasJavaMainMethod } val phaseName = "jvm" @@ -67,29 +68,84 @@ abstract class GenJVM extends SubComponent with GenJVMUtil with GenAndroid with override def erasedTypes = true def apply(cls: IClass) = sys.error("no implementation") + def isJavaEntryPoint(clasz: IClass) = { + val sym = clasz.symbol + def fail(msg: String) = { + clasz.cunit.warning(sym.pos, + sym.name + " has a main method, but " + sym.fullName('.') + " will not be a runnable program.\n" + + " " + msg + ", which means no static forwarder can be generated.\n" + // TODO: make this next claim true, if possible + // by generating valid main methods as static in module classes + // not sure what the jvm allows here + // + " You can still run the program by calling it as " + sym.javaSimpleName + " instead." + ) + false + } + sym.hasModuleFlag && hasJavaMainMethod(sym) && { + // At this point we've seen a module with a main method, so if this + // doesn't turn out to be a valid entry point, issue a warning. + val companion = sym.linkedClassOfClass + if (companion.isTrait) + fail("Its companion is a trait") + else if (hasJavaMainMethod(companion) && !(sym isSubClass companion)) + fail("Its companion contains its own main method") + // this is only because forwarders aren't smart enough yet + else if (companion.tpe.member(nme.main) != NoSymbol) + fail("Its companion contains its own main method (implementation restriction: no main is allowed, regardless of signature)") + else + true + } + } + override def run() { // we reinstantiate the bytecode generator at each run, to allow the GC // to collect everything - if (settings.debug.value) inform("[running phase " + name + " on icode]") + if (settings.debug.value) + inform("[running phase " + name + " on icode]") + if (settings.Xdce.value) for ((sym, cls) <- icodes.classes if inliner.isClosureClass(sym) && !deadCode.liveClosures(sym)) icodes.classes -= sym + // For predictably ordered error messages. + val sortedClasses = classes.values.toList sortBy ("" + _.symbol.fullName) + val entryPoints = sortedClasses filter isJavaEntryPoint + val bytecodeWriter = settings.outputDirs.getSingleOutput match { case Some(f) if f hasExtension "jar" => - new DirectToJarfileWriter(f) + // If no main class was specified, see if there's only one + // entry point among the classes going into the jar. + if (settings.mainClass.isDefault) { + entryPoints map (_.symbol fullName '.') match { + case Nil => + log("No Main-Class designated or discovered.") + case name :: Nil => + log("Unique entry point: setting Main-Class to " + name) + settings.mainClass.value = name + case names => + log("No Main-Class due to multiple entry points:\n " + names.mkString("\n ")) + } + } + else log("Main-Class was specified: " + settings.mainClass.value) + + new DirectToJarfileWriter(f.file) + case _ => if (settings.Ygenjavap.isDefault) new ClassBytecodeWriter { } else new ClassBytecodeWriter with JavapBytecodeWriter { } } + val codeGenerator = new BytecodeGenerator(bytecodeWriter) - classes.values foreach { c => + log("Created new bytecode generator for " + classes.size + " classes.") + + sortedClasses foreach { c => try codeGenerator.genClass(c) catch { case e: JCode.CodeSizeTooBigException => - log("Skipped class %s because it has methods that are too long.".format(c.toString)) + log("Skipped class %s because it has methods that are too long.".format(c)) } } + bytecodeWriter.close() classes.clear() } @@ -158,10 +214,8 @@ abstract class GenJVM extends SubComponent with GenJVMUtil with GenAndroid with val isInner = sym.isClass && !sym.rawowner.isPackageClass && !sym.isModuleClass // TODO: something atPhase(currentRun.flattenPhase.prev) which accounts for // being nested in parameterized classes (if we're going to selectively flatten.) - if (isInner) { - log("Inner class: " + sym.fullLocationString) + if (isInner && !innerClassBuffer(sym)) innerClassBuffer += sym - } super.javaName(sym) } @@ -222,7 +276,7 @@ abstract class GenJVM extends SubComponent with GenJVMUtil with GenAndroid with var isRemoteClass: Boolean = false var isParcelableClass = false - private val innerClassBuffer = new mutable.ListBuffer[Symbol] + private var innerClassBuffer = mutable.LinkedHashSet[Symbol]() def genClass(c: IClass) { clasz = c @@ -687,26 +741,32 @@ abstract class GenJVM extends SubComponent with GenJVMUtil with GenAndroid with // add inner classes which might not have been referenced yet atPhase(currentRun.erasurePhase.next) { for (sym <- List(clasz.symbol, clasz.symbol.linkedClassOfClass) ; m <- sym.info.decls ; if m.isClass) - innerClassBuffer += m + if (!innerClassBuffer(m)) + innerClassBuffer += m } val allInners = innerClassBuffer.toList if (allInners.nonEmpty) { + log(clasz.symbol.fullName('.') + " contains " + allInners.size + " inner classes.") val innerClassesAttr = jclass.getInnerClasses() // sort them so inner classes succeed their enclosing class // to satisfy the Eclipse Java compiler - //for (innerSym <- innerClasses.toList sortBy (_.name.length)) { - for (innerSym <- allInners.distinct sortBy (_.name.length)) { - var flags = javaFlags(innerSym) - if (innerSym.rawowner.hasModuleFlag) - flags |= ACC_STATIC - - innerClassesAttr.addEntry( - javaName(innerSym), - outerName(innerSym), - innerName(innerSym), - flags & INNER_CLASSES_FLAGS + for (innerSym <- allInners sortBy (_.name.length)) { + val flags = { + val staticFlag = if (innerSym.rawowner.hasModuleFlag) ACC_STATIC else 0 + (javaFlags(innerSym) | staticFlag) & INNER_CLASSES_FLAGS + } + val jname = javaName(innerSym) + val oname = outerName(innerSym) + val iname = innerName(innerSym) + + // Mimicking javap inner class output + debuglog( + if (oname == null || iname == null) "//class " + jname + else "//%s=class %s of class %s".format(iname, jname, oname) ) + + innerClassesAttr.addEntry(jname, oname, iname, flags) } } } diff --git a/src/compiler/scala/tools/nsc/io/Jar.scala b/src/compiler/scala/tools/nsc/io/Jar.scala index e8eab682fc..ad1598a85d 100644 --- a/src/compiler/scala/tools/nsc/io/Jar.scala +++ b/src/compiler/scala/tools/nsc/io/Jar.scala @@ -6,7 +6,7 @@ package scala.tools.nsc package io -import java.io.{ InputStream, OutputStream, IOException, FileNotFoundException, FileInputStream } +import java.io.{ InputStream, OutputStream, IOException, FileNotFoundException, FileInputStream, DataOutputStream } import java.util.jar._ import collection.JavaConverters._ import Attributes.Name @@ -33,7 +33,9 @@ import util.ClassPath // static Attributes.Name SPECIFICATION_VERSION class Jar(file: File) extends Iterable[JarEntry] { + def this(jfile: JFile) = this(File(jfile)) def this(path: String) = this(File(path)) + protected def errorFn(msg: String): Unit = Console println msg lazy val jarFile = new JarFile(file.jfile) @@ -45,7 +47,9 @@ class Jar(file: File) extends Iterable[JarEntry] { try f(in) finally in.close() } - def jarWriter() = new JarWriter(file) + def jarWriter(mainAttrs: (Attributes.Name, String)*) = { + new JarWriter(file, Jar.WManifest(mainAttrs: _*).underlying) + } override def foreach[U](f: JarEntry => U): Unit = withJarInput { in => Iterator continually in.getNextJarEntry() takeWhile (_ != null) foreach f @@ -60,26 +64,40 @@ class Jar(file: File) extends Iterable[JarEntry] { override def toString = "" + file } -class JarWriter(file: File, val manifest: Manifest = new Manifest()) { +class JarWriter(val file: File, val manifest: Manifest) { private lazy val out = new JarOutputStream(file.outputStream(), manifest) - def writeAllFrom(dir: Directory) = { + + /** Adds a jar entry for the given path and returns an output + * stream to which the data should immediately be written. + * This unusual interface exists to work with fjbg. + */ + def newOutputStream(path: String): DataOutputStream = { + val entry = new JarEntry(path) + out putNextEntry entry + new DataOutputStream(out) + } + + def writeAllFrom(dir: Directory) { try dir.list foreach (x => addEntry(x, "")) finally out.close() - - file } - private def addFile(entry: File, prefix: String) { - out putNextEntry new JarEntry(prefix + entry.name) - try transfer(entry.inputStream(), out) + def addStream(entry: JarEntry, in: InputStream) { + out putNextEntry entry + try transfer(in, out) finally out.closeEntry() } - private def addEntry(entry: Path, prefix: String) { + def addFile(file: File, prefix: String) { + val entry = new JarEntry(prefix + file.name) + addStream(entry, file.inputStream()) + } + def addEntry(entry: Path, prefix: String) { if (entry.isFile) addFile(entry.toFile, prefix) else addDirectory(entry.toDirectory, prefix + entry.name + "/") } - private def addDirectory(entry: Directory, prefix: String) { + def addDirectory(entry: Directory, prefix: String) { entry.list foreach (p => addEntry(p, prefix)) } + private def transfer(in: InputStream, out: OutputStream) = { val buf = new Array[Byte](10240) def loop(): Unit = in.read(buf, 0, buf.length) match { @@ -88,9 +106,47 @@ class JarWriter(file: File, val manifest: Manifest = new Manifest()) { } loop } + + def close() = out.close() } object Jar { + type AttributeMap = java.util.Map[Attributes.Name, String] + + object WManifest { + def apply(mainAttrs: (Attributes.Name, String)*): WManifest = { + val m = WManifest(new JManifest) + for ((k, v) <- mainAttrs) + m(k) = v + + m + } + def apply(manifest: JManifest): WManifest = new WManifest(manifest) + implicit def unenrichManifest(x: WManifest): JManifest = x.underlying + } + class WManifest(manifest: JManifest) { + for ((k, v) <- initialMainAttrs) + this(k) = v + + def underlying = manifest + def attrs = manifest.getMainAttributes().asInstanceOf[AttributeMap].asScala withDefaultValue null + def initialMainAttrs: Map[Attributes.Name, String] = { + import scala.util.Properties._ + Map( + Name.MANIFEST_VERSION -> "1.0", + ScalaCompilerVersion -> versionNumberString + ) + } + + def apply(name: Attributes.Name): String = attrs(name) + def apply(name: String): String = apply(new Attributes.Name(name)) + def update(key: Attributes.Name, value: String) = attrs.put(key, value) + def update(key: String, value: String) = attrs.put(new Attributes.Name(key), value) + + def mainClass: String = apply(Name.MAIN_CLASS) + def mainClass_=(value: String) = update(Name.MAIN_CLASS, value) + } + // See http://download.java.net/jdk7/docs/api/java/nio/file/Path.html // for some ideas. private val ZipMagicNumber = List[Byte](80, 75, 3, 4) @@ -100,10 +156,8 @@ object Jar { def isJarOrZip(f: Path, examineFile: Boolean): Boolean = f.hasExtension("zip", "jar") || (examineFile && magicNumberIsZip(f)) - def create(file: File, sourceDir: Directory, mainClass: String): File = { - val writer = new Jar(file).jarWriter() - writer.manifest(Name.MANIFEST_VERSION) = "1.0" - writer.manifest(Name.MAIN_CLASS) = mainClass + def create(file: File, sourceDir: Directory, mainClass: String) { + val writer = new Jar(file).jarWriter(Name.MAIN_CLASS -> mainClass) writer writeAllFrom sourceDir } } diff --git a/src/compiler/scala/tools/nsc/io/package.scala b/src/compiler/scala/tools/nsc/io/package.scala index 2c5e50e970..d0a1d88086 100644 --- a/src/compiler/scala/tools/nsc/io/package.scala +++ b/src/compiler/scala/tools/nsc/io/package.scala @@ -12,14 +12,8 @@ import java.util.jar.{ Attributes } package object io { type JManifest = java.util.jar.Manifest type JFile = java.io.File - private[io] implicit def installManifestOps(m: JManifest) = new ManifestOps(m) - - class ManifestOps(manifest: JManifest) { - def attrs = manifest.getMainAttributes() - def apply(name: Attributes.Name) = "" + attrs.get(name) - def update(key: Attributes.Name, value: String) = attrs.put(key, value) - } + implicit def enrichManifest(m: JManifest): Jar.WManifest = Jar.WManifest(m) private lazy val daemonThreadPool = DaemonThreadFactory.newPool() def runnable(body: => Unit): Runnable = new Runnable { override def run() = body } diff --git a/src/compiler/scala/tools/nsc/settings/AdvancedScalaSettings.scala b/src/compiler/scala/tools/nsc/settings/AdvancedScalaSettings.scala index 8f38470129..08b1f7374e 100644 --- a/src/compiler/scala/tools/nsc/settings/AdvancedScalaSettings.scala +++ b/src/compiler/scala/tools/nsc/settings/AdvancedScalaSettings.scala @@ -20,6 +20,7 @@ trait AdvancedScalaSettings { val future: BooleanSetting val generatephasegraph: StringSetting val logimplicits: BooleanSetting + val mainClass: StringSetting val migration: BooleanSetting val noforwarders: BooleanSetting val nojline: BooleanSetting diff --git a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala index ed432562dc..2e7cb9595a 100644 --- a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala +++ b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala @@ -76,6 +76,7 @@ trait ScalaSettings extends AbsScalaSettings val prompt = BooleanSetting ("-Xprompt", "Display a prompt after each error (debugging option).") val resident = BooleanSetting ("-Xresident", "Compiler stays resident: read source filenames from standard input.") val script = StringSetting ("-Xscript", "object", "Treat the source file as a script and wrap it in a main method.", "") + val mainClass = StringSetting ("-Xmain-class", "path", "Class for manifest's Main-Class entry (only useful with -d <jar>)", "") val Xshowcls = StringSetting ("-Xshow-class", "class", "Show internal representation of class.", "") val Xshowobj = StringSetting ("-Xshow-object", "object", "Show internal representation of object.", "") val showPhases = BooleanSetting ("-Xshow-phases", "Print a synopsis of compiler phases.") diff --git a/src/compiler/scala/tools/nsc/util/ClassPath.scala b/src/compiler/scala/tools/nsc/util/ClassPath.scala index 23b53fb29f..1487b42843 100644 --- a/src/compiler/scala/tools/nsc/util/ClassPath.scala +++ b/src/compiler/scala/tools/nsc/util/ClassPath.scala @@ -25,6 +25,8 @@ object ClassPath { def scalaLibrary = locate[ScalaObject] def scalaCompiler = locate[Global] + def infoFor[T](value: T) = info(value.getClass) + def info[T](clazz: Class[T]) = new ClassAndJarInfo()(ClassManifest fromClass clazz) def info[T: ClassManifest] = new ClassAndJarInfo[T] def locate[T: ClassManifest] = info[T] rootClasspath def locateJar[T: ClassManifest] = info[T].rootPossibles find (x => isJarOrZip(x)) map (x => File(x)) diff --git a/src/library/scala/util/Properties.scala b/src/library/scala/util/Properties.scala index d2c9e6770b..998661895b 100644 --- a/src/library/scala/util/Properties.scala +++ b/src/library/scala/util/Properties.scala @@ -10,11 +10,16 @@ package scala.util import java.io.{ IOException, PrintWriter } +import java.util.jar.Attributes.{ Name => AttributeName } /** Loads `library.properties` from the jar. */ object Properties extends PropertiesTrait { protected def propCategory = "library" protected def pickJarBasedOn = classOf[ScalaObject] + + /** Scala manifest attributes. + */ + val ScalaCompilerVersion = new AttributeName("Scala-Compiler-Version") } private[scala] trait PropertiesTrait { @@ -90,6 +95,11 @@ private[scala] trait PropertiesTrait { Some(s) } + /** Either the development or release version if known, otherwise + * the empty string. + */ + def versionNumberString = scalaPropOrEmpty("version.number") + /** The version number of the jar this was loaded from plus "version " prefix, * or "version (unknown)" if it cannot be determined. */ diff --git a/test/files/neg/main1.check b/test/files/neg/main1.check new file mode 100644 index 0000000000..734c78e54d --- /dev/null +++ b/test/files/neg/main1.check @@ -0,0 +1,26 @@ +main1.scala:3: error: Foo has a main method, but foo1.Foo will not be a runnable program. + Its companion is a trait, which means no static forwarder can be generated. + + object Foo { // companion is trait + ^ +main1.scala:10: error: Foo has a main method, but foo2.Foo will not be a runnable program. + Its companion contains its own main method, which means no static forwarder can be generated. + + object Foo { // companion has its own main + ^ +main1.scala:22: error: Foo has a main method, but foo3.Foo will not be a runnable program. + Its companion contains its own main method (implementation restriction: no main is allowed, regardless of signature), which means no static forwarder can be generated. + + object Foo { // Companion contains main, but not an interfering main. + ^ +main1.scala:31: error: Foo has a main method, but foo4.Foo will not be a runnable program. + Its companion contains its own main method (implementation restriction: no main is allowed, regardless of signature), which means no static forwarder can be generated. + + object Foo extends Foo { // Inherits main from the class + ^ +main1.scala:39: error: Foo has a main method, but foo5.Foo will not be a runnable program. + Its companion contains its own main method (implementation restriction: no main is allowed, regardless of signature), which means no static forwarder can be generated. + + object Foo extends Foo { // Overrides main from the class + ^ +5 errors found diff --git a/test/files/neg/main1.flags b/test/files/neg/main1.flags new file mode 100644 index 0000000000..e8fb65d50c --- /dev/null +++ b/test/files/neg/main1.flags @@ -0,0 +1 @@ +-Xfatal-warnings
\ No newline at end of file diff --git a/test/files/neg/main1.scala b/test/files/neg/main1.scala new file mode 100644 index 0000000000..2b5551ac38 --- /dev/null +++ b/test/files/neg/main1.scala @@ -0,0 +1,45 @@ +// negatives +package foo1 { + object Foo { // companion is trait + def main(args: Array[String]): Unit = () + } + trait Foo +} + +package foo2 { + object Foo { // companion has its own main + def main(args: Array[String]): Unit = () + } + class Foo { + def main(args: Array[String]): Unit = () + } +} + +// these should all be made to work, but are negatives for now +// because forwarders need more work. + +package foo3 { + object Foo { // Companion contains main, but not an interfering main. + def main(args: Array[String]): Unit = () + } + class Foo { + def main(args: Int): Unit = () + } +} + +package foo4 { + object Foo extends Foo { // Inherits main from the class + } + class Foo { + def main(args: Array[String]): Unit = () + } +} + +package foo5 { + object Foo extends Foo { // Overrides main from the class + override def main(args: Array[String]): Unit = () + } + class Foo { + def main(args: Array[String]): Unit = () + } +} diff --git a/test/pending/run/jar-version.scala b/test/pending/run/jar-version.scala new file mode 100644 index 0000000000..b79dfe733d --- /dev/null +++ b/test/pending/run/jar-version.scala @@ -0,0 +1,11 @@ +import scala.util.Properties._ +import scala.tools.nsc.util.ClassPath._ + +object Test { + def main(args: Array[String]): Unit = { + infoFor(this).jarManifestMainAttrs get ScalaCompilerVersion match { + case Some(v) => println("I was built by scala compiler version " + v) + case _ => println("I was not apprised of which scala compiler version built me.") + } + } +} |