Skip to content

Commit a962834

Browse files
authored
Ensure BSP respects --power mode (#2997)
1 parent 02b94f7 commit a962834

File tree

4 files changed

+221
-4
lines changed

4 files changed

+221
-4
lines changed

modules/cli/src/main/scala/scala/cli/ScalaCli.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ object ScalaCli {
4949
val isPower = isPowerEnv.orElse(isPowerConfigDb).getOrElse(false)
5050
!isPower
5151
}
52-
def allowRestrictedFeatures = !isSipScala
52+
def setPowerMode(power: Boolean): Unit = isSipScala = !power
53+
def allowRestrictedFeatures = !isSipScala
5354
def fullRunnerName =
5455
if (progName.contains(scalaCliBinaryName)) "Scala CLI" else "Scala code runner"
5556
def baseRunnerName = if (progName.contains(scalaCliBinaryName)) scalaCliBinaryName else "scala"

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ import scala.build.bsp.{BspReloadableOptions, BspThreads}
1010
import scala.build.errors.BuildException
1111
import scala.build.input.Inputs
1212
import scala.build.options.{BuildOptions, Scope}
13-
import scala.cli.CurrentParams
1413
import scala.cli.commands.ScalaCommand
1514
import scala.cli.commands.publish.ConfigUtil.*
1615
import scala.cli.commands.shared.SharedOptions
1716
import scala.cli.config.{ConfigDb, Keys}
1817
import scala.cli.launcher.LauncherOptions
18+
import scala.cli.util.ConfigDbUtils
19+
import scala.cli.{CurrentParams, ScalaCli}
1920
import scala.concurrent.Await
2021
import scala.concurrent.duration.Duration
2122

@@ -30,6 +31,7 @@ object Bsp extends ScalaCommand[BspOptions] {
3031
val content = os.read.bytes(os.Path(optionsPath, os.pwd))
3132
readFromArray(content)(SharedOptions.jsonCodec)
3233
}.getOrElse(options.shared)
34+
3335
private def latestLauncherOptions(options: BspOptions): LauncherOptions =
3436
options.jsonLauncherOptions
3537
.map(path => os.Path(path, os.pwd))
@@ -51,6 +53,24 @@ object Bsp extends ScalaCommand[BspOptions] {
5153
override def sharedOptions(options: BspOptions): Option[SharedOptions] =
5254
Option(latestSharedOptions(options))
5355

56+
private def refreshPowerMode(
57+
latestLauncherOptions: LauncherOptions,
58+
latestSharedOptions: SharedOptions,
59+
latestEnvs: Map[String, String]
60+
): Unit = {
61+
val previousPowerMode = ScalaCli.allowRestrictedFeatures
62+
val configPowerMode = ConfigDbUtils.getLatestConfigDbOpt(latestSharedOptions.logger)
63+
.flatMap(_.get(Keys.power).toOption)
64+
.flatten
65+
.getOrElse(false)
66+
val envPowerMode = latestEnvs.get("SCALA_CLI_POWER").exists(_.toBoolean)
67+
val launcherPowerArg = latestLauncherOptions.powerOptions.power
68+
val subCommandPowerArg = latestSharedOptions.powerOptions.power
69+
val latestPowerMode = configPowerMode || launcherPowerArg || subCommandPowerArg || envPowerMode
70+
// only set power mode if it's been turned on since, never turn it off in BSP
71+
if !previousPowerMode && latestPowerMode then ScalaCli.setPowerMode(latestPowerMode)
72+
}
73+
5474
// not reusing buildOptions here, since they should be reloaded live instead
5575
override def runCommand(options: BspOptions, args: RemainingArgs, logger: Logger): Unit = {
5676
if (options.shared.logging.verbosity >= 3)
@@ -60,6 +80,8 @@ object Bsp extends ScalaCommand[BspOptions] {
6080
val getLauncherOptions: () => LauncherOptions = () => latestLauncherOptions(options)
6181
val getEnvsFromFile: () => Map[String, String] = () => latestEnvsFromFile(options)
6282

83+
refreshPowerMode(getLauncherOptions(), getSharedOptions(), getEnvsFromFile())
84+
6385
val preprocessInputs: Seq[String] => Either[BuildException, (Inputs, BuildOptions)] =
6486
argsSeq =>
6587
either {
@@ -68,6 +90,8 @@ object Bsp extends ScalaCommand[BspOptions] {
6890
val envs = getEnvsFromFile()
6991
val initialInputs = value(sharedOptions.inputs(argsSeq, () => Inputs.default()))
7092

93+
refreshPowerMode(launcherOptions, sharedOptions, envs)
94+
7195
if (sharedOptions.logging.verbosity >= 3)
7296
pprint.err.log(initialInputs)
7397

@@ -114,6 +138,7 @@ object Bsp extends ScalaCommand[BspOptions] {
114138
val envs = getEnvsFromFile()
115139
val bspBuildOptions = buildOptions(sharedOptions, launcherOptions, envs)
116140
.orElse(finalBuildOptions)
141+
refreshPowerMode(launcherOptions, sharedOptions, envs)
117142
BspReloadableOptions(
118143
buildOptions = bspBuildOptions,
119144
bloopRifleConfig = sharedOptions.bloopRifleConfig(Some(bspBuildOptions))
@@ -129,6 +154,7 @@ object Bsp extends ScalaCommand[BspOptions] {
129154
val envs = getEnvsFromFile()
130155
val bloopRifleConfig = sharedOptions.bloopRifleConfig(Some(finalBuildOptions))
131156
.orExit(sharedOptions.logger)
157+
refreshPowerMode(launcherOptions, sharedOptions, envs)
132158

133159
BspReloadableOptions(
134160
buildOptions = buildOptions(sharedOptions, launcherOptions, envs),

modules/cli/src/main/scala/scala/cli/util/ConfigDbUtils.scala

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import scala.cli.commands.publish.ConfigUtil.wrapConfigException
66
import scala.cli.config.{ConfigDb, Key}
77

88
object ConfigDbUtils {
9-
lazy val configDb: Either[ConfigDbException, ConfigDb] =
9+
private def getLatestConfigDb: Either[ConfigDbException, ConfigDb] =
1010
ConfigDb.open(Directories.directories.dbPath.toNIO).wrapConfigException
1111

12+
lazy val configDb: Either[ConfigDbException, ConfigDb] = getLatestConfigDb
13+
1214
extension [T](either: Either[Exception, T]) {
1315
private def handleConfigDbException(f: BuildException => Unit): Option[T] =
1416
either match
@@ -24,6 +26,9 @@ object ConfigDbUtils {
2426
def getConfigDbOpt(logger: Logger): Option[ConfigDb] =
2527
configDb.handleConfigDbException(logger.debug)
2628

29+
def getLatestConfigDbOpt(logger: Logger): Option[ConfigDb] =
30+
getLatestConfigDb.handleConfigDbException(logger.debug)
31+
2732
extension (db: ConfigDb) {
2833
def getOpt[T](configDbKey: Key[T], f: BuildException => Unit): Option[T] =
2934
db.get(configDbKey).handleConfigDbException(f).flatten

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

Lines changed: 186 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ abstract class BspTestDefinitions extends ScalaCliSuite with TestScalaVersionArg
7878
attempts: Int = if (TestUtil.isCI) 3 else 1,
7979
pauseDuration: FiniteDuration = 5.seconds,
8080
bspOptions: List[String] = List.empty,
81+
bspEnvs: Map[String, String] = Map.empty,
8182
reuseRoot: Option[os.Path] = None,
8283
stdErrOpt: Option[os.RelPath] = None,
8384
extraOptionsOverride: Seq[String] = extraOptions
@@ -96,7 +97,7 @@ abstract class BspTestDefinitions extends ScalaCliSuite with TestScalaVersionArg
9697
val stderr: os.ProcessOutput = stdErrPathOpt.getOrElse(os.Inherit)
9798

9899
val proc = os.proc(TestUtil.cli, "bsp", bspOptions ++ extraOptionsOverride, args)
99-
.spawn(cwd = root, stderr = stderr)
100+
.spawn(cwd = root, stderr = stderr, env = bspEnvs)
100101
var remoteServer: b.BuildServer & b.ScalaBuildServer & b.JavaBuildServer & b.JvmBuildServer =
101102
null
102103

@@ -2114,6 +2115,190 @@ abstract class BspTestDefinitions extends ScalaCliSuite with TestScalaVersionArg
21142115
}
21152116
}
21162117

2118+
for {
2119+
setPowerByLauncherOpt <- Seq(true, false)
2120+
setPowerBySubCommandOpt <- Seq(true, false)
2121+
setPowerByEnv <- Seq(true, false)
2122+
setPowerByConfig <- Seq(true, false)
2123+
powerIsSet =
2124+
setPowerByLauncherOpt || setPowerBySubCommandOpt || setPowerByEnv || setPowerByConfig
2125+
powerSettingDescription = {
2126+
val launcherSetting = if (setPowerByLauncherOpt) "launcher option" else ""
2127+
val subCommandSetting = if (setPowerBySubCommandOpt) "setup-ide option" else ""
2128+
val envSetting = if (setPowerByEnv) "environment variable" else ""
2129+
val configSetting = if (setPowerByConfig) "config" else ""
2130+
List(launcherSetting, subCommandSetting, envSetting, configSetting)
2131+
.filter(_.nonEmpty)
2132+
.mkString(", ")
2133+
}
2134+
testDescription =
2135+
if (powerIsSet)
2136+
s"BSP respects --power mode set by $powerSettingDescription (example: using python directive)"
2137+
else
2138+
"BSP fails when --power mode is not set for experimental directives (example: using python directive)"
2139+
} test(testDescription) {
2140+
val scriptName = "requires-power.sc"
2141+
val inputs = TestInputs(os.rel / scriptName ->
2142+
s"""//> using python
2143+
|println("scalapy is experimental")""".stripMargin)
2144+
inputs.fromRoot { root =>
2145+
val configFile = os.rel / "config" / "config.json"
2146+
val configEnvs = Map("SCALA_CLI_CONFIG" -> configFile.toString())
2147+
val setupIdeEnvs: Map[String, String] =
2148+
if (setPowerByEnv) Map("SCALA_CLI_POWER" -> "true") ++ configEnvs
2149+
else configEnvs
2150+
val launcherOpts =
2151+
if (setPowerByLauncherOpt) List("--power")
2152+
else List.empty
2153+
val subCommandOpts =
2154+
if (setPowerBySubCommandOpt) List("--power")
2155+
else List.empty
2156+
val args = launcherOpts ++ List("setup-ide", scriptName) ++ subCommandOpts
2157+
os.proc(TestUtil.cli, args).call(cwd = root, env = setupIdeEnvs)
2158+
if (setPowerByConfig)
2159+
os.proc(TestUtil.cli, "config", "power", "true")
2160+
.call(cwd = root, env = configEnvs)
2161+
val ideOptionsPath = root / Constants.workspaceDirName / "ide-options-v2.json"
2162+
expect(ideOptionsPath.toNIO.toFile.exists())
2163+
val ideLauncherOptsPath = root / Constants.workspaceDirName / "ide-launcher-options.json"
2164+
expect(ideLauncherOptsPath.toNIO.toFile.exists())
2165+
val ideEnvsPath = root / Constants.workspaceDirName / "ide-envs.json"
2166+
expect(ideEnvsPath.toNIO.toFile.exists())
2167+
val jsonOptions = List(
2168+
"--json-options",
2169+
ideOptionsPath.toString,
2170+
"--json-launcher-options",
2171+
ideLauncherOptsPath.toString,
2172+
"--envs-file",
2173+
ideEnvsPath.toString
2174+
)
2175+
withBsp(
2176+
inputs,
2177+
Seq("."),
2178+
bspOptions = jsonOptions,
2179+
bspEnvs = configEnvs,
2180+
reuseRoot = Some(root)
2181+
) {
2182+
(_, _, remoteServer) =>
2183+
async {
2184+
val targets = await(remoteServer.workspaceBuildTargets().asScala)
2185+
.getTargets.asScala
2186+
.filter(!_.getId.getUri.contains("-test"))
2187+
.map(_.getId())
2188+
val compileResult =
2189+
await(remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala)
2190+
if (powerIsSet) {
2191+
expect(compileResult.getStatusCode == b.StatusCode.OK)
2192+
val runResult =
2193+
await(remoteServer.buildTargetRun(new b.RunParams(targets.head)).asScala)
2194+
expect(runResult.getStatusCode == b.StatusCode.OK)
2195+
}
2196+
else
2197+
expect(compileResult.getStatusCode == b.StatusCode.ERROR)
2198+
}
2199+
}
2200+
}
2201+
}
2202+
2203+
test("BSP reloads --power mode after setting it via env passed to setup-ide") {
2204+
val scriptName = "requires-power.sc"
2205+
val inputs = TestInputs(os.rel / scriptName ->
2206+
s"""//> using python
2207+
|println("scalapy is experimental")""".stripMargin)
2208+
inputs.fromRoot { root =>
2209+
os.proc(TestUtil.cli, "setup-ide", scriptName, extraOptions).call(cwd = root)
2210+
val ideEnvsPath = root / Constants.workspaceDirName / "ide-envs.json"
2211+
expect(ideEnvsPath.toNIO.toFile.exists())
2212+
val jsonOptions = List("--envs-file", ideEnvsPath.toString)
2213+
withBsp(inputs, Seq(scriptName), bspOptions = jsonOptions, reuseRoot = Some(root)) {
2214+
(_, _, remoteServer) =>
2215+
async {
2216+
val targets = await(remoteServer.workspaceBuildTargets().asScala)
2217+
.getTargets.asScala
2218+
.filter(!_.getId.getUri.contains("-test"))
2219+
.map(_.getId())
2220+
2221+
// compilation should fail before reload, as --power mode is off
2222+
val compileBeforeReloadResult =
2223+
await(remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala)
2224+
expect(compileBeforeReloadResult.getStatusCode == b.StatusCode.ERROR)
2225+
2226+
// enable --power mode via env for setup-ide
2227+
os.proc(TestUtil.cli, "setup-ide", scriptName, extraOptions)
2228+
.call(cwd = root, env = Map("SCALA_CLI_POWER" -> "true"))
2229+
2230+
// compilation should now succeed
2231+
val reloadResponse =
2232+
extractWorkspaceReloadResponse(await(remoteServer.workspaceReload().asScala))
2233+
expect(reloadResponse.isEmpty)
2234+
val compileAfterReloadResult =
2235+
await(remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala)
2236+
expect(compileAfterReloadResult.getStatusCode == b.StatusCode.OK)
2237+
2238+
// code should also be runnable via BSP now
2239+
val runResult =
2240+
await(remoteServer.buildTargetRun(new b.RunParams(targets.head)).asScala)
2241+
expect(runResult.getStatusCode == b.StatusCode.OK)
2242+
}
2243+
}
2244+
}
2245+
}
2246+
2247+
test("BSP reloads --power mode after setting it via config") {
2248+
val scriptName = "requires-power.sc"
2249+
val inputs = TestInputs(os.rel / scriptName ->
2250+
s"""//> using python
2251+
|println("scalapy is experimental")""".stripMargin)
2252+
inputs.fromRoot { root =>
2253+
val configFile = os.rel / "config" / "config.json"
2254+
val configEnvs = Map("SCALA_CLI_CONFIG" -> configFile.toString())
2255+
os.proc(TestUtil.cli, "setup-ide", scriptName, extraOptions).call(
2256+
cwd = root,
2257+
env = configEnvs
2258+
)
2259+
val ideEnvsPath = root / Constants.workspaceDirName / "ide-envs.json"
2260+
expect(ideEnvsPath.toNIO.toFile.exists())
2261+
val jsonOptions = List("--envs-file", ideEnvsPath.toString)
2262+
withBsp(
2263+
inputs,
2264+
Seq(scriptName),
2265+
bspOptions = jsonOptions,
2266+
bspEnvs = configEnvs,
2267+
reuseRoot = Some(root)
2268+
) {
2269+
(_, _, remoteServer) =>
2270+
async {
2271+
val targets = await(remoteServer.workspaceBuildTargets().asScala)
2272+
.getTargets.asScala
2273+
.filter(!_.getId.getUri.contains("-test"))
2274+
.map(_.getId())
2275+
2276+
// compilation should fail before reload, as --power mode is off
2277+
val compileBeforeReloadResult =
2278+
await(remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala)
2279+
expect(compileBeforeReloadResult.getStatusCode == b.StatusCode.ERROR)
2280+
2281+
// enable --power mode via config
2282+
os.proc(TestUtil.cli, "config", "power", "true")
2283+
.call(cwd = root, env = configEnvs)
2284+
2285+
// compilation should now succeed
2286+
val reloadResponse =
2287+
extractWorkspaceReloadResponse(await(remoteServer.workspaceReload().asScala))
2288+
expect(reloadResponse.isEmpty)
2289+
val compileAfterReloadResult =
2290+
await(remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala)
2291+
expect(compileAfterReloadResult.getStatusCode == b.StatusCode.OK)
2292+
2293+
// code should also be runnable via BSP now
2294+
val runResult =
2295+
await(remoteServer.buildTargetRun(new b.RunParams(targets.head)).asScala)
2296+
expect(runResult.getStatusCode == b.StatusCode.OK)
2297+
}
2298+
}
2299+
}
2300+
}
2301+
21172302
private def checkIfBloopProjectIsInitialised(
21182303
root: os.Path,
21192304
buildTargetsResp: b.WorkspaceBuildTargetsResult

0 commit comments

Comments
 (0)