diff --git a/modules/cli/src/main/scala/scala/cli/ScalaCli.scala b/modules/cli/src/main/scala/scala/cli/ScalaCli.scala index b139f378fd..b2ec2c6e42 100644 --- a/modules/cli/src/main/scala/scala/cli/ScalaCli.scala +++ b/modules/cli/src/main/scala/scala/cli/ScalaCli.scala @@ -186,6 +186,17 @@ object ScalaCli { System.err.println( s"Warning: Only java properties are supported in .scala-jvmopts file. Other options are ignored: ${otherOpts.mkString(", ")} " ) + // load java properties from config + for { + configDb <- ConfigDbUtils.configDb.toOption + properties <- configDb.get(Keys.javaProperties).getOrElse(Nil) + } + properties.foreach { opt => + opt.stripPrefix("-D").split("=", 2).match { + case Array(key, value) => System.setProperty(key, value) + case _ => System.err.println(s"Warning: Invalid java property in config: $opt") + } + } } private def main0(args: Array[String]): Unit = { diff --git a/modules/cli/src/main/scala/scala/cli/commands/config/Config.scala b/modules/cli/src/main/scala/scala/cli/commands/config/Config.scala index 7495cb6a67..87765b34b1 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/config/Config.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/config/Config.scala @@ -16,14 +16,7 @@ import scala.cli.commands.publish.ConfigUtil.* import scala.cli.commands.shared.HelpGroup import scala.cli.commands.util.JvmUtils import scala.cli.commands.{ScalaCommand, SpecificationLevel} -import scala.cli.config.{ - ConfigDb, - Keys, - PasswordOption, - PublishCredentials, - RepositoryCredentials, - Secret -} +import scala.cli.config._ import scala.cli.util.ArgHelpers.* import scala.cli.util.ConfigDbUtils object Config extends ScalaCommand[ConfigOptions] { @@ -285,6 +278,8 @@ object Config extends ScalaCommand[ConfigOptions] { else values + checkIfAskForUpdate(entry, finalValues, db, options) + db.setFromString(entry, finalValues) .wrapConfigException .orExit(logger) @@ -298,4 +293,49 @@ object Config extends ScalaCommand[ConfigOptions] { } } } + + /** Check whether to ask for an update depending on the provided key. + */ + private def checkIfAskForUpdate( + entry: Key[_], + newValues: Seq[String], + db: ConfigDb, + options: ConfigOptions + ): Unit = entry match { + case listEntry: Key.StringListEntry => + val previousValue = db.get(listEntry).wrapConfigException.orExit(logger).getOrElse(Nil) + + confirmUpdateValue( + listEntry.fullName, + previousValue, + newValues, + options + ).wrapConfigException.orExit(logger) + case _ => () + } + + /** If the new value is different from the previous value, ask user for confirmation or suggest to + * use --force option. If the new value is the same as the previous value, confirm the operation. + * If force option is provided, skip the confirmation. + */ + private def confirmUpdateValue( + keyFullName: String, + previousValues: Seq[String], + newValues: Seq[String], + options: ConfigOptions + ): Either[Exception, Unit] = + val (newValuesStr, previousValueStr) = (newValues.mkString(", "), previousValues.mkString(", ")) + val shouldUpdate = !options.force && newValuesStr != previousValueStr && previousValues.nonEmpty + + if shouldUpdate then + val interactive = options.global.logging.verbosityOptions.interactiveInstance() + val msg = + s"Do you want to change the key '$keyFullName' from '$previousValueStr' to '$newValuesStr'?" + interactive.confirmOperation(msg) match { + case Some(true) => Right(()) + case _ => Left(new Exception( + s"Unable to change the value for the key: '$keyFullName' from '$previousValueStr' to '$newValuesStr' without the force flag. Please pass -f or --force to override." + )) + } + else Right(()) } diff --git a/modules/cli/src/main/scala/scala/cli/commands/config/ConfigOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/config/ConfigOptions.scala index c35b51f1a0..833e392a93 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/config/ConfigOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/config/ConfigOptions.scala @@ -75,7 +75,13 @@ final case class ConfigOptions( @HelpMessage("For repository.credentials, whether to use these credentials should be passed upon redirection") @Tag(tags.restricted) @Tag(tags.inShortHelp) - passOnRedirect: Option[Boolean] = None + passOnRedirect: Option[Boolean] = None, + @Group(HelpGroup.Config.toString) + @HelpMessage("Force overwriting values for key") + @ExtraName("f") + @Tag(tags.inShortHelp) + @Tag(tags.should) + force: Boolean = false ) extends HasGlobalOptions // format: on diff --git a/modules/config/src/main/scala/scala/cli/config/Keys.scala b/modules/config/src/main/scala/scala/cli/config/Keys.scala index 4edbee31e7..634798fea2 100644 --- a/modules/config/src/main/scala/scala/cli/config/Keys.scala +++ b/modules/config/src/main/scala/scala/cli/config/Keys.scala @@ -126,6 +126,13 @@ object Keys { "Default repository, syntax: https://first-repo.company.com https://second-repo.company.com", specificationLevel = SpecificationLevel.RESTRICTED ) + val javaProperties = new Key.StringListEntry( + prefix = Nil, + name = "java.properties", + description = + "Java properties", + specificationLevel = SpecificationLevel.SHOULD + ) // Kept for binary compatibility val repositoriesMirrors = repositoryMirrors @@ -161,6 +168,7 @@ object Keys { ghToken, globalInteractiveWasSuggested, interactive, + javaProperties, suppressDirectivesInMultipleFilesWarning, suppressOutdatedDependenciessWarning, suppressExperimentalFeatureWarning, diff --git a/modules/docs-tests/src/main/scala/sclicheck/sclicheck.scala b/modules/docs-tests/src/main/scala/sclicheck/sclicheck.scala index a5c75399be..b48cb42e02 100644 --- a/modules/docs-tests/src/main/scala/sclicheck/sclicheck.scala +++ b/modules/docs-tests/src/main/scala/sclicheck/sclicheck.scala @@ -284,6 +284,7 @@ def checkFile(file: os.Path, options: Options): Unit = } case Commands.Clear(_) => os.list(out).filterNot(_ == binDir).foreach(os.remove.all) + os.remove(os.Path(sys.env("SCALA_CLI_CONFIG"))) try println(Blue(s"\n[${file.relativeTo(os.pwd)}] Running checks in $out")) diff --git a/modules/integration/src/test/scala/scala/cli/integration/ConfigTests.scala b/modules/integration/src/test/scala/scala/cli/integration/ConfigTests.scala index 8acaf5bb26..dee07c9299 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/ConfigTests.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/ConfigTests.scala @@ -522,4 +522,42 @@ class ConfigTests extends ScalaCliSuite { } } + test("change value for key") { + val configFile = os.rel / "config" / "config.json" + val configEnv = Map("SCALA_CLI_CONFIG" -> configFile.toString) + val (props, props2, props3) = ("props=test", "props2=test2", "props3=test3") + val key = "java.properties" + TestInputs.empty.fromRoot { root => + // set some values first time + os.proc(TestUtil.cli, "--power", "config", key, props, props2).call( + cwd = root, + env = configEnv + ) + + // override some values should throw error without force flag + val res = os.proc(TestUtil.cli, "--power", "config", key, props, props2, props3).call( + cwd = root, + env = configEnv, + check = false, + mergeErrIntoOut = true + ) + + expect(res.exitCode == 1) + expect(res.out.trim().contains("pass -f or --force")) + + os.proc(TestUtil.cli, "--power", "config", key, props, props2, props3, "-f").call( + cwd = root, + env = configEnv, + check = false + ) + val propertiesFromConfig = os.proc(TestUtil.cli, "--power", "config", key) + .call(cwd = root, env = configEnv) + .out.trim() + + expect(propertiesFromConfig.contains(props)) + expect(propertiesFromConfig.contains(props2)) + expect(propertiesFromConfig.contains(props3)) + } + } + } diff --git a/modules/integration/src/test/scala/scala/cli/integration/SipScalaTests.scala b/modules/integration/src/test/scala/scala/cli/integration/SipScalaTests.scala index d66c6dd9e0..59af6ec2ec 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/SipScalaTests.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/SipScalaTests.scala @@ -32,7 +32,7 @@ class SipScalaTests extends ScalaCliSuite { os.proc(TestUtil.cli, "config", configKey, disableSetting) .call(cwd = root, env = homeEnv) testWhenDisabled(root, homeEnv) - os.proc(TestUtil.cli, "config", configKey, "true") + os.proc(TestUtil.cli, "config", configKey, "true", "-f") .call(cwd = root, env = homeEnv) testWhenEnabled(root, homeEnv) } diff --git a/website/docs/commands/publishing/publish-setup.md b/website/docs/commands/publishing/publish-setup.md index 756f141922..33599dfba8 100644 --- a/website/docs/commands/publishing/publish-setup.md +++ b/website/docs/commands/publishing/publish-setup.md @@ -66,6 +66,8 @@ scala-cli --power config publish.user.email "alex@alex.me" scala-cli --power config publish.user.url "https://alex.me" ``` + + The email can be left empty if you'd rather not put your email in POM files: ```bash scala-cli --power config publish.user.email "" diff --git a/website/docs/reference/cli-options.md b/website/docs/reference/cli-options.md index cb27aa8c89..dc80b591e5 100644 --- a/website/docs/reference/cli-options.md +++ b/website/docs/reference/cli-options.md @@ -207,6 +207,12 @@ For repository.credentials, whether to use these credentials are optional For repository.credentials, whether to use these credentials should be passed upon redirection +### `--force` + +Aliases: `-f` + +Force overwriting values for key + ## Cross options Available in commands: diff --git a/website/docs/reference/commands.md b/website/docs/reference/commands.md index a8e076b23b..a2af92d426 100644 --- a/website/docs/reference/commands.md +++ b/website/docs/reference/commands.md @@ -55,6 +55,7 @@ Available keys: - httpProxy.user HTTP proxy user (used for authentication). - interactive Globally enables interactive mode (the '--interactive' flag). - interactive-was-suggested Setting indicating if the global interactive mode was already suggested. + - java.properties Java properties - pgp.public-key The PGP public key, used for signing. - pgp.secret-key The PGP secret key, used for signing. - pgp.secret-key-password The PGP secret key password, used for signing. diff --git a/website/docs/reference/scala-command/cli-options.md b/website/docs/reference/scala-command/cli-options.md index 610d5ea168..2c30ad5ede 100644 --- a/website/docs/reference/scala-command/cli-options.md +++ b/website/docs/reference/scala-command/cli-options.md @@ -178,6 +178,14 @@ Aliases: `--remove` Remove an entry from config +### `--force` + +Aliases: `-f` + +`SHOULD have` per Scala Runner specification + +Force overwriting values for key + ## Debug options Available in commands: diff --git a/website/docs/reference/scala-command/commands.md b/website/docs/reference/scala-command/commands.md index b9170a54e9..6f59298e9d 100644 --- a/website/docs/reference/scala-command/commands.md +++ b/website/docs/reference/scala-command/commands.md @@ -54,6 +54,7 @@ Available keys: - httpProxy.user HTTP proxy user (used for authentication). - interactive Globally enables interactive mode (the '--interactive' flag). - interactive-was-suggested Setting indicating if the global interactive mode was already suggested. + - java.properties Java properties - pgp.public-key The PGP public key, used for signing. - pgp.secret-key The PGP secret key, used for signing. - pgp.secret-key-password The PGP secret key password, used for signing. diff --git a/website/docs/reference/scala-command/runner-specification.md b/website/docs/reference/scala-command/runner-specification.md index 5c265540af..aa3a7ccf1d 100644 --- a/website/docs/reference/scala-command/runner-specification.md +++ b/website/docs/reference/scala-command/runner-specification.md @@ -610,6 +610,7 @@ Available keys: - httpProxy.user HTTP proxy user (used for authentication). - interactive Globally enables interactive mode (the '--interactive' flag). - interactive-was-suggested Setting indicating if the global interactive mode was already suggested. + - java.properties Java properties - pgp.public-key The PGP public key, used for signing. - pgp.secret-key The PGP secret key, used for signing. - pgp.secret-key-password The PGP secret key password, used for signing. @@ -667,6 +668,12 @@ Remove an entry from config Aliases: `--remove` +**--force** + +Force overwriting values for key + +Aliases: `-f` +
### Implementantation specific options diff --git a/website/docs/under-the-hood.md b/website/docs/under-the-hood.md index 8d6fb8df8b..e452cd0e34 100644 --- a/website/docs/under-the-hood.md +++ b/website/docs/under-the-hood.md @@ -46,12 +46,18 @@ In order set them, the `-D` command-line flags must be placed as the first optio scala-cli -Dfoo1=bar1 -Dfoo2=bar2 run ... ``` +:::note +- `scala-cli run . -Dfoo=bar` would pass the java property into your Scala app +- `scala-cli -Dfoo=bar run .` would pass the java property into `scala-cli`. +::: + The Scala CLI can also load Java properties from the `.scala-jvmopts` file present in the current working directory and import these Java properties into Scala CLI. Any java options in the `.scala-jvmopts` that are not recognizable as Java properties will be ignored. The example below shows that the Java properties `foo1` and `foo2` from the `.scala-jvmopts` file will be passed into the Scala CLI: + ```bash ignore $ cat .scala-jvmopts -Dfoo1=bar1 @@ -59,7 +65,15 @@ $ cat .scala-jvmopts $ scala-cli run ... ``` +You can set Java properties globally for the Scala CLI launcher using the `config` command. +The example below shows how to set the Java properties `-Djavax.net.ssl.trustStore=cacerts` and `-Dfoo=bar2`: + +```bash +scala-cli config java.properties Djavax.net.ssl.trustStore=cacerts Dfoo=bar2 +``` + :::note -- `scala-cli run . -Dfoo=bar` would pass the java property into your Scala app -- `scala-cli -Dfoo=bar run .` would pass the java property into `scala-cli.` -::: \ No newline at end of file +Please note that if you need to modify the Java properties, you have to redefine all of them. It's not possible +to update just a single value via the `config` command. Each update effectively replaces the entire Java properties +list. +:::