Skip to content

Commit

Permalink
Set globally Java properties for ScalaCLI launcher (#2317)
Browse files Browse the repository at this point in the history
* Add java.properties key into config

* sclicheck: remove config file for clear command

* Ask user to confirm when change value for key

* Load java properties into ScalaCli from config

* Add docs
  • Loading branch information
lwronski authored Aug 3, 2023
1 parent dfd5339 commit 085006e
Show file tree
Hide file tree
Showing 14 changed files with 156 additions and 13 deletions.
11 changes: 11 additions & 0 deletions modules/cli/src/main/scala/scala/cli/ScalaCli.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
56 changes: 48 additions & 8 deletions modules/cli/src/main/scala/scala/cli/commands/config/Config.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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] {
Expand Down Expand Up @@ -285,6 +278,8 @@ object Config extends ScalaCommand[ConfigOptions] {
else
values

checkIfAskForUpdate(entry, finalValues, db, options)

db.setFromString(entry, finalValues)
.wrapConfigException
.orExit(logger)
Expand All @@ -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(())
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 8 additions & 0 deletions modules/config/src/main/scala/scala/cli/config/Keys.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -161,6 +168,7 @@ object Keys {
ghToken,
globalInteractiveWasSuggested,
interactive,
javaProperties,
suppressDirectivesInMultipleFilesWarning,
suppressOutdatedDependenciessWarning,
suppressExperimentalFeatureWarning,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 2 additions & 0 deletions website/docs/commands/publishing/publish-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ scala-cli --power config publish.user.email "[email protected]"
scala-cli --power config publish.user.url "https://alex.me"
```

<!-- clear -->

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 ""
Expand Down
6 changes: 6 additions & 0 deletions website/docs/reference/cli-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions website/docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions website/docs/reference/scala-command/cli-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions website/docs/reference/scala-command/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions website/docs/reference/scala-command/runner-specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -667,6 +668,12 @@ Remove an entry from config

Aliases: `--remove`

**--force**

Force overwriting values for key

Aliases: `-f`

<details><summary>

### Implementantation specific options
Expand Down
20 changes: 17 additions & 3 deletions website/docs/under-the-hood.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,34 @@ 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
-Dfoo2=bar2
$ 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.`
:::
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.
:::

0 comments on commit 085006e

Please sign in to comment.