From aa9150fd2de873b5a8489f2ab6623c6dfb39d3ce Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Fri, 28 Jun 2024 16:35:42 +0200 Subject: [PATCH] Ensure BSP respects --power mode --- .../src/main/scala/scala/cli/ScalaCli.scala | 3 +- .../scala/scala/cli/commands/bsp/Bsp.scala | 28 ++- .../scala/scala/cli/util/ConfigDbUtils.scala | 7 +- .../cli/integration/BspTestDefinitions.scala | 187 +++++++++++++++++- 4 files changed, 221 insertions(+), 4 deletions(-) diff --git a/modules/cli/src/main/scala/scala/cli/ScalaCli.scala b/modules/cli/src/main/scala/scala/cli/ScalaCli.scala index 96e58eedad..8f9becd6a6 100644 --- a/modules/cli/src/main/scala/scala/cli/ScalaCli.scala +++ b/modules/cli/src/main/scala/scala/cli/ScalaCli.scala @@ -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" diff --git a/modules/cli/src/main/scala/scala/cli/commands/bsp/Bsp.scala b/modules/cli/src/main/scala/scala/cli/commands/bsp/Bsp.scala index 467f94f73a..468c86d44c 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/bsp/Bsp.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/bsp/Bsp.scala @@ -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 @@ -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)) @@ -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) + 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) @@ -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 { @@ -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) @@ -114,6 +138,7 @@ object Bsp extends ScalaCommand[BspOptions] { val envs = getEnvsFromFile() val bspBuildOptions = buildOptions(sharedOptions, launcherOptions, envs) .orElse(finalBuildOptions) + refreshPowerMode(launcherOptions, sharedOptions, envs) BspReloadableOptions( buildOptions = bspBuildOptions, bloopRifleConfig = sharedOptions.bloopRifleConfig(Some(bspBuildOptions)) @@ -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), diff --git a/modules/cli/src/main/scala/scala/cli/util/ConfigDbUtils.scala b/modules/cli/src/main/scala/scala/cli/util/ConfigDbUtils.scala index 5fa5bb17d2..172fe3ba29 100644 --- a/modules/cli/src/main/scala/scala/cli/util/ConfigDbUtils.scala +++ b/modules/cli/src/main/scala/scala/cli/util/ConfigDbUtils.scala @@ -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 @@ -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 diff --git a/modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala index 6582b04126..8c5f6ebcd7 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala @@ -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 @@ -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 @@ -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