diff --git a/core/constants/src/mill/constants/CacheFiles.java b/core/constants/src/mill/constants/CacheFiles.java new file mode 100644 index 000000000000..6a7053d643f8 --- /dev/null +++ b/core/constants/src/mill/constants/CacheFiles.java @@ -0,0 +1,15 @@ +package mill.constants; + +public class CacheFiles { + /** Prefix for all cache files. */ + public static final String prefix = "mill-"; + + public static String filename(String name) { + return prefix + name; + } + + public static final String javaHome = "java-home"; + + /** Caches the classpath for the mill runner. */ + public static final String resolveRunner = "resolve-runner"; +} diff --git a/integration/feature/mill-scala-version/resources/.directory b/integration/feature/mill-scala-version/resources/.directory new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/integration/feature/mill-scala-version/src/MillScalaVersionTests.scala b/integration/feature/mill-scala-version/src/MillScalaVersionTests.scala new file mode 100644 index 000000000000..3b44160421dd --- /dev/null +++ b/integration/feature/mill-scala-version/src/MillScalaVersionTests.scala @@ -0,0 +1,82 @@ +package mill.integration + +import mill.constants.CacheFiles +import mill.testkit.{IntegrationTester, UtestIntegrationTestSuite} +import utest.* + +object MillScalaVersionTests extends UtestIntegrationTestSuite { + private def writeBuildMill(tester: IntegrationTester, scalaVersion: Option[String]): Unit = { + val scalaVersionLine = scalaVersion.fold("")(v => s"//| mill-scala-version: $v") + os.write.over( + tester.workspacePath / "build.mill", + s"""$scalaVersionLine + |package build + | + |// empty file + |""".stripMargin + ) + } + + private def readResolveRunner(tester: IntegrationTester) = + os.read.lines(tester.workspacePath / "out" / CacheFiles.filename(CacheFiles.resolveRunner)) + + private def scalaLibraryFor(version: String) = + s"org/scala-lang/scala3-library_3/$version/scala3-library_3-$version.jar" + + private def scalaCompilerFor(version: String) = + s"org/scala-lang/scala3-compiler_3/$version/scala3-compiler_3-$version.jar" + + private val ScalaVersion = "3.7.2-RC1" + + val tests: Tests = Tests { + test("noDirective") - integrationTest { tester => + import tester.* + + writeBuildMill(tester, scalaVersion = None) + val res = eval("version") + assert(res.isSuccess) + + val lines = readResolveRunner(tester) + assert(!lines.exists(_.contains(scalaCompilerFor(ScalaVersion)))) + assert(!lines.exists(_.contains(scalaLibraryFor(ScalaVersion)))) + } + + test("withDirective") - integrationTest { tester => + import tester.* + + writeBuildMill(tester, scalaVersion = Some(ScalaVersion)) + val res = eval("version") + assert(res.isSuccess) + + val lines = readResolveRunner(tester) + assert(lines.exists(_.contains(scalaCompilerFor(ScalaVersion)))) + assert(lines.exists(_.contains(scalaLibraryFor(ScalaVersion)))) + } + + test("withDirectiveAndThenWithout") - integrationTest { tester => + import tester.* + + { + writeBuildMill(tester, scalaVersion = Some(ScalaVersion)) + val res = eval("version") + assert(res.isSuccess) + + val lines = readResolveRunner(tester) + assert(lines.exists(_.contains(scalaCompilerFor(ScalaVersion)))) + assert(lines.exists(_.contains(scalaLibraryFor(ScalaVersion)))) + } + + { + // This should recompile the build with the new version. + writeBuildMill(tester, scalaVersion = None) + val res = eval("version") + assert(res.isSuccess) + + val lines = readResolveRunner(tester) + assert(!lines.exists(_.contains(scalaCompilerFor(ScalaVersion)))) + assert(!lines.exists(_.contains(scalaLibraryFor(ScalaVersion)))) + } + } + + } +} diff --git a/runner/launcher/src/mill/launcher/CoursierClient.scala b/runner/launcher/src/mill/launcher/CoursierClient.scala index 1a1bc1dfa623..4e15b374e3ae 100644 --- a/runner/launcher/src/mill/launcher/CoursierClient.scala +++ b/runner/launcher/src/mill/launcher/CoursierClient.scala @@ -12,32 +12,53 @@ import scala.concurrent.duration.Duration import mill.coursierutil.TestOverridesRepo object CoursierClient { - def resolveMillDaemon() = { + + /** + * Resolves the classpath for the mill daemon. + * + * @param scalaVersion the version of the Scala to use. If not specified, the version that mill uses itself will be + * used. + */ + def resolveMillDaemon(scalaVersion: Option[String]): Array[String] = { val repositories = Await.result(Resolve().finalRepositories.future(), Duration.Inf) val coursierCache0 = FileCache[Task]() .withLogger(coursier.cache.loggers.RefreshLogger.create()) - val artifactsResultOrError = { - + val artifactsResult = { val resolve = Resolve() .withCache(coursierCache0) - .withDependencies(Seq(Dependency( - Module(Organization("com.lihaoyi"), ModuleName("mill-runner-daemon_3"), Map()), - VersionConstraint(mill.client.BuildInfo.millVersion) - ))) + .withDependencies(Seq( + Dependency( + Module( + Organization("com.lihaoyi"), + ModuleName("mill-runner-daemon_3"), + attributes = Map.empty + ), + VersionConstraint(mill.client.BuildInfo.millVersion) + ) + ) ++ scalaVersion.map { version => + Dependency( + Module( + Organization("org.scala-lang"), + ModuleName("scala3-compiler_3"), + attributes = Map.empty + ), + VersionConstraint(version) + ) + }) .withRepositories(Seq(TestOverridesRepo) ++ repositories) resolve.either() match { case Left(err) => sys.error(err.toString) - case Right(v) => + case Right(resolution) => Artifacts(coursierCache0) - .withResolution(v) + .withResolution(resolution) .eitherResult() .right.get } } - artifactsResultOrError.artifacts.map(_._2.toString).toArray + artifactsResult.artifacts.iterator.map { case (_, file) => file.toString }.toArray } def resolveJavaHome(id: String): java.io.File = { diff --git a/runner/launcher/src/mill/launcher/MillProcessLauncher.java b/runner/launcher/src/mill/launcher/MillProcessLauncher.java index 01bce96f905c..945cc021a18c 100644 --- a/runner/launcher/src/mill/launcher/MillProcessLauncher.java +++ b/runner/launcher/src/mill/launcher/MillProcessLauncher.java @@ -15,10 +15,8 @@ import java.util.function.Supplier; import java.util.stream.Stream; import mill.client.ClientUtil; -import mill.constants.BuildInfo; -import mill.constants.CodeGenConstants; -import mill.constants.DaemonFiles; -import mill.constants.EnvVars; +import mill.constants.*; +import scala.Option$; public class MillProcessLauncher { @@ -120,10 +118,12 @@ static List loadMillConfig(String key) throws Exception { Object conf = mill.launcher.ConfigReader.readYaml( buildFile, buildFile.getFileName().toString()); if (!(conf instanceof Map)) return new String[] {}; + @SuppressWarnings("unchecked") Map conf2 = (Map) conf; if (!conf2.containsKey(key)) return new String[] {}; if (conf2.get(key) instanceof List) { + @SuppressWarnings("unchecked") List list = (List) conf2.get(key); String[] arr = new String[list.size()]; for (int i = 0; i < arr.length; i++) { @@ -152,7 +152,15 @@ static List millOpts() throws Exception { } static String millJvmVersion() throws Exception { - List res = loadMillConfig("mill-jvm-version"); + return loadMillConfigSingleValue("mill-jvm-version"); + } + + static String millScalaVersion() throws Exception { + return loadMillConfigSingleValue("mill-scala-version"); + } + + static String loadMillConfigSingleValue(String key) throws Exception { + List res = loadMillConfig(key); if (res.isEmpty()) return null; else return res.get(0); } @@ -183,7 +191,7 @@ static String javaHome() throws Exception { if (jvmId != null) { final String jvmIdFinal = jvmId; javaHome = cachedComputedValue0( - "java-home", + CacheFiles.javaHome, jvmId, () -> new String[] {CoursierClient.resolveJavaHome(jvmIdFinal).getAbsolutePath()}, // Make sure we check to see if the saved java home exists before using @@ -228,10 +236,16 @@ static List millLaunchJvmCommand() throws Exception { // extra opts vmOptions.addAll(millJvmOpts()); + var maybeScalaVersion = millScalaVersion(); vmOptions.add("-XX:+HeapDumpOnOutOfMemoryError"); vmOptions.add("-cp"); + var classPathCacheKey = "mill:" + BuildInfo.millVersion + ",scala:" + + (maybeScalaVersion == null ? "default" : maybeScalaVersion); String[] runnerClasspath = cachedComputedValue0( - "resolve-runner", BuildInfo.millVersion, () -> CoursierClient.resolveMillDaemon(), arr -> { + CacheFiles.resolveRunner, + classPathCacheKey, + () -> CoursierClient.resolveMillDaemon(Option$.MODULE$.apply(maybeScalaVersion)), + arr -> { for (String s : arr) { if (!Files.exists(Paths.get(s))) return false; } @@ -246,10 +260,23 @@ static String[] cachedComputedValue(String name, String key, Supplier return cachedComputedValue0(name, key, block, arr -> true); } + /** + * Loads a value from the cache, or computes it if it's not in the cache, or re-computes it if it's + * in the cache but invalid. + *

+ * The cache is stored in the `out/mill-{name}` file and contains only a single key. + * + * @param name name of the cached value + * @param key key of the value in the cache. If the cache exists but the key doesn't match, the value + * will be re-computed. + * @param block block to compute the value + * @param validate function to validate the value. If the value is in cache but invalid, it will be + * re-computed. + */ static String[] cachedComputedValue0( String name, String key, Supplier block, Function validate) { try { - Path cacheFile = Paths.get(".").resolve(out).resolve("mill-" + name); + Path cacheFile = Paths.get(".").resolve(out).resolve(CacheFiles.filename(name)); String[] value = null; if (Files.exists(cacheFile)) { String[] savedInfo = Files.readString(cacheFile).split("\n"); diff --git a/website/docs/modules/ROOT/pages/cli/build-header.adoc b/website/docs/modules/ROOT/pages/cli/build-header.adoc index b907ab47ee57..adfa84ce8ca7 100644 --- a/website/docs/modules/ROOT/pages/cli/build-header.adoc +++ b/website/docs/modules/ROOT/pages/cli/build-header.adoc @@ -15,6 +15,7 @@ For example, a build header may look something like: //| mill-opts: ["--jobs=0.5C"] //| mill-jvm-version: temurin:11 //| mill-jvm-opts: ["-XX:NonProfiledCodeHeapSize=250m", "-XX:ReservedCodeCacheSize=500m"] +//| mill-scala-version: 3.7.2-RC1 //| # newlines are allowed too //| //| repositories: @@ -125,3 +126,20 @@ _build.mill_ Missing environment variables are converted to the empty string. +== mill-scala-version + +Mill allows you to specify the Scala version you want to use for your build +via a `mill-scala-version` key in the build header: + +_build.mill_ + +[source] +---- +//| mill-scala-version: 3.7.2-RC1 +---- + +This allows you to use newer versions of Scala than the version used by Mill +itself, as long as the version you use is backwards compatible. + +If `mill-scala-version` is not provided, Mill uses the version of Scala +used by Mill itself.