Skip to content
1 change: 1 addition & 0 deletions modules/build/src/main/scala/scala/build/bsp/BspImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ final class BspImpl(
val generatedSourcesTest = sourcesTest.generateSources(allInputs.generatedSrcRoot(Scope.Test))

bspServer.setExtraDependencySources(options0Main.classPathOptions.extraSourceJars)
bspServer.setExtraTestDependencySources(options0Test.classPathOptions.extraSourceJars)
bspServer.setGeneratedSources(Scope.Main, generatedSourcesMain)
bspServer.setGeneratedSources(Scope.Test, generatedSourcesTest)

Expand Down
16 changes: 13 additions & 3 deletions modules/build/src/main/scala/scala/build/bsp/BspServer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,16 @@ class BspServer(

override def onConnectWithClient(client: BuildClient): Unit = this.client = Some(client)

private var extraDependencySources: Seq[os.Path] = Nil
@volatile private var extraDependencySources: Seq[os.Path] = Nil
def setExtraDependencySources(sourceJars: Seq[os.Path]): Unit = {
extraDependencySources = sourceJars
}

@volatile private var extraTestDependencySources: Seq[os.Path] = Nil
def setExtraTestDependencySources(sourceJars: Seq[os.Path]): Unit = {
extraTestDependencySources = sourceJars
}

// Can we accept some errors in some circumstances?
override protected def onFatalError(throwable: Throwable, context: String): Unit = {
val sw = new StringWriter()
Expand Down Expand Up @@ -192,8 +197,13 @@ class BspServer(
super.buildTargetDependencySources(check(params)).thenApply { res =>
val updatedItems = res.getItems.asScala.map {
case item if validTarget(item.getTarget) =>
val updatedSources = item.getSources.asScala ++ extraDependencySources.map { sourceJar =>
sourceJar.toNIO.toUri.toASCIIString
val isTestTarget = item.getTarget.getUri.endsWith("-test")
val validExtraDependencySources =
if isTestTarget then (extraDependencySources ++ extraTestDependencySources).distinct
else extraDependencySources
val updatedSources = item.getSources.asScala ++ validExtraDependencySources.map {
sourceJar =>
sourceJar.toNIO.toUri.toASCIIString
}
new b.DependencySourcesItem(item.getTarget, updatedSources.asJava)
case other => other
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,4 +290,39 @@ class DirectiveTests extends munit.FunSuite {
}
}

def testSourceJar(getDirectives: (String, String) => String): Unit = {
val dummyJar = "Dummy.jar"
val dummySourcesJar = "Dummy-sources.jar"
TestInputs(
os.rel / "Main.scala" ->
s"""${getDirectives(dummyJar, dummySourcesJar)}
|object Main extends App {
| println("Hello")
|}
|""".stripMargin,
os.rel / dummyJar -> "dummy",
os.rel / dummySourcesJar -> "dummy-sources"
).withBuild(baseOptions, buildThreads, bloopConfigOpt) {
(root, _, maybeBuild) =>
val build = maybeBuild.orThrow
val Some(jar) = build.options.classPathOptions.extraClassPath.headOption
expect(jar == root / dummyJar)
val Some(sourceJar) = build.options.classPathOptions.extraSourceJars.headOption
expect(sourceJar == root / dummySourcesJar)
}
}

test("source jar") {
testSourceJar((dummyJar, dummySourcesJar) =>
s"""//> using jar $dummyJar
|//> using sourceJar $dummySourcesJar""".stripMargin
)
}

test("assumed source jar") {
testSourceJar((dummyJar, dummySourcesJar) =>
s"//> using jars $dummyJar $dummySourcesJar"
)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class Default(
args.remaining.nonEmpty || options.shared.snippet.executeScript.nonEmpty ||
options.shared.snippet.executeScala.nonEmpty || options.shared.snippet.executeJava.nonEmpty ||
options.shared.snippet.executeMarkdown.nonEmpty ||
(options.shared.extraJarsAndClassPath.nonEmpty && options.sharedRun.mainClass.mainClass.nonEmpty)
(options.shared.extraClasspathWasPassed && options.sharedRun.mainClass.mainClass.nonEmpty)
if shouldDefaultToRun then RunOptions.parser else ReplOptions.parser
}.parse(options.legacyScala.filterNonDeprecatedArgs(rawArgs, progName, logger)) match
case Left(e) => error(e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ final case class PackageOptions(
library: Boolean = false,
@Group(HelpGroup.Package.toString)
@HelpMessage("Generate a source JAR rather than an executable JAR")
@Name("sources")
@Name("src")
@Name("sourceJar")
@Tag(tags.restricted)
@Tag(tags.inShortHelp)
source: Boolean = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ import scala.build.input.{Element, Inputs, ResourceDirectory, ScalaCliInvokeData
import scala.build.interactive.Interactive
import scala.build.interactive.Interactive.{InteractiveAsk, InteractiveNop}
import scala.build.internal.CsLoggerUtil.*
import scala.build.internal.util.ConsoleUtils.ScalaCliConsole
import scala.build.internal.{Constants, FetchExternalBinary, ObjectCodeWrapper, OsLibc, Util}
import scala.build.options.ScalaVersionUtil.fileWithTtl0
import scala.build.options.{Platform, ScalacOpt, ShadowingSeq}
import scala.build.preprocessing.directives.ClasspathUtils.*
import scala.build.preprocessing.directives.Toolkit
import scala.build.options as bo
import scala.cli.ScalaCli
Expand Down Expand Up @@ -298,6 +300,14 @@ final case class SharedOptions(
case _ => Right(None)
}
}
val (assumedSourceJars, extraRegularJarsAndClasspath) =
extraJarsAndClassPath.partition(_.hasSourceJarSuffix)
if assumedSourceJars.nonEmpty then
val assumedSourceJarsString = assumedSourceJars.mkString(", ")
logger.message(
s"""[${Console.YELLOW}warn${Console.RESET}] Jars with the ${ScalaCliConsole.GRAY}*-sources.jar${Console.RESET} name suffix are assumed to be source jars.
|The following jars were assumed to be source jars and will be treated as such: $assumedSourceJarsString""".stripMargin
)
bo.BuildOptions(
suppressWarningOptions =
bo.SuppressWarningOptions(
Expand Down Expand Up @@ -350,8 +360,9 @@ final case class SharedOptions(
runJmh = if (enableJmh) Some(true) else None
),
classPathOptions = bo.ClassPathOptions(
extraClassPath = extraJarsAndClassPath,
extraClassPath = extraRegularJarsAndClasspath,
extraCompileOnlyJars = extraCompileOnlyClassPath,
extraSourceJars = extraSourceJars.extractedClassPath ++ assumedSourceJars,
extraRepositories = dependencies.repository.map(_.trim).filter(_.nonEmpty),
extraDependencies = ShadowingSeq.from(
SharedOptions.parseDependencies(
Expand Down Expand Up @@ -399,6 +410,8 @@ final case class SharedOptions(
(extraJars ++ scalac.scalacOption.getScalacOption("-classpath"))
.extractedClassPath

def extraClasspathWasPassed: Boolean = extraJarsAndClassPath.exists(!_.hasSourceJarSuffix)

def extraCompileOnlyClassPath: List[os.Path] = extraCompileOnlyJars.extractedClassPath

def globalInteractiveWasSuggested: Either[BuildException, Option[Boolean]] = either {
Expand Down Expand Up @@ -554,7 +567,7 @@ final case class SharedOptions(
javaSnippetList = allJavaSnippets,
markdownSnippetList = allMarkdownSnippets,
enableMarkdown = markdown.enableMarkdown,
extraClasspathWasPassed = extraJarsAndClassPath.nonEmpty
extraClasspathWasPassed = extraClasspathWasPassed
)

def allScriptSnippets: List[String] = snippet.scriptSnippet ++ snippet.executeScript
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package scala.build.preprocessing.directives

object ClasspathUtils {
extension (classpathItem: os.Path) {
def hasSourceJarSuffix: Boolean = classpathItem.last.endsWith("-sources.jar")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,24 @@ import scala.build.directives.*
import scala.build.errors.{BuildException, CompositeBuildException, WrongJarPathError}
import scala.build.options.{BuildOptions, ClassPathOptions, Scope, WithBuildRequirements}
import scala.build.preprocessing.ScopePath
import scala.build.preprocessing.directives.ClasspathUtils.*
import scala.build.preprocessing.directives.CustomJar.JarType
import scala.build.{Logger, Positioned}
import scala.cli.commands.SpecificationLevel
import scala.util.{Failure, Success, Try}

@DirectiveGroupName("Custom JAR")
@DirectiveExamples(
"//> using jar /Users/alexandre/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/com/chuusai/shapeless_2.13/2.3.7/shapeless_2.13-2.3.7.jar"
) @DirectiveExamples(
)
@DirectiveExamples(
"//> using test.jar /Users/alexandre/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/com/chuusai/shapeless_2.13/2.3.7/shapeless_2.13-2.3.7.jar"
)
@DirectiveExamples("//> using sourceJar /path/to/custom-jar-sources.jar")
@DirectiveExamples(
"//> using sourceJars /path/to/custom-jar-sources.jar /path/to/another-jar-sources.jar"
)
@DirectiveExamples("//> using test.sourceJar /path/to/test-custom-jar-sources.jar")
@DirectiveUsage(
"`//> using jar `_path_ | `//> using jars `_path1_ _path2_ …",
"""//> using jar _path_
Expand All @@ -29,19 +37,47 @@ final case class CustomJar(
@DirectiveName("test.jar")
@DirectiveName("test.jars")
testJar: DirectiveValueParser.WithScopePath[List[Positioned[String]]] =
DirectiveValueParser.WithScopePath.empty(Nil),
@DirectiveName("sources.jar")
@DirectiveName("sourcesJars")
@DirectiveName("sources.jars")
@DirectiveName("sourceJar")
@DirectiveName("source.jar")
@DirectiveName("sourceJars")
@DirectiveName("source.jars")
sourcesJar: DirectiveValueParser.WithScopePath[List[Positioned[String]]] =
DirectiveValueParser.WithScopePath.empty(Nil),
@DirectiveName("test.sources.jar")
@DirectiveName("test.sourcesJars")
@DirectiveName("test.sources.jars")
@DirectiveName("test.sourceJar")
@DirectiveName("test.source.jar")
@DirectiveName("test.sourceJars")
@DirectiveName("test.source.jars")
testSourcesJar: DirectiveValueParser.WithScopePath[List[Positioned[String]]] =
DirectiveValueParser.WithScopePath.empty(Nil)
) extends HasBuildOptionsWithRequirements {
def buildOptionsList: List[Either[BuildException, WithBuildRequirements[BuildOptions]]] =
List(
CustomJar.buildOptions(jar).map(_.withEmptyRequirements),
CustomJar.buildOptions(testJar).map(_.withScopeRequirement(Scope.Test))
CustomJar.buildOptions(jar, JarType.Jar)
.map(_.withEmptyRequirements),
CustomJar.buildOptions(testJar, JarType.Jar)
.map(_.withScopeRequirement(Scope.Test)),
CustomJar.buildOptions(sourcesJar, JarType.SourcesJar)
.map(_.withEmptyRequirements),
CustomJar.buildOptions(testSourcesJar, JarType.SourcesJar)
.map(_.withScopeRequirement(Scope.Test))
)
}

object CustomJar {
val handler: DirectiveHandler[CustomJar] = DirectiveHandler.derive
def buildOptions(jar: DirectiveValueParser.WithScopePath[List[Positioned[String]]])
: Either[BuildException, BuildOptions] = {
enum JarType:
case Jar, SourcesJar
def buildOptions(
jar: DirectiveValueParser.WithScopePath[List[Positioned[String]]],
jarType: JarType
): Either[BuildException, BuildOptions] = {
val cwd = jar.scopePath
jar.value
.map { posPathStr =>
Expand All @@ -56,11 +92,12 @@ object CustomJar {
.sequence
.left.map(CompositeBuildException(_))
.map { paths =>
BuildOptions(
classPathOptions = ClassPathOptions(
extraClassPath = paths
)
)
val classPathOptions = jarType match
case JarType.Jar =>
val (sourceJars, regularJars) = paths.partition(_.hasSourceJarSuffix)
ClassPathOptions(extraClassPath = regularJars, extraSourceJars = sourceJars)
case JarType.SourcesJar => ClassPathOptions(extraSourceJars = paths)
BuildOptions(classPathOptions = classPathOptions)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1373,6 +1373,107 @@ abstract class BspTestDefinitions(val scalaVersionOpt: Option[String])
}
}

def testSourceJars(
directives: String = "//> using jar Message.jar",
getBspOptions: os.RelPath => List[String] = _ => List.empty,
checkTestTarget: Boolean = false
): Unit = {
val jarSources = os.rel / "jarStuff"
val mainSources = os.rel / "src"
val jarPath = mainSources / "Message.jar"
val sourceJarPath = mainSources / "Message-sources.jar"
val inputs = TestInputs(
jarSources / "Message.scala" -> "case class Message(value: String)",
mainSources / "Main.scala" ->
s"""$directives
|object Main extends App {
| println(Message("Hello").value)
|}
|""".stripMargin
)
inputs.fromRoot { root =>
// package the library jar
os.proc(
TestUtil.cli,
"--power",
"package",
jarSources,
"--library",
"-o",
jarPath,
extraOptions
)
.call(cwd = root)
// package the sources jar
os.proc(
TestUtil.cli,
"--power",
"package",
jarSources,
"--source",
"-o",
sourceJarPath,
extraOptions
)
.call(cwd = root)
withBsp(
inputs,
Seq(mainSources.toString),
reuseRoot = Some(root),
bspOptions = getBspOptions(sourceJarPath)
) {
(_, _, remoteServer) =>
async {
val buildTargetsResp = await(remoteServer.workspaceBuildTargets().asScala)
val targets = buildTargetsResp
.getTargets
.asScala
val Some(mainTarget) = targets.find(!_.getId.getUri.contains("-test"))
val Some(testTarget) = targets.find(_.getId.getUri.contains("-test"))
// ensure that the project compiles
val compileRes = await(remoteServer.buildTargetCompile(
new b.CompileParams(List(mainTarget.getId).asJava)
).asScala)
expect(compileRes.getStatusCode == b.StatusCode.OK)
// ensure that the source jar is in the dependency sources
val dependencySourcesResp = await {
remoteServer
.buildTargetDependencySources(
new b.DependencySourcesParams(List(mainTarget.getId, testTarget.getId).asJava)
)
.asScala
}
val dependencySourceItems = dependencySourcesResp.getItems.asScala
val sources = dependencySourceItems
.filter(dsi =>
if (checkTestTarget) dsi.getTarget == testTarget.getId
else dsi.getTarget == mainTarget.getId
)
.flatMap(_.getSources.asScala)
expect(sources.exists(_.endsWith(sourceJarPath.last)))
}
}
}
}

test("source jars handled correctly from the command line") {
testSourceJars(getBspOptions = sourceJarPath => List("--source-jar", sourceJarPath.toString))
}

test(
"source jars handled correctly from the command line smartly assuming a *-sources.jar is a source jar"
) {
testSourceJars(getBspOptions = sourceJarPath => List("--extra-jar", sourceJarPath.toString))
}

test("source jars handled correctly from a test scope using directive") {
testSourceJars(
directives = """//> using jar Message.jar
|//> using test.sourceJar Message-sources.jar""".stripMargin,
checkTestTarget = true
)
}

private def checkIfBloopProjectIsInitialised(
root: os.Path,
buildTargetsResp: b.WorkspaceBuildTargetsResult
Expand Down
Loading