Skip to content
Draft
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
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ RUN git config --global --add safe.directory *
COPY . .

RUN sbt publishLocal dumpScipJavaVersion
RUN mkdir -p /app && coursier bootstrap "com.sourcegraph:scip-java_2.13:$(cat VERSION)" -f -o /app/scip-java -M com.sourcegraph.scip_java.ScipJava
RUN mkdir -p /app && coursier bootstrap "com.sourcegraph:scip-java:$(cat VERSION)" -f -o /app/scip-java -M com.sourcegraph.scip_java.ScipJava

COPY ./bin/scip-java-docker-script.sh /usr/bin/scip-java

Expand Down
83 changes: 67 additions & 16 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ lazy val V =
val protobuf = "4.34.2"
val scipBindings = "0.8.0"
val scalaXml = "2.1.0"
val moped = "0.2.0"
val gradle = "8.10"
val scala213 = "2.13.13"
val scalameta = "4.9.3"
val kotlinVersion = "2.2.0"
val kotest = "4.6.3"
val kctfork = "0.7.1"
val clikt = "5.0.3"
val kotlinxSerialization = "1.9.0"
}

// sbt-git's bundled JGit can't read linked worktrees; shell out to
Expand Down Expand Up @@ -196,28 +197,29 @@ lazy val mavenPlugin = project

lazy val cli = project
.in(file("scip-java"))
.enablePlugins(KotlinPlugin, PackPlugin, DockerPlugin)
.settings(
moduleName := "scip-java",
crossPaths := false,
autoScalaLibrary := false,
kotlinVersion := V.kotlinVersion,
kotlincJvmTarget := "11",
Compile / javacOptions ++= Seq("--release", "11"),
(Compile / mainClass) := Some("com.sourcegraph.scip_java.ScipJava"),
(run / baseDirectory) := (ThisBuild / baseDirectory).value,
// ScipJava.main can call System.exit, so we always fork the JVM when
// sbt invokes it directly (e.g. from the scip-kotlinc snapshots
// task) so it cannot kill the surrounding sbt process.
Compile / run / fork := true,
buildInfoKeys :=
Seq[BuildInfoKey](
version,
sbtVersion,
scalaVersion,
"javacModuleOptions" -> javacModuleOptions,
"scalametaVersion" -> V.scalameta,
"scala213" -> V.scala213
),
buildInfoPackage := "com.sourcegraph.scip_java",
// Generate a tiny Java `BuildInfo` class replacing the previous
// sbt-buildinfo-generated Scala object. Same shape as the Gradle plugin's
// `GradlePluginBuildInfo` (introduced in the Gradle plugin Kotlin port).
Compile / sourceGenerators += scipJavaCliBuildInfoGenerator.taskValue,
libraryDependencies ++=
List(
"org.scala-lang.modules" %% "scala-xml" % V.scalaXml,
"org.scalameta" %% "moped" % V.moped,
"com.github.ajalt.clikt" % "clikt-jvm" % V.clikt,
"org.jetbrains.kotlinx" % "kotlinx-serialization-json-jvm" %
V.kotlinxSerialization,
"org.jetbrains.kotlin" % "kotlin-compiler-embeddable" % V.kotlinVersion,
"org.jetbrains.kotlin" % "kotlin-scripting-common" % V.kotlinVersion,
"org.jetbrains.kotlin" % "kotlin-scripting-jvm" % V.kotlinVersion,
Expand Down Expand Up @@ -289,10 +291,59 @@ lazy val cli = project
docker / dockerfile :=
NativeDockerfile((ThisBuild / baseDirectory).value / "Dockerfile")
)
.enablePlugins(PackPlugin, DockerPlugin, BuildInfoPlugin)
.dependsOn(scip)

// Task key for regenerating the SCIP/SCIP golden snapshots emitted by
// Source-generator for the CLI's build-info Java class. Replaces the
// sbt-buildinfo-generated Scala BuildInfo object so the CLI module stays
// Kotlin/Java-only (and the generated class is straightforward to consume
// from Kotlin).
lazy val scipJavaCliBuildInfoGenerator = Def.task {
val out = (Compile / sourceManaged).value / "com" / "sourcegraph" /
"scip_java" / "BuildInfo.java"
IO.createDirectory(out.getParentFile)
val optionsLiteral = javacModuleOptions
.map(javaStringLiteral)
.mkString("Arrays.asList(", ", ", ")")
val versionLiteral = javaStringLiteral(version.value)
val contents =
s"""package com.sourcegraph.scip_java;
|
|import java.util.Arrays;
|import java.util.Collections;
|import java.util.List;
|
|public final class BuildInfo {
| private BuildInfo() {}
| public static final String version = $versionLiteral;
| public static final List<String> javacModuleOptions =
| Collections.unmodifiableList($optionsLiteral);
|}
|""".stripMargin
IO.write(out, contents)
Seq(out)
}

def javaStringLiteral(value: String): String = {
val escaped = value.flatMap {
case '\\' =>
"\\\\"
case '"' =>
"\\\""
case '\n' =>
"\\n"
case '\r' =>
"\\r"
case '\t' =>
"\\t"
case c if c.isControl =>
f"\\u${c.toInt}%04x"
case c =>
c.toString
}
"\"" + escaped + "\""
}

// Task key for regenerating the SCIP golden snapshots emitted by
// the scip-kotlinc compiler plugin over the Kotlin minimized fixtures.
// We deliberately do NOT call this `snapshots` to avoid colliding with the
// existing top-level `snapshots` test project (`lazy val snapshots = project`).
Expand Down Expand Up @@ -615,8 +666,8 @@ val testSettings = List(
libraryDependencies ++=
List(
"org.scalameta" %% "munit" % "0.7.29",
"org.scalameta" %% "moped-testkit" % V.moped,
"org.scalameta" %% "scalameta" % V.scalameta,
"com.lihaoyi" %% "os-lib" % "0.9.3",
"com.lihaoyi" %% "pprint" % "0.6.6"
)
)
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ Run `scip-java index --help` to learn more about the available command-line
options.

```scala mdoc:passthrough
com.sourcegraph.scip_java.ScipJava.printHelp(Console.out)
com.sourcegraph.scip_java.ScipJava.INSTANCE.printHelp(Console.out)
```

## Supported programming languages
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.sourcegraph.scip_aggregator

import com.sourcegraph.scip_java.CliReporter
import java.nio.file.NoSuchFileException

/**
* Console reporter for the `aggregate` command.
*
* The old reporter rendered an `InteractiveProgressBar` via moped's
* `paiges`-based renderer. Progress reporting is dropped here to keep the
* CLI runtime free of Scala libraries; the renderer was always silent
* when fewer than 100 files were processed anyway.
*/
class ConsoleScipAggregatorReporter(private val reporter: CliReporter) : ScipAggregatorReporter() {

override fun error(e: Throwable) {
if (e is NoSuchFileException) {
reporter.error("no such file: ${e.message}")
} else {
reporter.error(e)
}
}

override fun hasErrors(): Boolean = reporter.hasErrors()

override fun startProcessing(taskSize: Int) {}

override fun processedOneItem() {}

override fun endProcessing() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.sourcegraph.scip_java

import java.io.PrintStream
import java.nio.file.Path
import java.nio.file.Paths

/**
* Captures the per-invocation environment of a scip-java CLI run.
*
* Tests inject a custom environment to redirect stdout/stderr into a
* byte buffer, point the working directory at a temporary fixture
* directory, and so on.
*/
data class CliEnvironment(
val workingDirectory: Path = Paths.get("").toAbsolutePath(),
val environmentVariables: Map<String, String> = System.getenv(),
val standardOutput: PrintStream = System.out,
val standardError: PrintStream = System.err,
val isProgressBarEnabled: Boolean = System.console() != null,
) {
fun withWorkingDirectory(cwd: Path): CliEnvironment = copy(workingDirectory = cwd)

fun withStandardOutput(out: PrintStream): CliEnvironment = copy(standardOutput = out)

fun withStandardError(err: PrintStream): CliEnvironment = copy(standardError = err)
}
48 changes: 48 additions & 0 deletions scip-java/src/main/kotlin/com/sourcegraph/scip_java/CliReporter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.sourcegraph.scip_java

import java.util.concurrent.atomic.AtomicInteger

/**
* Minimal reporter that mirrors the moped `ConsoleReporter` API surface that
* scip-java actually uses (info/warning/error/debug/hasErrors/exitCode).
*
* `info` is written to stdout to match the previous behaviour of the
* default moped reporter; `warning` and `error` go to stderr.
*/
class CliReporter(private val env: CliEnvironment) {
private val errorCount = AtomicInteger()

fun info(message: String) {
env.standardOutput.println(message)
}

fun warning(message: String) {
env.standardError.println("warning: $message")
}

fun error(message: String) {
errorCount.incrementAndGet()
env.standardError.println("error: $message")
}

/**
* Debug messages are dropped to avoid leaking noise into snapshot tests.
*/
@Suppress("UNUSED_PARAMETER")
fun debug(message: String) {
// intentional no-op
}

fun error(e: Throwable) {
errorCount.incrementAndGet()
e.printStackTrace(env.standardError)
}

fun hasErrors(): Boolean = errorCount.get() > 0

fun exitCode(): Int = if (hasErrors()) 1 else 0

fun reset() {
errorCount.set(0)
}
}
105 changes: 105 additions & 0 deletions scip-java/src/main/kotlin/com/sourcegraph/scip_java/Embedded.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.sourcegraph.scip_java

import com.sourcegraph.scip_java.buildtools.ProcessResult
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption

object Embedded {

fun scipJar(tmpDir: Path): Path = copyFile(tmpDir, "scip-plugin.jar")

fun gradlePluginJar(tmpDir: Path): Path = copyFile(tmpDir, "gradle-plugin.jar")

fun scipKotlincJar(tmpDir: Path): Path = copyFile(tmpDir, "scip-kotlinc.jar")

private fun javacErrorpath(tmp: Path): Path = tmp.resolve("errorpath.txt")

fun customJavac(sourceroot: Path, targetroot: Path, tmp: Path): Path {
val bin = tmp.resolve("bin")
val javac = bin.resolve("javac")
val java = bin.resolve("java")
val pluginpath = scipJar(tmp)
val errorpath = javacErrorpath(tmp)
val javacopts = targetroot.resolve("javacopts.txt")
Files.createDirectories(targetroot)
Files.createDirectories(bin)
Files.write(
java,
("#!/usr/bin/env bash\n" +
"java \"\$@\"\n").toByteArray(StandardCharsets.UTF_8),
)
val newJavacopts = tmp.resolve("javac_newarguments")
// --add-exports flags required to access internal javac APIs from our
// SCIP plugin. Always set; Java 11+ is the supported baseline.
val javacModuleOptions = BuildInfo.javacModuleOptions.joinToString(" ")
val injectScipArguments =
listOf(
"java",
"-Dscip.errorpath=$errorpath",
"-Dscip.pluginpath=$pluginpath",
"-Dscip.sourceroot=$sourceroot",
"-Dscip.targetroot=$targetroot",
"-Dscip.output=\$NEW_JAVAC_OPTS",
"-Dscip.old-output=$javacopts",
"-classpath $pluginpath",
"com.sourcegraph.scip_javac.InjectScipOptions",
"\"\$@\"",
).joinToString(" ")
val script = buildString {
append("#!/usr/bin/env bash\n")
append("set -eu\n")
append("LAUNCHER_ARGS=()\n")
append("NEW_JAVAC_OPTS=\"$newJavacopts-\$RANDOM\"\n")
append("for arg in \"\$@\"; do\n")
append(" if [[ \$arg == -J* ]]; then\n")
append(" LAUNCHER_ARGS+=(\"\$arg\")\n")
append(" fi\n")
append("done\n")
append(injectScipArguments).append('\n')
append("if [ \${#LAUNCHER_ARGS[@]} -eq 0 ]; then\n")
append(" javac $javacModuleOptions \"@\$NEW_JAVAC_OPTS\"\n")
append("else\n")
append(" javac $javacModuleOptions \"@\$NEW_JAVAC_OPTS\" \"\${LAUNCHER_ARGS[@]}\"\n")
append("fi\n")
}
Files.write(javac, script.toByteArray(StandardCharsets.UTF_8))
javac.toFile().setExecutable(true)
java.toFile().setExecutable(true)
return javac
}

/**
* The custom javac wrapper reports errors to a specific file if unexpected
* errors happen. The javac wrapper gets invoked by builds tools like
* Gradle/Maven, which hide the actual errors from the script because they
* assume the standard output is from javac. This file is used a side-channel
* to avoid relying on the error reporting from Gradle/Maven.
*/
fun reportUnexpectedJavacErrors(reporter: CliReporter, tmp: Path): ProcessResult? {
val errorpath = javacErrorpath(tmp)
if (!Files.isRegularFile(errorpath)) return null
reporter.error("unexpected javac compile errors")
Files.readAllLines(errorpath).forEach { reporter.error(it) }
return ProcessResult(1)
}

/** Returns the string contents of the scip_java.bzl file on disk. */
fun bazelAspectFile(tmpDir: Path): String {
val tmpFile = copyFile(tmpDir, "scip-java/scip_java.bzl")
val contents = String(Files.readAllBytes(tmpFile), StandardCharsets.UTF_8)
Files.deleteIfExists(tmpFile)
return contents
}

private fun copyFile(tmpDir: Path, filename: String): Path {
val input =
Embedded::class.java.getResourceAsStream("/$filename")
?: error("missing embedded resource: /$filename")
val out = tmpDir.resolve(filename)
Files.createDirectories(out.parent)
input.use { Files.copy(it, out, StandardCopyOption.REPLACE_EXISTING) }
return out
}
}
28 changes: 28 additions & 0 deletions scip-java/src/main/kotlin/com/sourcegraph/scip_java/ScipJava.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.sourcegraph.scip_java

import java.io.PrintStream

/**
* Public entry point for the scip-java CLI. The single [app] instance is
* shared across test suites and reset between invocations.
*/
object ScipJava {

@JvmField
val app: ScipJavaApp = ScipJavaApp()

@JvmStatic
fun main(args: Array<String>) {
app.runAndExitIfNonZero(args.toList())
}

fun printHelp(out: PrintStream) {
out.println("```text")
out.println("$ scip-java index --help")
val replacement = ScipJavaApp().apply {
env = env.withStandardOutput(out).withStandardError(out)
}
replacement.run(listOf("index", "--help"))
out.println("```")
}
}
Loading
Loading