Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure BSP respects --power mode #2997

Merged
merged 1 commit into from
Jul 4, 2024
Merged
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
3 changes: 2 additions & 1 deletion modules/cli/src/main/scala/scala/cli/ScalaCli.scala
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ object ScalaCli {
val isPower = isPowerEnv.orElse(isPowerConfigDb).getOrElse(false)
!isPower
}
def allowRestrictedFeatures = !isSipScala
def setPowerMode(power: Boolean): Unit = isSipScala = !power
def allowRestrictedFeatures = !isSipScala
def fullRunnerName =
if (progName.contains(scalaCliBinaryName)) "Scala CLI" else "Scala code runner"
def baseRunnerName = if (progName.contains(scalaCliBinaryName)) scalaCliBinaryName else "scala"
Expand Down
28 changes: 27 additions & 1 deletion modules/cli/src/main/scala/scala/cli/commands/bsp/Bsp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import scala.build.bsp.{BspReloadableOptions, BspThreads}
import scala.build.errors.BuildException
import scala.build.input.Inputs
import scala.build.options.{BuildOptions, Scope}
import scala.cli.CurrentParams
import scala.cli.commands.ScalaCommand
import scala.cli.commands.publish.ConfigUtil.*
import scala.cli.commands.shared.SharedOptions
import scala.cli.config.{ConfigDb, Keys}
import scala.cli.launcher.LauncherOptions
import scala.cli.util.ConfigDbUtils
import scala.cli.{CurrentParams, ScalaCli}
import scala.concurrent.Await
import scala.concurrent.duration.Duration

Expand All @@ -30,6 +31,7 @@ object Bsp extends ScalaCommand[BspOptions] {
val content = os.read.bytes(os.Path(optionsPath, os.pwd))
readFromArray(content)(SharedOptions.jsonCodec)
}.getOrElse(options.shared)

private def latestLauncherOptions(options: BspOptions): LauncherOptions =
options.jsonLauncherOptions
.map(path => os.Path(path, os.pwd))
Expand All @@ -51,6 +53,24 @@ object Bsp extends ScalaCommand[BspOptions] {
override def sharedOptions(options: BspOptions): Option[SharedOptions] =
Option(latestSharedOptions(options))

private def refreshPowerMode(
latestLauncherOptions: LauncherOptions,
latestSharedOptions: SharedOptions,
latestEnvs: Map[String, String]
): Unit = {
val previousPowerMode = ScalaCli.allowRestrictedFeatures
val configPowerMode = ConfigDbUtils.getLatestConfigDbOpt(latestSharedOptions.logger)
.flatMap(_.get(Keys.power).toOption)
.flatten
.getOrElse(false)
Gedochao marked this conversation as resolved.
Show resolved Hide resolved
val envPowerMode = latestEnvs.get("SCALA_CLI_POWER").exists(_.toBoolean)
val launcherPowerArg = latestLauncherOptions.powerOptions.power
val subCommandPowerArg = latestSharedOptions.powerOptions.power
val latestPowerMode = configPowerMode || launcherPowerArg || subCommandPowerArg || envPowerMode
// only set power mode if it's been turned on since, never turn it off in BSP
if !previousPowerMode && latestPowerMode then ScalaCli.setPowerMode(latestPowerMode)
}

// not reusing buildOptions here, since they should be reloaded live instead
override def runCommand(options: BspOptions, args: RemainingArgs, logger: Logger): Unit = {
if (options.shared.logging.verbosity >= 3)
Expand All @@ -60,6 +80,8 @@ object Bsp extends ScalaCommand[BspOptions] {
val getLauncherOptions: () => LauncherOptions = () => latestLauncherOptions(options)
val getEnvsFromFile: () => Map[String, String] = () => latestEnvsFromFile(options)

refreshPowerMode(getLauncherOptions(), getSharedOptions(), getEnvsFromFile())

val preprocessInputs: Seq[String] => Either[BuildException, (Inputs, BuildOptions)] =
argsSeq =>
either {
Expand All @@ -68,6 +90,8 @@ object Bsp extends ScalaCommand[BspOptions] {
val envs = getEnvsFromFile()
val initialInputs = value(sharedOptions.inputs(argsSeq, () => Inputs.default()))

refreshPowerMode(launcherOptions, sharedOptions, envs)

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

Expand Down Expand Up @@ -114,6 +138,7 @@ object Bsp extends ScalaCommand[BspOptions] {
val envs = getEnvsFromFile()
val bspBuildOptions = buildOptions(sharedOptions, launcherOptions, envs)
.orElse(finalBuildOptions)
refreshPowerMode(launcherOptions, sharedOptions, envs)
Gedochao marked this conversation as resolved.
Show resolved Hide resolved
BspReloadableOptions(
buildOptions = bspBuildOptions,
bloopRifleConfig = sharedOptions.bloopRifleConfig(Some(bspBuildOptions))
Expand All @@ -129,6 +154,7 @@ object Bsp extends ScalaCommand[BspOptions] {
val envs = getEnvsFromFile()
val bloopRifleConfig = sharedOptions.bloopRifleConfig(Some(finalBuildOptions))
.orExit(sharedOptions.logger)
refreshPowerMode(launcherOptions, sharedOptions, envs)

BspReloadableOptions(
buildOptions = buildOptions(sharedOptions, launcherOptions, envs),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import scala.cli.commands.publish.ConfigUtil.wrapConfigException
import scala.cli.config.{ConfigDb, Key}

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

lazy val configDb: Either[ConfigDbException, ConfigDb] = getLatestConfigDb

extension [T](either: Either[Exception, T]) {
private def handleConfigDbException(f: BuildException => Unit): Option[T] =
either match
Expand All @@ -24,6 +26,9 @@ object ConfigDbUtils {
def getConfigDbOpt(logger: Logger): Option[ConfigDb] =
configDb.handleConfigDbException(logger.debug)

def getLatestConfigDbOpt(logger: Logger): Option[ConfigDb] =
getLatestConfigDb.handleConfigDbException(logger.debug)

extension (db: ConfigDb) {
def getOpt[T](configDbKey: Key[T], f: BuildException => Unit): Option[T] =
db.get(configDbKey).handleConfigDbException(f).flatten
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ abstract class BspTestDefinitions extends ScalaCliSuite with TestScalaVersionArg
attempts: Int = if (TestUtil.isCI) 3 else 1,
pauseDuration: FiniteDuration = 5.seconds,
bspOptions: List[String] = List.empty,
bspEnvs: Map[String, String] = Map.empty,
reuseRoot: Option[os.Path] = None,
stdErrOpt: Option[os.RelPath] = None,
extraOptionsOverride: Seq[String] = extraOptions
Expand All @@ -96,7 +97,7 @@ abstract class BspTestDefinitions extends ScalaCliSuite with TestScalaVersionArg
val stderr: os.ProcessOutput = stdErrPathOpt.getOrElse(os.Inherit)

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

Expand Down Expand Up @@ -2107,6 +2108,190 @@ abstract class BspTestDefinitions extends ScalaCliSuite with TestScalaVersionArg
}
}

for {
setPowerByLauncherOpt <- Seq(true, false)
setPowerBySubCommandOpt <- Seq(true, false)
setPowerByEnv <- Seq(true, false)
setPowerByConfig <- Seq(true, false)
powerIsSet =
setPowerByLauncherOpt || setPowerBySubCommandOpt || setPowerByEnv || setPowerByConfig
powerSettingDescription = {
val launcherSetting = if (setPowerByLauncherOpt) "launcher option" else ""
val subCommandSetting = if (setPowerBySubCommandOpt) "setup-ide option" else ""
val envSetting = if (setPowerByEnv) "environment variable" else ""
val configSetting = if (setPowerByConfig) "config" else ""
List(launcherSetting, subCommandSetting, envSetting, configSetting)
.filter(_.nonEmpty)
.mkString(", ")
}
testDescription =
if (powerIsSet)
s"BSP respects --power mode set by $powerSettingDescription (example: using python directive)"
else
"BSP fails when --power mode is not set for experimental directives (example: using python directive)"
} test(testDescription) {
val scriptName = "requires-power.sc"
val inputs = TestInputs(os.rel / scriptName ->
s"""//> using python
|println("scalapy is experimental")""".stripMargin)
inputs.fromRoot { root =>
val configFile = os.rel / "config" / "config.json"
val configEnvs = Map("SCALA_CLI_CONFIG" -> configFile.toString())
val setupIdeEnvs: Map[String, String] =
if (setPowerByEnv) Map("SCALA_CLI_POWER" -> "true") ++ configEnvs
else configEnvs
val launcherOpts =
if (setPowerByLauncherOpt) List("--power")
else List.empty
val subCommandOpts =
if (setPowerBySubCommandOpt) List("--power")
else List.empty
val args = launcherOpts ++ List("setup-ide", scriptName) ++ subCommandOpts
os.proc(TestUtil.cli, args).call(cwd = root, env = setupIdeEnvs)
if (setPowerByConfig)
os.proc(TestUtil.cli, "config", "power", "true")
.call(cwd = root, env = configEnvs)
val ideOptionsPath = root / Constants.workspaceDirName / "ide-options-v2.json"
expect(ideOptionsPath.toNIO.toFile.exists())
val ideLauncherOptsPath = root / Constants.workspaceDirName / "ide-launcher-options.json"
expect(ideLauncherOptsPath.toNIO.toFile.exists())
val ideEnvsPath = root / Constants.workspaceDirName / "ide-envs.json"
expect(ideEnvsPath.toNIO.toFile.exists())
val jsonOptions = List(
"--json-options",
ideOptionsPath.toString,
"--json-launcher-options",
ideLauncherOptsPath.toString,
"--envs-file",
ideEnvsPath.toString
)
withBsp(
inputs,
Seq("."),
bspOptions = jsonOptions,
bspEnvs = configEnvs,
reuseRoot = Some(root)
) {
(_, _, remoteServer) =>
async {
val targets = await(remoteServer.workspaceBuildTargets().asScala)
.getTargets.asScala
.filter(!_.getId.getUri.contains("-test"))
.map(_.getId())
val compileResult =
await(remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala)
if (powerIsSet) {
expect(compileResult.getStatusCode == b.StatusCode.OK)
val runResult =
await(remoteServer.buildTargetRun(new b.RunParams(targets.head)).asScala)
expect(runResult.getStatusCode == b.StatusCode.OK)
}
else
expect(compileResult.getStatusCode == b.StatusCode.ERROR)
}
}
}
}

test("BSP reloads --power mode after setting it via env passed to setup-ide") {
val scriptName = "requires-power.sc"
val inputs = TestInputs(os.rel / scriptName ->
s"""//> using python
|println("scalapy is experimental")""".stripMargin)
inputs.fromRoot { root =>
os.proc(TestUtil.cli, "setup-ide", scriptName, extraOptions).call(cwd = root)
val ideEnvsPath = root / Constants.workspaceDirName / "ide-envs.json"
expect(ideEnvsPath.toNIO.toFile.exists())
val jsonOptions = List("--envs-file", ideEnvsPath.toString)
withBsp(inputs, Seq(scriptName), bspOptions = jsonOptions, reuseRoot = Some(root)) {
(_, _, remoteServer) =>
async {
val targets = await(remoteServer.workspaceBuildTargets().asScala)
.getTargets.asScala
.filter(!_.getId.getUri.contains("-test"))
.map(_.getId())

// compilation should fail before reload, as --power mode is off
val compileBeforeReloadResult =
await(remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala)
expect(compileBeforeReloadResult.getStatusCode == b.StatusCode.ERROR)

// enable --power mode via env for setup-ide
os.proc(TestUtil.cli, "setup-ide", scriptName, extraOptions)
.call(cwd = root, env = Map("SCALA_CLI_POWER" -> "true"))

// compilation should now succeed
val reloadResponse =
extractWorkspaceReloadResponse(await(remoteServer.workspaceReload().asScala))
expect(reloadResponse.isEmpty)
val compileAfterReloadResult =
await(remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala)
expect(compileAfterReloadResult.getStatusCode == b.StatusCode.OK)

// code should also be runnable via BSP now
val runResult =
await(remoteServer.buildTargetRun(new b.RunParams(targets.head)).asScala)
expect(runResult.getStatusCode == b.StatusCode.OK)
}
}
}
}

test("BSP reloads --power mode after setting it via config") {
val scriptName = "requires-power.sc"
val inputs = TestInputs(os.rel / scriptName ->
s"""//> using python
|println("scalapy is experimental")""".stripMargin)
inputs.fromRoot { root =>
val configFile = os.rel / "config" / "config.json"
val configEnvs = Map("SCALA_CLI_CONFIG" -> configFile.toString())
os.proc(TestUtil.cli, "setup-ide", scriptName, extraOptions).call(
cwd = root,
env = configEnvs
)
val ideEnvsPath = root / Constants.workspaceDirName / "ide-envs.json"
expect(ideEnvsPath.toNIO.toFile.exists())
val jsonOptions = List("--envs-file", ideEnvsPath.toString)
withBsp(
inputs,
Seq(scriptName),
bspOptions = jsonOptions,
bspEnvs = configEnvs,
reuseRoot = Some(root)
) {
(_, _, remoteServer) =>
async {
val targets = await(remoteServer.workspaceBuildTargets().asScala)
.getTargets.asScala
.filter(!_.getId.getUri.contains("-test"))
.map(_.getId())

// compilation should fail before reload, as --power mode is off
val compileBeforeReloadResult =
await(remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala)
expect(compileBeforeReloadResult.getStatusCode == b.StatusCode.ERROR)

// enable --power mode via config
os.proc(TestUtil.cli, "config", "power", "true")
.call(cwd = root, env = configEnvs)

// compilation should now succeed
val reloadResponse =
extractWorkspaceReloadResponse(await(remoteServer.workspaceReload().asScala))
expect(reloadResponse.isEmpty)
val compileAfterReloadResult =
await(remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala)
expect(compileAfterReloadResult.getStatusCode == b.StatusCode.OK)

// code should also be runnable via BSP now
val runResult =
await(remoteServer.buildTargetRun(new b.RunParams(targets.head)).asScala)
expect(runResult.getStatusCode == b.StatusCode.OK)
}
}
}
}

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