Skip to content

Commit

Permalink
Provide default JVM version to be used by Mill server process, when `…
Browse files Browse the repository at this point in the history
….mill-jvm-version` and system `java` are not provided (#4597)

This PR makes Mill's server use a fixed JVM version by default in the
case where no other Java version is available:

1. No `.mill-jvm-version` file present
2. No `java` command available environmentally
3. The Mill client has no `java.home` set (i.e. it is a graal native
image)

This makes running Mill on a clean environment without a JVM installed
work by default. Previously, it either needed you to define a
`.mill-jvm-version` or install a JVM, either of which is a manual step
we now can now avoid. Now it instead uses the version of Java configured
in Mill's own `.mill-jvm-version` file

We still allow any system `java` command to take precedence over the
default, since that's what most people would expect when using the JVM.

Added an integration test in CI `no-jvm-bootstrap` that runs Mill with
the native launcher with no `.mill-jvm-version` file and no system
`java` installed to verify that it falls back to the Mill default JVM
version. Needed to add some hacky test hooks to make sure we exercise
the code path in question
  • Loading branch information
lihaoyi authored Feb 25, 2025
1 parent 6825727 commit 5281a01
Show file tree
Hide file tree
Showing 18 changed files with 159 additions and 54 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/post-build-selective.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ on:
type: string

coursierarchive:
default: ''
default: "/tmp"
required: false
type: string
java-version:
required: true
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/publish-artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,22 @@ jobs:
matrix:
include:
- os: ubuntu-latest
coursierarchive: ""
coursierarchive: "/tmp"
publishartifacts: __.publishArtifacts
uploadgithub: true

- os: ubuntu-24.04-arm
coursierarchive: ""
coursierarchive: "/tmp"
publishartifacts: dist.native.publishArtifacts
uploadgithub: false

- os: macos-13
coursierarchive: ""
coursierarchive: "/tmp"
publishartifacts: dist.native.publishArtifacts
uploadgithub: false

- os: macos-latest
coursierarchive: ""
coursierarchive: "/tmp"
publishartifacts: dist.native.publishArtifacts
uploadgithub: false

Expand Down
18 changes: 14 additions & 4 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Uncommment this to replace the rest of the file when you want to debug stuff in CI
#

#
#name: Run Debug
#
Expand All @@ -9,18 +9,19 @@
# workflow_dispatch:
#
#jobs:
# debug-windows:
# debug:
## runs-on: ubuntu-latest
# runs-on: windows-latest
# steps:
# - uses: actions/checkout@v4
# with: { fetch-depth: 1 }
#
# - run: ./mill scalalib.test mill.scalalib.LauncherTests.launcher
# - run: ./mill 'integration.feature[no-java-bootstrap].native.server.test'
# env:
# COURSIER_ARCHIVE_CACHE: "C:/coursier-arc"
#


# We run full CI on push builds to main and on all pull requests
#
# To maximize bug-catching changes while keeping CI times reasonable, we run
Expand Down Expand Up @@ -114,7 +115,7 @@ jobs:
- uses: ./.github/actions/post-build-selective
with:
millargs: ${{ matrix.millargs }}
coursierarchive: ""
coursierarchive: "/tmp"
install-android-sdk: false
shell: bash

Expand Down Expand Up @@ -184,6 +185,12 @@ jobs:
millargs: "'integration.{failure,feature,ide}.__.packaged.server.test'"
install-android-sdk: false

# run this specifically in `native` mode to make sure our non-JVM native image
# launcher is able to bootstrap everything necessary without a JVM installed
- java-version: 17
millargs: "'integration.bootstrap[no-java-bootstrap].native.server.test'"
install-android-sdk: false

# These invalidation tests need to be exercised in both execution modes
# to make sure they work with and without -i/--no-server being passed
- java-version: 17
Expand Down Expand Up @@ -225,6 +232,9 @@ jobs:
- java-version: 11 # Run this with Mill native launcher as a smoketest
millargs: "'integration.invalidation.__.native.server.test'"

- java-version: 17
millargs: "'integration.bootstrap[no-java-bootstrap].native.server.test'"

uses: ./.github/workflows/post-build-selective.yml
with:
os: windows-latest
Expand Down
2 changes: 1 addition & 1 deletion .mill-jvm-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
zulu:17.0.13
zulu:17.0.14
2 changes: 2 additions & 0 deletions build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,8 @@ def millBinPlatform: T[String] = Task {

def baseDir = build.millSourcePath

def millJvmVersion = Task.Source(Task.workspace / ".mill-jvm-version")

val essentialBridgeScalaVersions =
Seq(Deps.scalaVersion, Deps.scala2Version, Deps.workerScalaVersion212)
// published compiler bridges
Expand Down
25 changes: 14 additions & 11 deletions ci/mill-bootstrap.patch
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
diff --git a/build.mill b/build.mill
index 36bf4e9a593..84fd76e6645 100644
index 5c48e1e4e43..538e4b86e0c 100644
--- a/build.mill
+++ b/build.mill
@@ -1,16 +1,16 @@
Expand Down Expand Up @@ -54,18 +54,18 @@ index 36bf4e9a593..84fd76e6645 100644
-def baseDir = build.millSourcePath
+def baseDir = build.moduleDir

val essentialBridgeScalaVersions =
Seq(Deps.scalaVersion, Deps.scala2Version, Deps.workerScalaVersion212)
@@ -467,7 +467,7 @@ trait MillPublishJavaModule extends MillJavaModule with PublishModule {
def millJvmVersion = Task.Source(Task.workspace / ".mill-jvm-version")

@@ -469,7 +469,7 @@ trait MillPublishJavaModule extends MillJavaModule with PublishModule {
/**
* Some custom scala settings and test convenience
*/
-trait MillScalaModule extends ScalaModule with MillJavaModule with ScalafixModule { outer =>
+trait MillScalaModule extends ScalaModule with MillJavaModule /*with ScalafixModule */{ outer =>
+trait MillScalaModule extends ScalaModule with MillJavaModule/* with ScalafixModule*/ { outer =>
def scalaVersion = Deps.scalaVersion
def scalapVersion: T[String] = Deps.scala2Version
def scalafixScalaBinaryVersion = T {
@@ -524,8 +524,8 @@ trait MillScalaModule extends ScalaModule with MillJavaModule with ScalafixModul
@@ -526,8 +526,8 @@ trait MillScalaModule extends ScalaModule with MillJavaModule with ScalafixModul
val binaryVersion = ZincWorkerUtil.scalaBinaryVersion(sv)
val hasModuleDefs = binaryVersion == "2.13" || binaryVersion == "3"
super.scalacPluginIvyDeps() ++
Expand All @@ -76,7 +76,7 @@ index 36bf4e9a593..84fd76e6645 100644
}

def mandatoryIvyDeps = T {
@@ -533,13 +533,13 @@ trait MillScalaModule extends ScalaModule with MillJavaModule with ScalafixModul
@@ -535,13 +535,13 @@ trait MillScalaModule extends ScalaModule with MillJavaModule with ScalafixModul
val binaryVersion = ZincWorkerUtil.scalaBinaryVersion(sv)
val hasModuleDefs = binaryVersion == "2.13" || binaryVersion == "3"
super.mandatoryIvyDeps() ++
Expand All @@ -92,7 +92,7 @@ index 36bf4e9a593..84fd76e6645 100644
def scalafixConfig = T { Some(T.workspace / ".scalafix.conf") }
def forkArgs = super.forkArgs() ++ outer.testArgs()
def moduleDeps = outer.testModuleDeps
@@ -579,7 +579,8 @@ trait MillBaseTestsModule extends TestModule {
@@ -581,7 +581,8 @@ trait MillBaseTestsModule extends TestModule {
trait MillPublishScalaModule extends MillScalaModule with MillPublishJavaModule

/** Publishable module which contains strictly handled API. */
Expand All @@ -102,7 +102,7 @@ index 36bf4e9a593..84fd76e6645 100644
import com.github.lolgab.mill.mima._
override def mimaBinaryIssueFilters: T[Seq[ProblemFilter]] = Seq(
// (5x) MIMA doesn't properly ignore things which are nested inside other private things
@@ -709,7 +710,7 @@ trait MillStableScalaModule extends MillPublishScalaModule with Mima {
@@ -711,7 +712,7 @@ trait MillStableScalaModule extends MillPublishScalaModule with Mima {
def skipPreviousVersions: T[Seq[String]] = T {
T.log.info("Skipping mima for previous versions (!!1000s of errors due to Scala 3)")
mimaPreviousVersions() // T(Seq.empty[String])
Expand Down Expand Up @@ -489,7 +489,7 @@ index 62190fd6c1e..9d4f176c915 100644
val (repoPath, repoHash) = repoInfo(crossValue)
def repoSlug = repoPath.split("/").last
diff --git a/integration/package.mill b/integration/package.mill
index af00b40d3f0..e43343cabf1 100644
index 53cdc15a9f6..9271663b81e 100644
--- a/integration/package.mill
+++ b/integration/package.mill
@@ -3,13 +3,12 @@ package build.integration
Expand Down Expand Up @@ -530,7 +530,7 @@ index af00b40d3f0..e43343cabf1 100644
def scalaVersion = build.Deps.scalaVersion

def forkEnv =
@@ -87,11 +86,11 @@ object `package` extends RootModule {
@@ -87,13 +86,13 @@ object `package` extends RootModule {
}
}

Expand All @@ -543,6 +543,9 @@ index af00b40d3f0..e43343cabf1 100644
- object ide extends Cross[IdeIntegrationCrossModule](build.listIn(millSourcePath / "ide"))
+ extends Cross[IntegrationCrossModule](build.listIn(moduleDir / "invalidation"))
+ object ide extends Cross[IdeIntegrationCrossModule](build.listIn(moduleDir / "ide"))
object bootstrap
- extends Cross[IdeIntegrationCrossModule](build.listIn(millSourcePath / "bootstrap"))
+ extends Cross[IdeIntegrationCrossModule](build.listIn(moduleDir / "bootstrap"))
trait IntegrationCrossModule extends build.MillScalaModule with IntegrationTestModule {
override def moduleDeps = super[IntegrationTestModule].moduleDeps
def forkEnv = super.forkEnv() ++ Seq(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package bar;
public class Bar {
public static void main(String[] args) {
System.out.println("Hello World! " + System.getProperty("java.version"));
}
}
8 changes: 8 additions & 0 deletions integration/bootstrap/no-java-bootstrap/resources/build.mill
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package build
import mill._, javalib._

def foo = Task {
println(System.getProperty("java.version"))
}

object bar extends JavaModule
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package mill.integration

import mill.testkit.UtestIntegrationTestSuite

import utest._

object NoJavaBootstrapTests extends UtestIntegrationTestSuite {
// Don't propagate `JAVA_HOME` to this test suite, because we want to exercise
// the code path where `JAVA_HOME` is not present during bootstrapping
override def propagateJavaHome = false
val tests: Tests = Tests {
test - integrationTest { tester =>
import tester._
os.remove(tester.workspacePath / ".mill-jvm-version")
// The Mill server process should use the default Mill Java version,
// even without the `.mill-jvm-version` present
//
// Force Mill client to ignore any system `java` installation, to make sure
// this tests works reliably regardless of what is installed on the system
val res1 = eval(
"foo",
env = Map("MILL_TEST_SUITE_IGNORE_SYSTEM_JAVA" -> "true"),
stderr = os.Inherit
)

assert(res1.out == System.getProperty("java.version"))

// Any `JavaModule`s run from the Mill server should also inherit
// the default Mill Java version from it
val res2 = eval(
"bar.run",
env = Map("MILL_TEST_SUITE_IGNORE_SYSTEM_JAVA" -> "true"),
stderr = os.Inherit
)

assert(res2.out == s"Hello World! ${System.getProperty("java.version")}")
}
}
}
4 changes: 2 additions & 2 deletions integration/ide/bsp-server/src/BspServerTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import ch.epfl.scala.{bsp4j => b}
import mill.api.BuildInfo
import mill.bsp.Constants
import mill.integration.BspServerTestUtil._
import mill.testkit.{UtestIntegrationTestSuite, IntegrationTester}
import mill.testkit.UtestIntegrationTestSuite
import utest._

import scala.jdk.CollectionConverters._
Expand Down Expand Up @@ -48,7 +48,7 @@ object BspServerTests extends UtestIntegrationTestSuite {

withBspServer(
workspacePath,
IntegrationTester.millTestSuiteEnv
millTestSuiteEnv
) { (buildServer, initRes) =>
val scala2Version = sys.props.getOrElse("TEST_SCALA_2_13_VERSION", ???)
val scala3Version = sys.props.getOrElse("MILL_SCALA_3_NEXT_VERSION", ???)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ object VersionChangeTests extends UtestIntegrationTestSuite {
val javaVersion1 = eval(("show", "javaVersion"))
assert(!javaVersion1.out.contains("19.0.2"))

os.write(workspacePath / ".mill-jvm-version", "temurin:19.0.2")
os.write.over(workspacePath / ".mill-jvm-version", "temurin:19.0.2")

val javaVersion2 = eval(("show", "javaVersion"))
assert(javaVersion2.out == "\"19.0.2\"")
Expand Down
2 changes: 2 additions & 0 deletions integration/package.mill
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ object `package` extends RootModule {
object invalidation
extends Cross[IntegrationCrossModule](build.listIn(millSourcePath / "invalidation"))
object ide extends Cross[IdeIntegrationCrossModule](build.listIn(millSourcePath / "ide"))
object bootstrap
extends Cross[IdeIntegrationCrossModule](build.listIn(millSourcePath / "bootstrap"))
trait IntegrationCrossModule extends build.MillScalaModule with IntegrationTestModule {
override def moduleDeps = super[IntegrationTestModule].moduleDeps
def forkEnv = super.forkEnv() ++ Seq(
Expand Down
15 changes: 13 additions & 2 deletions runner/client/src/mill/runner/client/MillProcessLauncher.java
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,24 @@ static boolean isWin() {
return System.getProperty("os.name", "").startsWith("Windows");
}

static String javaHome() throws IOException {
static String javaHome() throws Exception {
String jvmId;
Path millJvmVersionFile = millJvmVersionFile();

String javaHome = null;
if (Files.exists(millJvmVersionFile)) {
jvmId = Files.readString(millJvmVersionFile).trim();
} else {
boolean systemJavaExists =
new ProcessBuilder(isWin() ? "where" : "which", "java").start().waitFor() == 0;
if (systemJavaExists && System.getenv("MILL_TEST_SUITE_IGNORE_SYSTEM_JAVA") == null) {
jvmId = null;
} else {
jvmId = mill.client.BuildInfo.defaultJvmId;
}
}

if (jvmId != null) {

// Fast path to avoid calling `CoursierClient` and paying the classloading cost
// when the `javaHome` JVM has already been initialized for the configured `jvmId`
Expand All @@ -144,7 +155,7 @@ static String javaHome() throws IOException {
return javaHome;
}

static String javaExe() throws IOException {
static String javaExe() throws Exception {
String javaHome = javaHome();
if (javaHome == null) return "java";
else {
Expand Down
7 changes: 6 additions & 1 deletion runner/server/client/package.mill
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import mill.scalalib._
*/
object `package` extends RootModule with build.MillPublishJavaModule with BuildInfo {
def buildInfoPackageName = "mill.client"
def buildInfoMembers = Seq(BuildInfo.Value("millVersion", build.millVersion(), "Mill version."))
def millJvmVersion = Task.Source(Task.workspace / ".mill-jvm-version")

def buildInfoMembers = Seq(
BuildInfo.Value("millVersion", build.millVersion(), "Mill version."),
BuildInfo.Value("defaultJvmId", os.read(millJvmVersion().path).trim())
)

def moduleDeps = Seq(build.core.constants)

Expand Down
5 changes: 3 additions & 2 deletions testkit/src/mill/testkit/ExampleTester.scala
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ class ExampleTester(
val workspaceSourcePath: os.Path,
millExecutable: os.Path,
bashExecutable: String = ExampleTester.defaultBashExecutable(),
val baseWorkspacePath: os.Path
val baseWorkspacePath: os.Path,
val propagateJavaHome: Boolean = true
) extends IntegrationTesterBase {

def processCommandBlock(commandBlock: String): Unit = {
Expand Down Expand Up @@ -130,7 +131,7 @@ class ExampleTester(
stderr = os.Inherit,
cwd = workspacePath,
mergeErrIntoOut = true,
env = IntegrationTester.millTestSuiteEnv ++ windowsPathEnv,
env = millTestSuiteEnv ++ windowsPathEnv,
check = false
)

Expand Down
Loading

0 comments on commit 5281a01

Please sign in to comment.