Skip to content
This repository was archived by the owner on Sep 1, 2020. It is now read-only.

Commit dfb70b6

Browse files
committed
SI-9339 Support classpaths with no single compatible jline
As usual, the repl will use whatever jline 2 jar on the classpath, if there is one. Failing that, there's a fallback and an override. If instantiating the standard `jline.InteractiveReader` fails, we fall back to an embedded, shaded, version of jline, provided by `jline_embedded.InteractiveReader`. (Assume `import scala.tools.nsc.interpreter._` for this message.) The instantiation of `InteractiveReader` eagerly exercises jline, so that a linkage error will result if jline is missing or if the provided one is not binary compatible. The property `scala.repl.reader` overrides this behavior, if set to the FQN of a class that looks like `YourInteractiveReader` below. ``` class YourInteractiveReader(completer: () => Completion) extends InteractiveReader ``` The repl logs which classes it tried to instantiate under `-Ydebug`. # Changes to source & build The core of the repl (`src/repl`) no longer depends on jline. The jline interface is now in `src/repl-jline`. The embedded jline + our interface to it are generated by the `quick.repl` target. The build now also enforces that only `src/repl-jline` depends on jline. The sources in `src/repl` are now sure to be independent of it, though they do use reflection to instantiate a suitable subclass of `InteractiveReader`, as explained above. The `quick.repl` target builds the sources in `src/repl` and `src/repl-jline`, producing a jar for the `repl-jline` classes, which is then transformed using jarjar to obtain a shaded copy of the `scala.tools.nsc.interpreter.jline` package. Jarjar is used to combine the `jline` jar and the `repl-jline` into a new jar, rewriting package names as follows: - `org.fusesource` -> `scala.tools.fusesource_embedded` - `jline` -> `scala.tools.jline_embedded` - `scala.tools.nsc.interpreter.jline` -> `scala.tools.nsc.interpreter.jline_embedded` Classes not reachable from `scala.tools.**` are pruned, as well as empty dirs. The classes in the `repl-jline` jar as well as those in the rewritten one are copied to the repl's output directory. PS: The sbt build is not updated, sorry. PPS: A more recent fork of jarjar: https://github.com/shevek/jarjar.
1 parent 43139fa commit dfb70b6

File tree

7 files changed

+70
-13
lines changed

7 files changed

+70
-13
lines changed

build.sbt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ lazy val interactive = configureAsSubproject(project)
191191
.settings(disableDocsAndPublishingTasks: _*)
192192
.dependsOn(compiler)
193193

194+
// TODO: SI-9339 embed shaded copy of jline & its interface (see #4563)
194195
lazy val repl = configureAsSubproject(project)
195196
.settings(libraryDependencies += jlineDep)
196197
.settings(disableDocsAndPublishingTasks: _*)

build.xml

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,10 @@ TODO:
275275
<dependency groupId="biz.aQute" artifactId="bnd" version="1.50.0"/>
276276
</artifact:dependencies>
277277

278+
<artifact:dependencies pathId="jarjar.classpath">
279+
<dependency groupId="com.googlecode.jarjar" artifactId="jarjar" version="1.3"/>
280+
</artifact:dependencies>
281+
278282
<!-- JUnit -->
279283
<property name="junit.version" value="4.11"/>
280284
<artifact:dependencies pathId="junit.classpath" filesetId="junit.fileset">
@@ -696,7 +700,7 @@ TODO:
696700
<property name="partest-javaagent.description" value="Scala Compiler Testing Tool (compiler-specific java agent)"/>
697701

698702
<!-- projects without project-specific options: forkjoin, manual, bin, repl -->
699-
<for list="actors,compiler,interactive,scaladoc,library,parser-combinators,partest,partest-extras,partest-javaagent,reflect,scalap,swing,xml,continuations-plugin,continuations-library" param="project">
703+
<for list="actors,compiler,interactive,scaladoc,library,parser-combinators,partest,partest-extras,partest-javaagent,reflect,scalap,swing,xml,continuations-plugin,continuations-library,repl-jline" param="project">
700704
<sequential>
701705
<!-- description is mandatory -->
702706
<init-project-prop project="@{project}" name="package" default=""/> <!-- used by mvn-package, copy-bundle, make-bundle -->
@@ -799,6 +803,11 @@ TODO:
799803
<path id="quick.repl.build.path">
800804
<path refid="quick.compiler.build.path"/>
801805
<pathelement location="${build-quick.dir}/classes/repl"/>
806+
</path>
807+
808+
<path id="quick.repl-jline.build.path">
809+
<path refid="quick.repl.build.path"/>
810+
<pathelement location="${build-quick.dir}/classes/repl-jline"/>
802811
<path refid="repl.deps.classpath"/>
803812
</path>
804813

@@ -873,6 +882,8 @@ TODO:
873882
<fileset dir="${build-quick.dir}/classes/actors"/>
874883
</path>
875884

885+
<path id="pack.repl-jline.files"> <fileset dir="${build-quick.dir}/classes/repl-jline"/> </path>
886+
876887
<path id="pack.compiler.files">
877888
<fileset dir="${build-quick.dir}/classes/compiler"/>
878889

@@ -1076,6 +1087,7 @@ TODO:
10761087
</patternset>
10771088

10781089
<taskdef resource="scala/tools/ant/sabbus/antlib.xml" classpathref="starr.compiler.path"/>
1090+
<taskdef name="jarjar" classname="com.tonicsystems.jarjar.JarJarTask" classpathref="jarjar.classpath" />
10791091
</target>
10801092

10811093
<!-- ===========================================================================
@@ -1157,7 +1169,31 @@ TODO:
11571169
<staged-build with="locker" stage="quick" project="compiler"/> </target>
11581170

11591171
<target name="quick.repl" depends="quick.comp">
1160-
<staged-build with="locker" stage="quick" project="repl"/> </target>
1172+
<staged-build with="locker" stage="quick" project="repl"/>
1173+
<staged-build with="locker" stage="quick" project="repl-jline"/>
1174+
1175+
<staged-pack project="repl-jline"/>
1176+
1177+
<!-- make jline_embedded jar with classes of repl-jline and jline, then shade-->
1178+
<jarjar jarfile="${build-pack.dir}/${repl-jline.targetdir}/scala-repl-jline-embedded.jar" whenmanifestonly="fail">
1179+
<zipfileset src="${jline:jline:jar}"/>
1180+
<zipfileset src="${build-pack.dir}/${repl-jline.targetdir}/${repl-jline.targetjar}"/>
1181+
1182+
<rule pattern="org.fusesource.**" result="scala.tools.fusesource_embedded.@1"/>
1183+
<rule pattern="jline.**" result="scala.tools.jline_embedded.@1"/>
1184+
<rule pattern="scala.tools.nsc.interpreter.jline.**" result="scala.tools.nsc.interpreter.jline_embedded.@1"/>
1185+
<keep pattern="scala.tools.**"/>
1186+
</jarjar>
1187+
1188+
<!-- unzip jar to repl's class dir to obtain
1189+
- standard repl-jline
1190+
- a shaded repl-jline (scala/tools/nsc/interpreter/jline_embedded) & jline (scala.tools.jline_embedded)
1191+
-->
1192+
<copy todir="${build-quick.dir}/classes/repl">
1193+
<zipfileset src="${build-pack.dir}/${repl-jline.targetdir}/${repl-jline.targetjar}"/>
1194+
<zipfileset src="${build-pack.dir}/${repl-jline.targetdir}/scala-repl-jline-embedded.jar"/>
1195+
</copy>
1196+
</target>
11611197

11621198
<target name="quick.scaladoc" depends="quick.comp">
11631199
<staged-build with="locker" stage="quick" project="scaladoc" version="scaladoc"/> </target>

src/repl/scala/tools/nsc/interpreter/jline/JLineReader.scala renamed to src/repl-jline/scala/tools/nsc/interpreter/jline/JLineReader.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import scala.tools.nsc.interpreter.session.History
2424
*
2525
* Eagerly instantiates all relevant JLine classes, so that we can detect linkage errors on `new JLineReader` and retry.
2626
*/
27-
class JLineReader(completer: () => Completion) extends interpreter.InteractiveReader {
27+
class InteractiveReader(completer: () => Completion) extends interpreter.InteractiveReader {
2828
val interactive = true
2929

3030
val history: History = new JLineHistory.JLineFileHistory()

src/repl/scala/tools/nsc/interpreter/ILoop.scala

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import scala.concurrent.{ ExecutionContext, Await, Future, future }
2626
import ExecutionContext.Implicits._
2727
import java.io.{ BufferedReader, FileReader }
2828

29+
import scala.util.{Try, Success, Failure}
30+
2931
/** The Scala interactive shell. It provides a read-eval-print loop
3032
* around the Interpreter class.
3133
* After instantiation, clients should call the main() method.
@@ -860,18 +862,36 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter)
860862
* with SimpleReader.
861863
*/
862864
def chooseReader(settings: Settings): InteractiveReader = {
863-
def mkJLineReader(completer: () => Completion): InteractiveReader =
864-
try new jline.JLineReader(completer)
865-
catch {
866-
case ex@(_: Exception | _: NoClassDefFoundError) =>
867-
Console.println(f"Failed to created JLineReader: ${ex}%nFalling back to SimpleReader.")
868-
SimpleReader()
869-
}
870-
871865
if (settings.Xnojline || Properties.isEmacsShell) SimpleReader()
872866
else {
873-
if (settings.noCompletion) mkJLineReader(() => NoCompletion)
874-
else mkJLineReader(() => new JLineCompletion(intp))
867+
type Completer = () => Completion
868+
type ReaderMaker = Completer => InteractiveReader
869+
870+
def instantiate(className: String): ReaderMaker = completer => {
871+
if (settings.debug) Console.println(s"Trying to instantiate a InteractiveReader from $className")
872+
Class.forName(className).getConstructor(classOf[Completer]).
873+
newInstance(completer).
874+
asInstanceOf[InteractiveReader]
875+
}
876+
877+
def mkReader(maker: ReaderMaker) =
878+
if (settings.noCompletion) maker(() => NoCompletion)
879+
else maker(() => new JLineCompletion(intp)) // JLineCompletion is a misnomer -- it's not tied to jline
880+
881+
def internalClass(kind: String) = s"scala.tools.nsc.interpreter.$kind.InteractiveReader"
882+
val readerClasses = sys.props.get("scala.repl.reader").toStream ++ Stream(internalClass("jline"), internalClass("jline_embedded"))
883+
val readers = readerClasses map (cls => Try { mkReader(instantiate(cls)) })
884+
885+
val reader = (readers collect { case Success(reader) => reader } headOption) getOrElse SimpleReader()
886+
887+
if (settings.debug) {
888+
val readerDiags = (readerClasses, readers).zipped map {
889+
case (cls, Failure(e)) => s" - $cls --> " + e.getStackTrace.mkString(e.toString+"\n\t", "\n\t","\n")
890+
case (cls, Success(_)) => s" - $cls OK"
891+
}
892+
Console.println(s"All InteractiveReaders tried: ${readerDiags.mkString("\n","\n","\n")}")
893+
}
894+
reader
875895
}
876896
}
877897

0 commit comments

Comments
 (0)