Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,20 @@ You should have langoustine-tracer launcher in server root.
```bash
cs bootstrap tech.neander:langoustine-tracer_3:latest.release -f -o langoustine-tracer
```

## Debugging the presentation compiler

The PC is loaded from `org.scala-lang:scala3-presentation-compiler_3` at the build target's own
Scala version. To test a different PC build (e.g. a locally `publishLocal`-ed dotty), override the
version at launch — no recompile:

```bash
# all modules
SLS_PC_VERSION=3.8.4-RC1-bin-SNAPSHOT ./mill sls.run # or -Dsls.pc.version=...

# per module (keyed by build-target display name); falls back to SLS_PC_VERSION, then the target's version
SLS_PC_VERSIONS='sls=3.8.4-RC1-bin-SNAPSHOT,zincCli=3.8.3' ./mill sls.run # or -Dsls.pc.versions=...
```

Coursier's `ivy2Local` is on the default resolver, so a `publishLocal`-ed snapshot resolves directly.
An active override is logged per module on PC creation.
2 changes: 1 addition & 1 deletion build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ object sls extends CommonScalaModule {
mvn"ch.qos.logback:logback-classic:1.4.14",
mvn"com.lihaoyi::os-lib:0.11.4",
mvn"org.polyvariant.smithy4s-bsp::bsp4s:0.6.0",
mvn"org.scalameta:mtags-interfaces:1.6.3",
mvn"org.scalameta:mtags-interfaces:1.6.7",
mvn"com.evolution::scache:5.1.2",
mvn"org.typelevel::cats-parse:1.1.0",
mvn"io.get-coursier:interface:1.0.28",
Expand Down
5 changes: 5 additions & 0 deletions sls/src/org/scala/abusers/sls/ServerImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,11 @@ class ServerImpl(
implicit val rangeTransformer: Transformer[lsp4j.Range, lsp.Range] =
Transformer.define[lsp4j.Range, lsp.Range].enableBeanGetters.buildTransformer

// Newer lsp4j models Diagnostic.message as Either[String, MarkupContent]; lsp.Diagnostic.message is a plain String.
implicit val diagnosticMessageTransformer
: Transformer[lsp4j.jsonrpc.messages.Either[String, lsp4j.MarkupContent], String] =
e => if e.isLeft then e.getLeft else e.getRight.getValue

val handleDidChange: SynchronizedState ?=> lsp.DidChangeTextDocumentParams => IO[Unit] = {
val debounce = Debouncer(250.millis)

Expand Down
14 changes: 14 additions & 0 deletions sls/src/org/scala/abusers/sls/pc/PcParentClassLoader.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.scala.abusers.pc

/** Parent for the PC's `URLClassLoader`. Parent is the bootstrap loader, so `dotty.tools.*` and the stdlib load from
* the PC's own jars (not sls's bundled scala3-compiler). Only the host<->PC bridge packages delegate to the host, so
* they keep one class identity across the boundary.
*/
final class PcParentClassLoader(host: ClassLoader) extends ClassLoader(null) {

private val sharedPrefixes = List("scala.meta.pc.", "org.eclipse.lsp4j", "com.google.gson")

override def findClass(name: String): Class[?] =
if sharedPrefixes.exists(name.startsWith) then host.loadClass(name)
else throw new ClassNotFoundException(name)
}
40 changes: 40 additions & 0 deletions sls/src/org/scala/abusers/sls/pc/PcVersionOverride.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.scala.abusers.pc

/** Overrides the `scala3-presentation-compiler_3` version, decoupled from the build target's Scala version. Per-module
* (`-Dsls.pc.versions=mod=ver,...` / `SLS_PC_VERSIONS`, keyed by display name) over a global (`-Dsls.pc.version` /
* `SLS_PC_VERSION`); falls back to the target's own version.
*/
final case class PcVersionOverride(perModule: Map[String, String], global: Option[String]) {

def resolve(module: String, default: ScalaVersion): ScalaVersion =
perModule.get(module).orElse(global).map(ScalaVersion(_)).getOrElse(default)
}

object PcVersionOverride {

val empty: PcVersionOverride = PcVersionOverride(Map.empty, None)

/** Parse `module=version,module=version`; malformed entries are dropped. */
def parseMap(raw: String): Map[String, String] =
raw
.split(',')
.iterator
.map(_.trim)
.filter(_.nonEmpty)
.flatMap { entry =>
entry.split("=", 2) match {
case Array(k, v) if k.trim.nonEmpty && v.trim.nonEmpty => Some(k.trim -> v.trim)
case _ => None
}
}
.toMap

private def read(prop: String, env: String): Option[String] =
Option(System.getProperty(prop)).orElse(Option(System.getenv(env))).map(_.trim).filter(_.nonEmpty)

def fromEnv: PcVersionOverride =
PcVersionOverride(
perModule = read("sls.pc.versions", "SLS_PC_VERSIONS").map(parseMap).getOrElse(Map.empty),
global = read("sls.pc.version", "SLS_PC_VERSION"),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import org.scala.abusers.sls.CoursierResolver
import org.scala.abusers.sls.ScalaBuildTargetInformation
import org.scala.abusers.sls.ScalaBuildTargetInformation.*
import org.scala.abusers.sls.SynchronizedState
import org.slf4j.LoggerFactory

import java.net.URLClassLoader
import scala.concurrent.duration.*
Expand All @@ -24,8 +25,10 @@ trait PresentationCompilerProvider {
private class CachingPresentationCompilerProvider(
serviceLoader: BlockingServiceLoader,
compilers: SCache[IO, BuildTargetIdentifier, RawPresentationCompiler],
versionOverride: PcVersionOverride,
) extends PresentationCompilerProvider {
private val cache = Cache.create() // .withLogger TODO No completions here
private val logger = LoggerFactory.getLogger(getClass)
private val cache = Cache.create() // .withLogger TODO No completions here

private def fetchPresentationCompilerJars(scalaVersion: ScalaVersion): IO[Seq[AbsolutePath]] = {
val dep = Dependency.of(
Expand All @@ -49,7 +52,7 @@ private class CachingPresentationCompilerProvider(
IO.blocking {
val fullClasspath = compilerClasspath ++ projectClasspath
val urlFullClasspath = fullClasspath.map(_.toFile.toURI.toURL)
URLClassLoader(urlFullClasspath.toArray)
URLClassLoader(urlFullClasspath.toArray, PcParentClassLoader(getClass.getClassLoader))
}

private def createPC(scalaVersion: ScalaVersion, projectClasspath: List[AbsolutePath], scalacOptions: List[String]) =
Expand All @@ -58,13 +61,27 @@ private class CachingPresentationCompilerProvider(
classloader <- freshPresentationCompilerClassloader(projectClasspath, compilerClasspath)
pc <- serviceLoader.load(classOf[RawPresentationCompiler], PresentationCompilerProvider.classname, classloader)
scalacOptions0 = scalacOptions ++ Seq("-Ywith-best-effort-tasty", "-Ybest-effort")
_ <- IO.consoleForIO.error(
s"Creating presentation compiler with classpath: ${projectClasspath.map(_.toNioPath.toString).mkString(", ")} and options: ${scalacOptions0.mkString(" ")}"
_ <- IO(
logger.info(
s"Creating presentation compiler with classpath: ${projectClasspath.map(_.toNioPath.toString).mkString(", ")} and options: ${scalacOptions0.mkString(" ")}"
)
)
} yield pc.newInstance("pc-id-replace", projectClasspath.map(_.toNioPath).asJava, scalacOptions0.toList.asJava)

def get(info: ScalaBuildTargetInformation)(using SynchronizedState): IO[RawPresentationCompiler] =
compilers.getOrUpdate(info.buildTarget.id)(createPC(info.scalaVersion, info.classpath, info.compilerOptions))
def get(info: ScalaBuildTargetInformation)(using SynchronizedState): IO[RawPresentationCompiler] = {
val pcVersion = versionOverride.resolve(info.displayName, info.scalaVersion)
val logOverride = IO.whenA(pcVersion.value != info.scalaVersion.value) {
IO(
logger.info(
s"PC version override for module '${info.displayName}': ${info.scalaVersion.value} -> ${pcVersion.value}"
)
)
}
// in the thunk so it logs once per PC creation, not per request
compilers.getOrUpdate(info.buildTarget.id)(
logOverride *> createPC(pcVersion, info.classpath, info.compilerOptions)
)
}
}

object PresentationCompilerProvider {
Expand All @@ -82,7 +99,7 @@ object PresentationCompilerProvider {
ExpiringCache.Config(expireAfterRead = 5.minutes),
None,
)
} yield CachingPresentationCompilerProvider(serviceLoader, cache)
} yield CachingPresentationCompilerProvider(serviceLoader, cache, PcVersionOverride.fromEnv)
}

opaque type ScalaVersion = String
Expand Down
18 changes: 18 additions & 0 deletions sls/test/src/org/scala/abusers/sls/pc/PcVersionOverrideSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.scala.abusers.pc

import weaver.SimpleIOSuite

object PcVersionOverrideSpec extends SimpleIOSuite {

pureTest("parseMap reads module=version entries and drops malformed ones") {
val parsed = PcVersionOverride.parseMap("sls=3.8.4-SNAPSHOT, zincCli=3.8.3 ,broken,=novalue,nokey=")
expect(parsed == Map("sls" -> "3.8.4-SNAPSHOT", "zincCli" -> "3.8.3"))
}

pureTest("per-module entry wins over global, global wins over default, default otherwise") {
val ov = PcVersionOverride(perModule = Map("sls" -> "9.9.9"), global = Some("8.8.8"))
expect(ov.resolve("sls", ScalaVersion("3.8.3")).value == "9.9.9") and
expect(ov.resolve("zincCli", ScalaVersion("3.8.3")).value == "8.8.8") and
expect(PcVersionOverride.empty.resolve("sls", ScalaVersion("3.8.3")).value == "3.8.3")
}
}