sbt / sbt

sbt, the interactive build tool
https://scala-sbt.org
Apache License 2.0
4.81k stars 937 forks source link

Project using JLine 3.23.0 results in java.lang.NoSuchMethodError: org.jline.utils.AttributedString.fromAnsi #7177

Open stevedlawrence opened 1 year ago

stevedlawrence commented 1 year ago

steps

Create a project with these files to run a test using JLine 3.23.0 to read a line from a buffer:

build.sbt

name := "jline-test"

libraryDependencies ++= Seq(
    "org.fusesource.jansi" % "jansi" % "2.4.0",
    "org.jline" % "jline" % "3.23.0",
    "org.scalatest" %% "scalatest" % "3.2.15" % "test",
)

src/main/scala/Main.scala

package jlinetest

import org.jline.reader.LineReaderBuilder
import org.jline.terminal.impl.DumbTerminal

import java.io.InputStream
import java.io.OutputStream

object Foo {
  def getLine(in: InputStream, out: OutputStream): String = {
    val terminal = new DumbTerminal(in, out)
    val reader = LineReaderBuilder
      .builder()
      .terminal(terminal)
      .build()
    reader.readLine("> ")
  }
}

src/test/scala/Test.scala

package jlinetest

import org.scalatest.flatspec.AnyFlatSpec

import java.io.StringBufferInputStream
import java.io.ByteArrayOutputStream

class TestJLine extends AnyFlatSpec {

  "JLine 3.23.0" should "work with sbt" in {
    val is = new StringBufferInputStream("This is a line\n")
    val os = new ByteArrayOutputStream()
    val line = Foo.getLine(is, os)
    System.err.println(line)
  }

}

And run sbt test. You should get a failed test with this exception:

[info]   java.lang.NoSuchMethodError: org.jline.utils.AttributedString.fromAnsi(Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;)Lorg/jline/utils/AttributedString;
[info]   at org.jline.reader.impl.LineReaderImpl.fromAnsi(LineReaderImpl.java:4188)
[info]   at org.jline.reader.impl.LineReaderImpl.expandPromptPattern(LineReaderImpl.java:4110)
[info]   at org.jline.reader.impl.LineReaderImpl.setPrompt(LineReaderImpl.java:1213)
[info]   at org.jline.reader.impl.LineReaderImpl.readLine(LineReaderImpl.java:591)
[info]   at org.jline.reader.impl.LineReaderImpl.readLine(LineReaderImpl.java:475)
[info]   at jlinetest.Foo$.getLine(Main.scala:16)

problem

The new version of JLine modified org.jline.utils.AttributedString to add new functions (e.g. fromAnsi) and calls them from the LineReaderImpl.getLine function.

However, the way sbt sets up the classloader, LineReaderImpl is found in the jline-3.23.0.jar from in the Ivy cache, but AttributedString is found in the jline-terminal-3.19.0.jar in ~/.sbt/boot/... found via the JLineLoader. The older AttributedString is not compatible work with the newer LineReaderImpl, which leads to this exception.

I suspect JLine 3.22 works because the older version of AttributedString that sbt provides works with newer versions of Jline. This is no longer that case with JLine 3.23.

expectation

When running tests, jars needed by SBT (e.g. jline) should not be found on the classpath, instead preferring those that come from dependencies.

eed3si9n commented 1 year ago

could you try Test / fork := true please?

stevedlawrence commented 1 year ago

Forking tests fixes the issue, but we're hoping there's another solution. We take a big performance hit when we have to fork tests.

eed3si9n commented 1 year ago

If I remember correctly JLine is near the bottom of the layered classloader so I don't think there's good solution for now.

One potential fix might be persistent worker that I proposed in https://eed3si9n.com/sbt-2.0-ideas

tuxji commented 1 year ago

If sbt packages its classes and dependencies in an assembly jar (one of the ideas proposed in https://eed3si9n.com/sbt-2.0-ideas), sbt also can shade possibly conflicting dependencies like JLine under its own package, e.g., sbt.org.jline.reader.impl.LineReaderImpl instead of org.jline.reader.impl.LineReaderImpl. A caveat with shading dependencies is that it can create new problems as well as solve existing problems for some people (https://softwareengineering.stackexchange.com/questions/297276/what-is-a-shaded-java-dependency).