Skip to content

Commit 7ad37cc

Browse files
authored
Ensure JAVA_HOME of setup-ide is respected by bsp (#2920)
1 parent 0b7b6d7 commit 7ad37cc

File tree

9 files changed

+182
-21
lines changed

9 files changed

+182
-21
lines changed

modules/cli/src/main/scala/scala/cli/commands/bsp/Bsp.scala

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package scala.cli.commands.bsp
22

33
import caseapp.*
44
import com.github.plokhotnyuk.jsoniter_scala.core.*
5+
import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker
56

67
import scala.build.EitherCps.{either, value}
78
import scala.build.*
@@ -28,6 +29,16 @@ object Bsp extends ScalaCommand[BspOptions] {
2829
val content = os.read.bytes(os.Path(optionsPath, os.pwd))
2930
readFromArray(content)(SharedOptions.jsonCodec)
3031
}.getOrElse(options.shared)
32+
private def latestEnvsFromFile(options: BspOptions): Map[String, String] =
33+
options.envs
34+
.map(path => os.Path(path, os.pwd))
35+
.filter(path => os.exists(path) && os.isFile(path))
36+
.map { envsPath =>
37+
val content = os.read.bytes(os.Path(envsPath, os.pwd))
38+
implicit val mapCodec: JsonValueCodec[Map[String, String]] = JsonCodecMaker.make
39+
readFromArray(content)
40+
}
41+
.getOrElse(Map.empty)
3142
override def sharedOptions(options: BspOptions): Option[SharedOptions] =
3243
Option(latestSharedOptions(options))
3344

@@ -36,18 +47,20 @@ object Bsp extends ScalaCommand[BspOptions] {
3647
if (options.shared.logging.verbosity >= 3)
3748
pprint.err.log(args)
3849

39-
val getSharedOptions: () => SharedOptions = () => latestSharedOptions(options)
50+
val getSharedOptions: () => SharedOptions = () => latestSharedOptions(options)
51+
val getEnvsFromFile: () => Map[String, String] = () => latestEnvsFromFile(options)
4052

4153
val preprocessInputs: Seq[String] => Either[BuildException, (Inputs, BuildOptions)] =
4254
argsSeq =>
4355
either {
4456
val sharedOptions = getSharedOptions()
57+
val envs = getEnvsFromFile()
4558
val initialInputs = value(sharedOptions.inputs(argsSeq, () => Inputs.default()))
4659

4760
if (sharedOptions.logging.verbosity >= 3)
4861
pprint.err.log(initialInputs)
4962

50-
val baseOptions = buildOptions(sharedOptions)
63+
val baseOptions = buildOptions(sharedOptions, envs)
5164
val latestLogger = sharedOptions.logging.logger
5265
val persistentLogger = new PersistentDiagnosticLogger(latestLogger)
5366

@@ -85,10 +98,12 @@ object Bsp extends ScalaCommand[BspOptions] {
8598
* options extracted from sources
8699
*/
87100
val initialBspOptions = {
88-
val sharedOptions = getSharedOptions()
101+
val sharedOptions = getSharedOptions()
102+
val envs = getEnvsFromFile()
103+
val bspBuildOptions = buildOptions(sharedOptions, envs).orElse(finalBuildOptions)
89104
BspReloadableOptions(
90-
buildOptions = buildOptions(sharedOptions) orElse finalBuildOptions,
91-
bloopRifleConfig = sharedOptions.bloopRifleConfig(Some(finalBuildOptions))
105+
buildOptions = bspBuildOptions,
106+
bloopRifleConfig = sharedOptions.bloopRifleConfig(Some(bspBuildOptions))
92107
.orExit(sharedOptions.logger),
93108
logger = sharedOptions.logging.logger,
94109
verbosity = sharedOptions.logging.verbosity
@@ -97,11 +112,12 @@ object Bsp extends ScalaCommand[BspOptions] {
97112

98113
val bspReloadableOptionsReference = BspReloadableOptions.Reference { () =>
99114
val sharedOptions = getSharedOptions()
115+
val envs = getEnvsFromFile()
100116
val bloopRifleConfig = sharedOptions.bloopRifleConfig(Some(finalBuildOptions))
101117
.orExit(sharedOptions.logger)
102118

103119
BspReloadableOptions(
104-
buildOptions = buildOptions(sharedOptions),
120+
buildOptions = buildOptions(sharedOptions, envs),
105121
bloopRifleConfig = sharedOptions.bloopRifleConfig().orExit(sharedOptions.logger),
106122
logger = sharedOptions.logging.logger,
107123
verbosity = sharedOptions.logging.verbosity
@@ -130,10 +146,13 @@ object Bsp extends ScalaCommand[BspOptions] {
130146
}
131147
}
132148

133-
private def buildOptions(sharedOptions: SharedOptions): BuildOptions = {
149+
private def buildOptions(
150+
sharedOptions: SharedOptions,
151+
envs: Map[String, String]
152+
): BuildOptions = {
134153
val logger = sharedOptions.logger
135154
val baseOptions = sharedOptions.buildOptions().orExit(logger)
136-
baseOptions.copy(
155+
val adjustedOptions = baseOptions.copy(
137156
classPathOptions = baseOptions.classPathOptions.copy(
138157
fetchSources = baseOptions.classPathOptions.fetchSources.orElse(Some(true))
139158
),
@@ -148,5 +167,18 @@ object Bsp extends ScalaCommand[BspOptions] {
148167
baseOptions.notForBloopOptions.addRunnerDependencyOpt.orElse(Some(false))
149168
)
150169
)
170+
envs.get("JAVA_HOME")
171+
.filter(_ => adjustedOptions.javaOptions.javaHomeOpt.isEmpty)
172+
.map(javaHome =>
173+
adjustedOptions.copy(javaOptions =
174+
adjustedOptions.javaOptions.copy(javaHomeOpt =
175+
Some(Positioned(
176+
Seq(Position.Custom("ide.env.JAVA_HOME")),
177+
os.Path(javaHome, Os.pwd)
178+
))
179+
)
180+
)
181+
)
182+
.getOrElse(adjustedOptions)
151183
}
152184
}

modules/cli/src/main/scala/scala/cli/commands/bsp/BspOptions.scala

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,14 @@ final case class BspOptions(
1717
@ValueDescription("path")
1818
@Hidden
1919
@Tag(tags.implementation)
20-
jsonOptions: Option[String] = None
20+
jsonOptions: Option[String] = None,
21+
22+
@HelpMessage("Command-line options environment variables file")
23+
@ValueDescription("path")
24+
@Hidden
25+
@Tag(tags.implementation)
26+
@Name("envsFile")
27+
envs: Option[String] = None
2128
) extends HasSharedOptions {
2229
// format: on
2330
}

modules/cli/src/main/scala/scala/cli/commands/setupide/SetupIde.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package scala.cli.commands.setupide
33
import caseapp.*
44
import ch.epfl.scala.bsp4j.BspConnectionDetails
55
import com.github.plokhotnyuk.jsoniter_scala.core.*
6+
import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker
67
import com.google.gson.GsonBuilder
78

89
import java.nio.charset.{Charset, StandardCharsets}
@@ -133,6 +134,8 @@ object SetupIde extends ScalaCommand[SetupIdeOptions] {
133134
inputs.workspace / Constants.workspaceDirName / "ide-options-v2.json"
134135
val scalaCliBspInputsJsonDestination =
135136
inputs.workspace / Constants.workspaceDirName / "ide-inputs.json"
137+
val scalaCliBspEnvsJsonDestination =
138+
inputs.workspace / Constants.workspaceDirName / "ide-envs.json"
136139

137140
val inputArgs = inputs.elements.collect { case d: OnDisk => d.path.toString }
138141

@@ -151,6 +154,7 @@ object SetupIde extends ScalaCommand[SetupIdeOptions] {
151154
List(CommandUtils.getAbsolutePathToScalaCli(progName), "bsp") ++
152155
debugOpt ++
153156
List("--json-options", scalaCliBspJsonDestination.toString) ++
157+
List("--envs-file", scalaCliBspEnvsJsonDestination.toString) ++
154158
inputArgs
155159
val details = new BspConnectionDetails(
156160
bspName,
@@ -169,9 +173,12 @@ object SetupIde extends ScalaCommand[SetupIdeOptions] {
169173

170174
val gson = new GsonBuilder().setPrettyPrinting().create()
171175

176+
implicit val mapCodec: JsonValueCodec[Map[String, String]] = JsonCodecMaker.make
177+
172178
val json = gson.toJson(details)
173179
val scalaCliOptionsForBspJson = writeToArray(options.shared)(SharedOptions.jsonCodec)
174180
val scalaCliBspInputsJson = writeToArray(ideInputs)
181+
val scalaCliBspEnvsJson = writeToArray(sys.env)
175182

176183
if (inputs.workspaceOrigin.contains(WorkspaceOrigin.HomeDir))
177184
value(Left(new WorkspaceError(
@@ -192,6 +199,11 @@ object SetupIde extends ScalaCommand[SetupIdeOptions] {
192199
scalaCliBspInputsJson,
193200
createFolders = true
194201
)
202+
os.write.over(
203+
scalaCliBspEnvsJsonDestination,
204+
scalaCliBspEnvsJson,
205+
createFolders = true
206+
)
195207
logger.debug(s"Wrote $bspJsonDestination")
196208
Some(bspJsonDestination)
197209
}

modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,16 +209,20 @@ abstract class BspTestDefinitions extends ScalaCliSuite with TestScalaVersionArg
209209
val details = readBspConfig(root)
210210
val expectedIdeOptionsFile = root / Constants.workspaceDirName / "ide-options-v2.json"
211211
val expectedIdeInputsFile = root / Constants.workspaceDirName / "ide-inputs.json"
212+
val expectedIdeEnvsFile = root / Constants.workspaceDirName / "ide-envs.json"
212213
val expectedArgv = Seq(
213214
TestUtil.cliPath,
214215
"bsp",
215216
"--json-options",
216217
expectedIdeOptionsFile.toString,
218+
"--envs-file",
219+
expectedIdeEnvsFile.toString,
217220
root.toString
218221
)
219222
expect(details.argv == expectedArgv)
220223
expect(os.isFile(expectedIdeOptionsFile))
221224
expect(os.isFile(expectedIdeInputsFile))
225+
expect(os.isFile(expectedIdeEnvsFile))
222226
}
223227
}
224228

@@ -253,6 +257,8 @@ abstract class BspTestDefinitions extends ScalaCliSuite with TestScalaVersionArg
253257
"bsp",
254258
"--json-options",
255259
(root / "directory" / Constants.workspaceDirName / "ide-options-v2.json").toString,
260+
"--envs-file",
261+
(root / "directory" / Constants.workspaceDirName / "ide-envs.json").toString,
256262
(root / "directory" / "simple.sc").toString
257263
)
258264
expect(details.argv == expectedArgv)
@@ -2020,6 +2026,79 @@ abstract class BspTestDefinitions extends ScalaCliSuite with TestScalaVersionArg
20202026
}
20212027
}
20222028

2029+
test("BSP respects JAVA_HOME") {
2030+
val javaVersion = "22"
2031+
val inputs = TestInputs(os.rel / "check-java.sc" ->
2032+
s"""assert(System.getProperty("java.version").startsWith("$javaVersion"))
2033+
|println(System.getProperty("java.home"))""".stripMargin)
2034+
inputs.fromRoot { root =>
2035+
os.proc(TestUtil.cli, "bloop", "exit", "--power").call(cwd = root)
2036+
val java22Home =
2037+
os.Path(
2038+
os.proc(TestUtil.cs, "java-home", "--jvm", s"zulu:$javaVersion").call().out.trim(),
2039+
os.pwd
2040+
)
2041+
os.proc(TestUtil.cli, "setup-ide", "check-java.sc")
2042+
.call(cwd = root, env = Map("JAVA_HOME" -> java22Home.toString()))
2043+
val ideOptionsPath = root / Constants.workspaceDirName / "ide-options-v2.json"
2044+
expect(ideOptionsPath.toNIO.toFile.exists())
2045+
val ideEnvsPath = root / Constants.workspaceDirName / "ide-envs.json"
2046+
expect(ideEnvsPath.toNIO.toFile.exists())
2047+
val jsonOptions = List("--json-options", ideOptionsPath.toString)
2048+
val envOptions = List("--envs-file", ideEnvsPath.toString)
2049+
withBsp(inputs, Seq("."), bspOptions = jsonOptions ++ envOptions, reuseRoot = Some(root)) {
2050+
(_, _, remoteServer) =>
2051+
async {
2052+
val targets = await(remoteServer.workspaceBuildTargets().asScala)
2053+
.getTargets.asScala
2054+
.filter(!_.getId.getUri.contains("-test"))
2055+
.map(_.getId())
2056+
val compileResult =
2057+
await(remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala)
2058+
expect(compileResult.getStatusCode == b.StatusCode.OK)
2059+
val runResult =
2060+
await(remoteServer.buildTargetRun(new b.RunParams(targets.head)).asScala)
2061+
expect(runResult.getStatusCode == b.StatusCode.OK)
2062+
}
2063+
}
2064+
}
2065+
}
2066+
2067+
test("BSP respects --java-home") {
2068+
val javaVersion = "22"
2069+
val inputs = TestInputs(os.rel / "check-java.sc" ->
2070+
s"""assert(System.getProperty("java.version").startsWith("$javaVersion"))
2071+
|println(System.getProperty("java.home"))""".stripMargin)
2072+
inputs.fromRoot { root =>
2073+
os.proc(TestUtil.cli, "bloop", "exit", "--power").call(cwd = root)
2074+
val java22Home =
2075+
os.Path(
2076+
os.proc(TestUtil.cs, "java-home", "--jvm", s"zulu:$javaVersion").call().out.trim(),
2077+
os.pwd
2078+
)
2079+
os.proc(TestUtil.cli, "setup-ide", "check-java.sc", "--java-home", java22Home.toString())
2080+
.call(cwd = root)
2081+
val ideOptionsPath = root / Constants.workspaceDirName / "ide-options-v2.json"
2082+
expect(ideOptionsPath.toNIO.toFile.exists())
2083+
val jsonOptions = List("--json-options", ideOptionsPath.toString)
2084+
withBsp(inputs, Seq("."), bspOptions = jsonOptions, reuseRoot = Some(root)) {
2085+
(_, _, remoteServer) =>
2086+
async {
2087+
val targets = await(remoteServer.workspaceBuildTargets().asScala)
2088+
.getTargets.asScala
2089+
.filter(!_.getId.getUri.contains("-test"))
2090+
.map(_.getId())
2091+
val compileResult =
2092+
await(remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala)
2093+
expect(compileResult.getStatusCode == b.StatusCode.OK)
2094+
val runResult =
2095+
await(remoteServer.buildTargetRun(new b.RunParams(targets.head)).asScala)
2096+
expect(runResult.getStatusCode == b.StatusCode.OK)
2097+
}
2098+
}
2099+
}
2100+
}
2101+
20232102
private def checkIfBloopProjectIsInitialised(
20242103
root: os.Path,
20252104
buildTargetsResp: b.WorkspaceBuildTargetsResult

modules/integration/src/test/scala/scala/cli/integration/RunScalacCompatTestDefinitions.scala

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -377,18 +377,28 @@ trait RunScalacCompatTestDefinitions {
377377
}
378378
}
379379

380+
override def munitFlakyOK: Boolean = TestUtil.isCI && Properties.isMac && TestUtil.isNativeCli
381+
382+
def verifyScala212VersionCompiles(scalaPatchVersion: String): Unit = {
383+
TestInputs(os.rel / "s.sc" -> "println(util.Properties.versionNumberString)").fromRoot {
384+
root =>
385+
val scala212VersionString = s"2.12.$scalaPatchVersion"
386+
val res =
387+
os.proc(TestUtil.cli, "run", ".", "-S", scala212VersionString, TestUtil.extraOptions)
388+
.call(cwd = root)
389+
expect(res.out.trim() == scala212VersionString)
390+
}
391+
}
392+
380393
if (actualScalaVersion.startsWith("2.12."))
381-
test("verify that Scala version 2.12.x < 2.12.4 is respected and compiles correctly") {
382-
TestInputs(os.rel / "s.sc" -> "println(util.Properties.versionNumberString)").fromRoot {
383-
root =>
384-
(1 until 4).foreach { scalaPatchVersion =>
385-
val scala212VersionString = s"2.12.$scalaPatchVersion"
386-
val res =
387-
os.proc(TestUtil.cli, "run", ".", "-S", scala212VersionString, TestUtil.extraOptions)
388-
.call(cwd = root)
389-
expect(res.out.trim() == scala212VersionString)
390-
}
391-
}
394+
(1 until 4).map(_.toString).foreach { scalaPatchVersion =>
395+
// FIXME this shouldn't be flaky on Mac
396+
if (TestUtil.isCI && Properties.isMac && TestUtil.isNativeCli) test(
397+
s"verify that Scala version 2.12.$scalaPatchVersion is respected and compiles correctly".flaky
398+
)(verifyScala212VersionCompiles(scalaPatchVersion))
399+
else test(
400+
s"verify that Scala version 2.12.$scalaPatchVersion is respected and compiles correctly"
401+
)(verifyScala212VersionCompiles(scalaPatchVersion))
392402
}
393403

394404
test("scalac verbose") {

modules/integration/src/test/scala/scala/cli/integration/SharedRunTests.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class SharedRunTests extends ScalaCliSuite {
5151
}
5252

5353
test("Scala version in config file") {
54-
val confSv = "2.13.1"
54+
val confSv = "2.13.13"
5555
val inputs = TestInputs(
5656
os.rel / "test.sc" ->
5757
s"""//> using scala "$confSv"

website/docs/reference/cli-options.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1879,6 +1879,13 @@ Available in commands:
18791879
[Internal]
18801880
Command-line options JSON file
18811881

1882+
### `--envs`
1883+
1884+
Aliases: `--envs-file`
1885+
1886+
[Internal]
1887+
Command-line options environment variables file
1888+
18821889
### Bsp file options
18831890

18841891
Available in commands:

website/docs/reference/scala-command/cli-options.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1355,6 +1355,14 @@ Available in commands:
13551355

13561356
Command-line options JSON file
13571357

1358+
### `--envs`
1359+
1360+
Aliases: `--envs-file`
1361+
1362+
`IMPLEMENTATION specific` per Scala Runner specification
1363+
1364+
Command-line options environment variables file
1365+
13581366
### Bsp file options
13591367

13601368
Available in commands:

website/docs/reference/scala-command/runner-specification.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5008,6 +5008,12 @@ Exclude sources
50085008

50095009
Command-line options JSON file
50105010

5011+
**--envs**
5012+
5013+
Command-line options environment variables file
5014+
5015+
Aliases: `--envs-file`
5016+
50115017
</details>
50125018

50135019
---

0 commit comments

Comments
 (0)