Skip to content

Commit e4d1c9e

Browse files
authored
Merge pull request #2253 from lwronski/java-8
Support for running standalone launcher of scala-cli with JVM 8
2 parents 19edde6 + 0183cdf commit e4d1c9e

File tree

9 files changed

+140
-13
lines changed

9 files changed

+140
-13
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ jobs:
5454
run: ./mill -i integration.test.jvm
5555
env:
5656
SCALA_CLI_IT_GROUP: 1
57+
- name: Fat jar integration test
58+
run: ./mill integration.test.jvmBootstrapped 'scala.cli.integration.StandaloneLauncherTests.*'
5759

5860
jvm-tests-2:
5961
timeout-minutes: 120

build.sc

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,19 @@ object cliBootstrapped extends ScalaCliPublishModule {
5252
Assembly.Rule.ExcludePattern(".*\\.tasty"),
5353
Assembly.Rule.ExcludePattern(".*\\.semanticdb")
5454
) ++ super.assemblyRules
55+
56+
override def resources = T.sources {
57+
super.resources() ++ Seq(propertiesFilesResources())
58+
}
59+
60+
def propertiesFilesResources = T.persistent {
61+
val dir = T.dest / "resources"
62+
63+
val dest = dir / "java-properties" / "scala-cli-properties"
64+
val content = "scala-cli.kind=jvm.bootstrapped"
65+
os.write.over(dest, content, createFolders = true)
66+
PathRef(dir)
67+
}
5568
}
5669

5770
object `specification-level` extends Cross[SpecificationLevel](Scala.all: _*)
@@ -1047,6 +1060,11 @@ trait CliIntegration extends SbtModule with ScalaCliPublishModule with HasTests
10471060
cli.standaloneLauncher,
10481061
"jvm"
10491062
).test(args: _*)
1063+
def jvmBootstrapped(args: String*) =
1064+
new TestHelper(
1065+
cliBootstrapped.jar,
1066+
"jvmBootstrapped"
1067+
).test(args: _*)
10501068
def native(args: String*) =
10511069
new TestHelper(
10521070
if (System.getenv("SCALA_CLI_IT_FORCED_LAUNCHER_DIRECTORY") == null) cli.nativeImage

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import scala.build.Directories
1212
import scala.build.internal.Constants
1313
import scala.cli.config.{ConfigDb, Keys}
1414
import scala.cli.internal.Argv0
15+
import scala.cli.javaLauncher.JavaLauncherCli
1516
import scala.cli.launcher.{LauncherCli, LauncherOptions}
1617
import scala.cli.publish.BouncycastleSignerMaker
1718
import scala.cli.util.ConfigDbUtils
@@ -162,7 +163,16 @@ object ScalaCli {
162163
s"Java >= 17 is required to run $fullRunnerName (found Java $javaMajorVersion)"
163164
)
164165

166+
private def loadJavaPropertiesFromResources() = {
167+
val prop = new java.util.Properties()
168+
val cl = getClass.getResourceAsStream("/java-properties/scala-cli-properties")
169+
if cl != null then
170+
prop.load(cl)
171+
prop.stringPropertyNames().forEach(name => System.setProperty(name, prop.getProperty(name)))
172+
}
173+
165174
private def main0(args: Array[String]): Unit = {
175+
loadJavaPropertiesFromResources() // load java properties to detect launcher kind
166176
val remainingArgs = LauncherOptions.parser.stopAtFirstUnrecognized.parse(args.toVector) match {
167177
case Left(e) =>
168178
System.err.println(e.message)
@@ -175,6 +185,10 @@ object ScalaCli {
175185
else Nil
176186
val newArgs = powerArgs ++ args0
177187
LauncherCli.runAndExit(ver, launcherOpts, newArgs)
188+
case _ if
189+
javaMajorVersion < 17
190+
&& sys.props.get("scala-cli.kind").exists(_.startsWith("jvm")) =>
191+
JavaLauncherCli.runAndExit(args)
178192
case None =>
179193
if (launcherOpts.power)
180194
isSipScala = false
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package scala.cli.javaLauncher
2+
3+
import java.io.File
4+
5+
import scala.build.Positioned
6+
import scala.build.errors.BuildException
7+
import scala.build.internal.{OsLibc, Runner}
8+
import scala.build.options.{BuildOptions, JavaOptions}
9+
import scala.cli.commands.shared.LoggingOptions
10+
import scala.cli.javaLauncher.JavaLauncherCli.LauncherKind._
11+
object JavaLauncherCli {
12+
13+
def runAndExit(remainingArgs: Seq[String]): Nothing = {
14+
val logger = LoggingOptions().logger
15+
val scalaCliPath =
16+
System.getProperty("java.class.path").split(File.pathSeparator).iterator.toList.map { f =>
17+
os.Path(f, os.pwd)
18+
}
19+
20+
val buildOptions = BuildOptions(
21+
javaOptions = JavaOptions(
22+
jvmIdOpt = Some(OsLibc.baseDefaultJvm(OsLibc.jvmIndexOs, "17")).map(Positioned.none)
23+
)
24+
)
25+
val launcherKind = sys.props.get("scala-cli.kind") match {
26+
case Some("jvm.bootstrapped") => Bootstrapped
27+
case Some("jvm.standaloneLauncher") => StandaloneLauncher
28+
case _ => sys.error("should not happen")
29+
}
30+
val classPath = launcherKind match {
31+
case Bootstrapped => scalaCliPath
32+
case StandaloneLauncher => scalaCliPath.headOption.toList
33+
}
34+
val mainClass = launcherKind match {
35+
case Bootstrapped => "scala.cli.ScalaCli"
36+
case StandaloneLauncher => "coursier.bootstrap.launcher.ResourcesLauncher"
37+
}
38+
39+
val exitCode =
40+
Runner.runJvm(
41+
buildOptions.javaHome().value.javaCommand,
42+
buildOptions.javaOptions.javaOpts.toSeq.map(_.value.value),
43+
classPath,
44+
mainClass,
45+
remainingArgs,
46+
logger,
47+
allowExecve = true
48+
).waitFor()
49+
50+
sys.exit(exitCode)
51+
}
52+
53+
enum LauncherKind {
54+
case Bootstrapped, StandaloneLauncher
55+
}
56+
}

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

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ abstract class ExportJsonTestDefinitions(val scalaVersionOpt: Option[String])
4343
|"scalaVersion":"${Constants.scala3}",
4444
|"platform":"JVM",
4545
|"jvmVersion":"adopt:11",
46-
|"scopes": {
47-
| "main": {
46+
|"scopes": [[
47+
| "main", {
4848
| "sources": ["Main.scala"],
4949
| "dependencies": [
5050
| {
@@ -62,7 +62,7 @@ abstract class ExportJsonTestDefinitions(val scalaVersionOpt: Option[String])
6262
| "ivy:file:.../.ivy2/local/"
6363
| ]
6464
| }
65-
|}
65+
|]]
6666
|}
6767
|""".replaceAll("\\s|\\|", ""))
6868
}
@@ -107,8 +107,8 @@ abstract class ExportJsonTestDefinitions(val scalaVersionOpt: Option[String])
107107
|"scalaVersion":"3.2.2",
108108
|"platform":"Native",
109109
|"scalaNativeVersion":"${Constants.scalaNativeVersion}",
110-
|"scopes": {
111-
| "main": {
110+
|"scopes": [[
111+
| "main", {
112112
| "sources": ["Main.scala"],
113113
| "scalacOptions":["-Xasync"],
114114
| "scalaCompilerPlugins": [
@@ -136,8 +136,8 @@ abstract class ExportJsonTestDefinitions(val scalaVersionOpt: Option[String])
136136
| "ivy:file:.../scalacli/local-repo/...",
137137
| "ivy:file:.../.ivy2/local/"
138138
| ]
139-
| },
140-
| "test": {
139+
| }], [
140+
| "test", {
141141
| "sources":["unit.test.scala"],
142142
| "scalacOptions":["-Xasync"],
143143
| "scalaCompilerPlugins": [
@@ -169,7 +169,7 @@ abstract class ExportJsonTestDefinitions(val scalaVersionOpt: Option[String])
169169
| "resourcesDirs":["./resources"],
170170
| "customJarsDecls":["./TEST.jar"]
171171
| }
172-
|}
172+
|]]
173173
|}
174174
|""".replaceAll("\\s|\\|", ""))
175175
}
@@ -215,8 +215,8 @@ abstract class ExportJsonTestDefinitions(val scalaVersionOpt: Option[String])
215215
|"platform": "JS",
216216
|"scalaJsVersion": "${Constants.scalaJsVersion}",
217217
|"jsEsVersion":"es2015",
218-
|"scopes": {
219-
| "main": {
218+
|"scopes": [[
219+
| "main", {
220220
| "sources": ["Main.scala"],
221221
| "scalacOptions": ["-Xasync"],
222222
| "scalaCompilerPlugins": [
@@ -245,7 +245,7 @@ abstract class ExportJsonTestDefinitions(val scalaVersionOpt: Option[String])
245245
| "ivy:file:.../.ivy2/local/"
246246
| ]
247247
| }
248-
|}
248+
|]]
249249
|}
250250
|""".replaceAll("\\s|\\|", ""))
251251

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package scala.cli.integration
2+
3+
import com.eed3si9n.expecty.Expecty.expect
4+
5+
import java.io.File
6+
7+
class StandaloneLauncherTests extends ScalaCliSuite {
8+
9+
if (TestUtil.isJvmCli)
10+
test(s"Standalone launcher should run with java 8") {
11+
// It should download Java 17 and use it to run itself
12+
val message = "Hello World"
13+
val inputs = TestInputs(
14+
os.rel / "hello.sc" -> s"""println("$message")"""
15+
)
16+
inputs.fromRoot { root =>
17+
val java8Home =
18+
os.Path(os.proc(TestUtil.cs, "java-home", "--jvm", "zulu:8").call().out.trim(), os.pwd)
19+
20+
val extraEnv = Map(
21+
"JAVA_HOME" -> java8Home.toString,
22+
"PATH" -> ((java8Home / "bin").toString + File.pathSeparator + System.getenv("PATH"))
23+
)
24+
25+
val output =
26+
os.proc(TestUtil.cli, ".").call(cwd = root, env = extraEnv).out.trim()
27+
28+
expect(output == message)
29+
}
30+
}
31+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ object TestUtil {
1616

1717
val cliKind: String = sys.props("test.scala-cli.kind")
1818
val isNativeCli: Boolean = cliKind.startsWith("native")
19+
val isJvmCli: Boolean = cliKind.startsWith("jvm")
1920
val isCI: Boolean = System.getenv("CI") != null
2021
val cliPath: String = sys.props("test.scala-cli.path")
2122
val debugPortOpt: Option[String] = sys.props.get("test.scala-cli.debug.port")

project/deps.sc

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,11 @@ object Deps {
121121
def jimfs = ivy"com.google.jimfs:jimfs:1.2"
122122
def jniUtils = ivy"io.get-coursier.jniutils:windows-jni-utils:0.3.3"
123123
def jsoniterCore =
124-
ivy"com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core:${Versions.jsoniterScala}"
124+
ivy"com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core:${Versions.jsoniterScalaJava8}"
125125
def jsoniterCoreJava8 =
126126
ivy"com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core:${Versions.jsoniterScalaJava8}"
127127
def jsoniterMacros =
128-
ivy"com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:${Versions.jsoniterScala}"
128+
ivy"com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:${Versions.jsoniterScalaJava8}"
129129
def jsoniterMacrosJava8 =
130130
ivy"com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:${Versions.jsoniterScalaJava8}"
131131
def libsodiumjni = ivy"org.virtuslab.scala-cli:libsodiumjni:0.0.3"
@@ -165,10 +165,14 @@ object Deps {
165165
ivy"org.virtuslab.scala-cli-signing::shared:${Versions.signingCli}"
166166
// to prevent collisions with scala-cli's case-app version
167167
.exclude(("com.github.alexarchambault", "case-app_3"))
168+
.exclude(("com.github.plokhotnyuk.jsoniter-scala", "jsoniter-scala-core_3"))
169+
.exclude(("com.github.plokhotnyuk.jsoniter-scala", "jsoniter-scala-macros_3"))
168170
def signingCli =
169171
ivy"org.virtuslab.scala-cli-signing::cli:${Versions.signingCli}"
170172
// to prevent collisions with scala-cli's case-app version
171173
.exclude(("com.github.alexarchambault", "case-app_3"))
174+
.exclude(("com.github.plokhotnyuk.jsoniter-scala", "jsoniter-scala-core_3"))
175+
.exclude(("com.github.plokhotnyuk.jsoniter-scala", "jsoniter-scala-macros_3"))
172176
def slf4jNop = ivy"org.slf4j:slf4j-nop:2.0.7"
173177
def sttp = ivy"com.softwaremill.sttp.client3:core_2.13:3.8.15"
174178
def svm = ivy"org.graalvm.nativeimage:svm:$graalVmVersion"

project/settings.sc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,7 @@ trait CliLaunchers extends SbtModule { self =>
521521
val params = Parameters.Bootstrap(Seq(loaderContent), mainClass0)
522522
.withDeterministic(true)
523523
.withPreamble(preamble)
524+
.withJavaProperties(Seq("scala-cli.kind" -> "jvm.standaloneLauncher"))
524525

525526
BootstrapGenerator.generate(params, dest.toNIO)
526527

0 commit comments

Comments
 (0)