From 904e2cd04dbf164c75111041b2e4e1f4aac35204 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Fri, 30 Jan 2026 12:02:40 +0100 Subject: [PATCH 1/6] Migrate to Mill 1.1.2 (was 1.0.6) --- .mill-version | 2 +- build.mill.scala => build.mill | 7 +- mill.bat | 8 +- millw | 96 ++++++----- .../scala/build/tests/BuildOptionsTests.scala | 2 +- .../scala-cli-core/reflect-config.json | 66 +------- .../scala/cli/exportCmd/MillProject.scala | 2 +- .../scala/cli/packaging/NativeImage.scala | 3 +- .../scala/build/internals/NativeWrapper.scala | 154 +++++++++++------- .../integration/ExportMill10Tests212.scala | 3 - .../integration/ExportMill10Tests3Lts.scala | 3 - .../ExportMill10Tests3NextRc.scala | 3 - .../ExportMill10TestsDefault.scala | 3 - .../cli/integration/ExportMill1Tests212.scala | 3 + ...sts213.scala => ExportMill1Tests213.scala} | 2 +- .../integration/ExportMill1Tests3Lts.scala | 3 + .../integration/ExportMill1Tests3NextRc.scala | 3 + .../integration/ExportMill1TestsDefault.scala | 3 + .../ExportMillTestDefinitions.scala | 4 +- .../deps/{package.mill.scala => package.mill} | 17 +- project/{package.mill.scala => package.mill} | 0 .../{package.mill.scala => package.mill} | 2 +- .../{package.mill.scala => package.mill} | 0 .../{package.mill.scala => package.mill} | 0 .../{package.mill.scala => package.mill} | 0 website/docs/reference/cli-options.md | 2 +- 26 files changed, 193 insertions(+), 198 deletions(-) rename build.mill.scala => build.mill (99%) delete mode 100644 modules/integration/src/test/scala/scala/cli/integration/ExportMill10Tests212.scala delete mode 100644 modules/integration/src/test/scala/scala/cli/integration/ExportMill10Tests3Lts.scala delete mode 100644 modules/integration/src/test/scala/scala/cli/integration/ExportMill10Tests3NextRc.scala delete mode 100644 modules/integration/src/test/scala/scala/cli/integration/ExportMill10TestsDefault.scala create mode 100644 modules/integration/src/test/scala/scala/cli/integration/ExportMill1Tests212.scala rename modules/integration/src/test/scala/scala/cli/integration/{ExportMill10Tests213.scala => ExportMill1Tests213.scala} (89%) create mode 100644 modules/integration/src/test/scala/scala/cli/integration/ExportMill1Tests3Lts.scala create mode 100644 modules/integration/src/test/scala/scala/cli/integration/ExportMill1Tests3NextRc.scala create mode 100644 modules/integration/src/test/scala/scala/cli/integration/ExportMill1TestsDefault.scala rename project/deps/{package.mill.scala => package.mill} (97%) rename project/{package.mill.scala => package.mill} (100%) rename project/publish/{package.mill.scala => package.mill} (98%) rename project/settings/{package.mill.scala => package.mill} (100%) rename project/utils/{package.mill.scala => package.mill} (100%) rename project/website/{package.mill.scala => package.mill} (100%) diff --git a/.mill-version b/.mill-version index af0b7ddbff..45a1b3f445 100644 --- a/.mill-version +++ b/.mill-version @@ -1 +1 @@ -1.0.6 +1.1.2 diff --git a/build.mill.scala b/build.mill similarity index 99% rename from build.mill.scala rename to build.mill index 63b22fb554..aa388888b0 100644 --- a/build.mill.scala +++ b/build.mill @@ -1,3 +1,4 @@ +//| mill-jvm-version: system|17 //| mvnDeps: //| - io.github.alexarchambault.mill::mill-native-image::0.2.4 //| - io.github.alexarchambault.mill::mill-native-image-upload:0.2.4 @@ -67,7 +68,7 @@ trait CrossScalaDefaultToRunner extends CrossScalaDefault { self: Cross[?] => // Publish a bootstrapped, executable jar for a restricted environments object cliBootstrapped extends ScalaCliPublishModule { override def unmanagedClasspath: T[Seq[PathRef]] = - Task(cli(Scala.defaultInternal).nativeImageClassPath()) + Task(cli(Scala.defaultInternal).nativeImageClassPath().filter(ref => os.exists(ref.path))) override def jar: T[PathRef] = assembly() import mill.scalalib.Assembly @@ -864,7 +865,7 @@ trait Cli extends CrossSbtModule with ProtoBuildModule with CliLaunchers | def signingCliJvmVersion = ${Deps.Versions.signingCliJvmVersion} | def defaultMillVersion = "${BuildInfo.millVersion}" | def mill012Version = "${Deps.Versions.mill012Version}" - | def mill10Version = "${Deps.Versions.mill10Version}" + | def mill1Version = "${Deps.Versions.mill1Version}" | def defaultSbtVersion = "${Deps.Versions.sbtVersion}" | def defaultMavenVersion = "${Deps.Versions.mavenVersion}" | def defaultMavenScalaCompilerPluginVersion = "${Deps.Versions.mavenScalaCompilerPluginVersion}" @@ -1121,7 +1122,7 @@ trait CliIntegration extends SbtModule | def jmhGeneratorBytecodeModule = "${Deps.jmhGeneratorBytecode.dep.module.name.value}" | def defaultMillVersion = "${BuildInfo.millVersion}" | def mill012Version = "${Deps.Versions.mill012Version}" - | def mill10Version = "${Deps.Versions.mill10Version}" + | def mill1Version = "${Deps.Versions.mill1Version}" |} |""".stripMargin if (!os.isFile(dest) || os.read(dest) != code) diff --git a/mill.bat b/mill.bat index f7f143c8e6..ef5140a0aa 100755 --- a/mill.bat +++ b/mill.bat @@ -2,7 +2,7 @@ setlocal enabledelayedexpansion -if [!DEFAULT_MILL_VERSION!]==[] ( set "DEFAULT_MILL_VERSION=0.12.17" ) +if [!DEFAULT_MILL_VERSION!]==[] ( set "DEFAULT_MILL_VERSION=1.1.2" ) if [!MILL_GITHUB_RELEASE_CDN!]==[] ( set "MILL_GITHUB_RELEASE_CDN=" ) @@ -276,12 +276,8 @@ if [%~1%]==[--bsp] ( if [%~1%]==[--no-daemon] ( set MILL_FIRST_ARG=%1% ) else ( - if [%~1%]==[--repl] ( + if [%~1%]==[--help] ( set MILL_FIRST_ARG=%1% - ) else ( - if [%~1%]==[--help] ( - set MILL_FIRST_ARG=%1% - ) ) ) ) diff --git a/millw b/millw index 01b25fe528..bc04bdcd12 100755 --- a/millw +++ b/millw @@ -2,7 +2,7 @@ set -e -if [ -z "${DEFAULT_MILL_VERSION}" ] ; then DEFAULT_MILL_VERSION="0.12.17"; fi +if [ -z "${DEFAULT_MILL_VERSION}" ] ; then DEFAULT_MILL_VERSION="1.1.2"; fi if [ -z "${GITHUB_RELEASE_CDN}" ] ; then GITHUB_RELEASE_CDN=""; fi @@ -54,49 +54,67 @@ if [ -z "${MILL_FINAL_DOWNLOAD_FOLDER}" ] ; then MILL_FINAL_DOWNLOAD_FOLDER="${M MILL_NATIVE_SUFFIX="-native" MILL_JVM_SUFFIX="-jvm" -FULL_MILL_VERSION=$MILL_VERSION ARTIFACT_SUFFIX="" -set_artifact_suffix(){ - if [ "$(expr substr $(uname -s) 1 5 2>/dev/null)" = "Linux" ]; then + +# Check if GLIBC version is at least the required version +# Returns 0 (true) if GLIBC >= required version, 1 (false) otherwise +check_glibc_version() { + required_version="2.39" + required_major=$(echo "$required_version" | cut -d. -f1) + required_minor=$(echo "$required_version" | cut -d. -f2) + # Get GLIBC version from ldd --version (first line contains version like "ldd (GNU libc) 2.31") + glibc_version=$(ldd --version 2>/dev/null | head -n 1 | grep -oE '[0-9]+\.[0-9]+$' || echo "") + if [ -z "$glibc_version" ]; then + # If we can't determine GLIBC version, assume it's too old + return 1 + fi + glibc_major=$(echo "$glibc_version" | cut -d. -f1) + glibc_minor=$(echo "$glibc_version" | cut -d. -f2) + if [ "$glibc_major" -gt "$required_major" ]; then + return 0 + elif [ "$glibc_major" -eq "$required_major" ] && [ "$glibc_minor" -ge "$required_minor" ]; then + return 0 + else + return 1 + fi +} + +set_artifact_suffix() { + if [ "$(uname -s 2>/dev/null | cut -c 1-5)" = "Linux" ]; then + # Native binaries require new enough GLIBC; fall back to JVM launcher if older + if ! check_glibc_version; then + return + fi if [ "$(uname -m)" = "aarch64" ]; then ARTIFACT_SUFFIX="-native-linux-aarch64" else ARTIFACT_SUFFIX="-native-linux-amd64"; fi elif [ "$(uname)" = "Darwin" ]; then if [ "$(uname -m)" = "arm64" ]; then ARTIFACT_SUFFIX="-native-mac-aarch64" else ARTIFACT_SUFFIX="-native-mac-amd64"; fi else - echo "This native mill launcher supports only Linux and macOS." 1>&2 - exit 1 + echo "This native mill launcher supports only Linux and macOS." 1>&2 + exit 1 fi } case "$MILL_VERSION" in - *"$MILL_NATIVE_SUFFIX") - MILL_VERSION=${MILL_VERSION%"$MILL_NATIVE_SUFFIX"} - set_artifact_suffix - ;; + *"$MILL_NATIVE_SUFFIX") + MILL_VERSION=${MILL_VERSION%"$MILL_NATIVE_SUFFIX"} + set_artifact_suffix + ;; - *"$MILL_JVM_SUFFIX") + *"$MILL_JVM_SUFFIX") MILL_VERSION=${MILL_VERSION%"$MILL_JVM_SUFFIX"} - ;; - - *) - case "$MILL_VERSION" in - 0.1.*) ;; - 0.2.*) ;; - 0.3.*) ;; - 0.4.*) ;; - 0.5.*) ;; - 0.6.*) ;; - 0.7.*) ;; - 0.8.*) ;; - 0.9.*) ;; - 0.10.*) ;; - 0.11.*) ;; - 0.12.*) ;; - *) - set_artifact_suffix - esac - ;; + ;; + + *) + case "$MILL_VERSION" in + 0.1.* | 0.2.* | 0.3.* | 0.4.* | 0.5.* | 0.6.* | 0.7.* | 0.8.* | 0.9.* | 0.10.* | 0.11.* | 0.12.*) + ;; + *) + set_artifact_suffix + ;; + esac + ;; esac MILL="${MILL_FINAL_DOWNLOAD_FOLDER}/$MILL_VERSION$ARTIFACT_SUFFIX" @@ -104,11 +122,11 @@ MILL="${MILL_FINAL_DOWNLOAD_FOLDER}/$MILL_VERSION$ARTIFACT_SUFFIX" # If not already downloaded, download it if [ ! -s "${MILL}" ] || [ "$MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT" = "1" ] ; then case $MILL_VERSION in - 0.0.* | 0.1.* | 0.2.* | 0.3.* | 0.4.* ) + 0.0.* | 0.1.* | 0.2.* | 0.3.* | 0.4.*) MILL_DOWNLOAD_SUFFIX="" MILL_DOWNLOAD_FROM_MAVEN=0 ;; - 0.5.* | 0.6.* | 0.7.* | 0.8.* | 0.9.* | 0.10.* | 0.11.0-M* ) + 0.5.* | 0.6.* | 0.7.* | 0.8.* | 0.9.* | 0.10.* | 0.11.0-M*) MILL_DOWNLOAD_SUFFIX="-assembly" MILL_DOWNLOAD_FROM_MAVEN=0 ;; @@ -118,13 +136,13 @@ if [ ! -s "${MILL}" ] || [ "$MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT" = "1" ] ; then ;; esac case $MILL_VERSION in - 0.12.0 | 0.12.1 | 0.12.2 | 0.12.3 | 0.12.4 | 0.12.5 | 0.12.6 | 0.12.7 | 0.12.8 | 0.12.9 | 0.12.10 | 0.12.11 ) + 0.12.0 | 0.12.1 | 0.12.2 | 0.12.3 | 0.12.4 | 0.12.5 | 0.12.6 | 0.12.7 | 0.12.8 | 0.12.9 | 0.12.10 | 0.12.11) MILL_DOWNLOAD_EXT="jar" ;; - 0.12.* ) + 0.12.*) MILL_DOWNLOAD_EXT="exe" ;; - 0.* ) + 0.*) MILL_DOWNLOAD_EXT="jar" ;; *) @@ -145,8 +163,8 @@ if [ ! -s "${MILL}" ] || [ "$MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT" = "1" ] ; then if [ "$MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT" = "1" ] ; then - echo $MILL_DOWNLOAD_URL - echo $MILL + echo "$MILL_DOWNLOAD_URL" + echo "$MILL" exit 0 fi @@ -163,7 +181,7 @@ if [ ! -s "${MILL}" ] || [ "$MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT" = "1" ] ; then fi MILL_FIRST_ARG="" -if [ "$1" = "--bsp" ] || [ "${1#"-i"}" != "$1" ] || [ "$1" = "--interactive" ] || [ "$1" = "--no-server" ] || [ "$1" = "--no-daemon" ] || [ "$1" = "--repl" ] || [ "$1" = "--help" ] ; then +if [ "$1" = "--bsp" ] || [ "${1#"-i"}" != "$1" ] || [ "$1" = "--interactive" ] || [ "$1" = "--no-server" ] || [ "$1" = "--no-daemon" ] || [ "$1" = "--help" ] ; then # Need to preserve the first position of those listed options MILL_FIRST_ARG=$1 shift diff --git a/modules/build/src/test/scala/scala/build/tests/BuildOptionsTests.scala b/modules/build/src/test/scala/scala/build/tests/BuildOptionsTests.scala index bdb17e9409..34712d5c7c 100644 --- a/modules/build/src/test/scala/scala/build/tests/BuildOptionsTests.scala +++ b/modules/build/src/test/scala/scala/build/tests/BuildOptionsTests.scala @@ -176,7 +176,7 @@ class BuildOptionsTests extends TestUtil.ScalaCliBuildSuite { ) } - test("Scala 2.12.9-bin-1111111 shows No Valid Scala Version Error") { + test("Scala 2.12.9-bin-1111111 shows No Valid Scala Version Error".flaky) { val options = BuildOptions( scalaOptions = ScalaOptions( diff --git a/modules/cli/src/main/resources/META-INF/native-image/org.virtuslab/scala-cli-core/reflect-config.json b/modules/cli/src/main/resources/META-INF/native-image/org.virtuslab/scala-cli-core/reflect-config.json index c4cee53fdd..8051196c9e 100644 --- a/modules/cli/src/main/resources/META-INF/native-image/org.virtuslab/scala-cli-core/reflect-config.json +++ b/modules/cli/src/main/resources/META-INF/native-image/org.virtuslab/scala-cli-core/reflect-config.json @@ -150,6 +150,9 @@ }, { "name": "ch.epfl.scala.bsp4j.DebugProvider", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, "allDeclaredFields": true }, { @@ -180,13 +183,6 @@ "allDeclaredMethods": true, "allDeclaredFields": true }, - { - "name": "ch.epfl.scala.bsp4j.DependencyModulesDataKind", - "allDeclaredConstructors": true, - "allPublicConstructors": true, - "allDeclaredMethods": true, - "allDeclaredFields": true - }, { "name": "ch.epfl.scala.bsp4j.DependencyModulesItem", "allDeclaredConstructors": true, @@ -257,13 +253,6 @@ "allDeclaredMethods": true, "allDeclaredFields": true }, - { - "name": "ch.epfl.scala.bsp4j.FileChangeType", - "allDeclaredConstructors": true, - "allPublicConstructors": true, - "allDeclaredMethods": true, - "allDeclaredFields": true - }, { "name": "ch.epfl.scala.bsp4j.InitializeBuildParams", "allDeclaredConstructors": true, @@ -687,13 +676,6 @@ "allDeclaredMethods": true, "allDeclaredFields": true }, - { - "name": "ch.epfl.scala.bsp4j.TaskDataKind", - "allDeclaredConstructors": true, - "allPublicConstructors": true, - "allDeclaredMethods": true, - "allDeclaredFields": true - }, { "name": "ch.epfl.scala.bsp4j.TaskFinishParams", "allDeclaredConstructors": true, @@ -1232,13 +1214,6 @@ "allDeclaredMethods": true, "allDeclaredFields": true }, - { - "name": "scala.build.bsp.LoggingBuildServerAll", - "allDeclaredConstructors": true, - "allPublicConstructors": true, - "allDeclaredMethods": true, - "allDeclaredFields": true - }, { "name": "scala.build.bsp.LoggingJavaBuildServer", "allDeclaredConstructors": true, @@ -1267,41 +1242,6 @@ "allDeclaredMethods": true, "allDeclaredFields": true }, - { - "name": "scala.build.bsp.ScalaScriptBuildServer", - "allDeclaredConstructors": true, - "allPublicConstructors": true, - "allDeclaredMethods": true, - "allDeclaredFields": true - }, - { - "name": "scala.build.bsp.WrappedSourceItem", - "allDeclaredConstructors": true, - "allPublicConstructors": true, - "allDeclaredMethods": true, - "allDeclaredFields": true - }, - { - "name": "scala.build.bsp.WrappedSourcesItem", - "allDeclaredConstructors": true, - "allPublicConstructors": true, - "allDeclaredMethods": true, - "allDeclaredFields": true - }, - { - "name": "scala.build.bsp.WrappedSourcesParams", - "allDeclaredConstructors": true, - "allPublicConstructors": true, - "allDeclaredMethods": true, - "allDeclaredFields": true - }, - { - "name": "scala.build.bsp.WrappedSourcesResult", - "allDeclaredConstructors": true, - "allPublicConstructors": true, - "allDeclaredMethods": true, - "allDeclaredFields": true - }, { "name": "scala.build.bsp.protocol.TextEdit", "allDeclaredConstructors": true, diff --git a/modules/cli/src/main/scala/scala/cli/exportCmd/MillProject.scala b/modules/cli/src/main/scala/scala/cli/exportCmd/MillProject.scala index 8008e006ea..14573d9736 100644 --- a/modules/cli/src/main/scala/scala/cli/exportCmd/MillProject.scala +++ b/modules/cli/src/main/scala/scala/cli/exportCmd/MillProject.scala @@ -177,7 +177,7 @@ final case class MillProject( os.write(path0, content, createFolders = true) } - val outputBuildFile = if isMill1OrNewer then dir / "build.mill.scala" else dir / "build.sc" + val outputBuildFile = if isMill1OrNewer then dir / "build.mill" else dir / "build.sc" os.write(outputBuildFile, buildFileContent.getBytes(charSet)) } } diff --git a/modules/cli/src/main/scala/scala/cli/packaging/NativeImage.scala b/modules/cli/src/main/scala/scala/cli/packaging/NativeImage.scala index e4e8d66494..d02483b57c 100644 --- a/modules/cli/src/main/scala/scala/cli/packaging/NativeImage.scala +++ b/modules/cli/src/main/scala/scala/cli/packaging/NativeImage.scala @@ -50,7 +50,8 @@ object NativeImage { )( f: os.Path => T ): T = - if (Properties.isWin && currentHome.toString.length >= 180) { + // Lower threshold (was 180) to ensure native-image's internal paths don't exceed 260-char limit + if (Properties.isWin && currentHome.toString.length >= 90) { val (driveLetter, newHome) = getShortenedPath(currentHome, logger) val savedCodepage: String = getCodePage(logger) val result = diff --git a/modules/core/src/main/scala/scala/build/internals/NativeWrapper.scala b/modules/core/src/main/scala/scala/build/internals/NativeWrapper.scala index 026e1573bc..378ca904f7 100755 --- a/modules/core/src/main/scala/scala/build/internals/NativeWrapper.scala +++ b/modules/core/src/main/scala/scala/build/internals/NativeWrapper.scala @@ -17,6 +17,11 @@ import scala.util.Using object MsvcEnvironment { + // Lower threshold to ensure native-image's internal paths (which can add 100-130+ chars + // for deeply nested source files) don't exceed Windows 260-char MAX_PATH limit. + // Native-image creates paths like: native-sources\graal\com\oracle\svm\...\Target_ClassName.c + private val pathLengthLimit = 90 + /* * Call `native-image.exe` with captured vcvarsall.bat environment. * @return process exit code. @@ -26,69 +31,104 @@ object MsvcEnvironment { workingDir: os.Path, logger: Logger ): Int = { - val vcvOpt = vcvarsOpt(logger) - vcvOpt match { - case None => - logger.debug(s"not found: vcvars64.bat") - -1 - case Some(vcvars) => - logger.debug(s"Using vcvars script $vcvars") - - val msvcEnv: Map[String, String] = captureVcvarsEnv(vcvars, workingDir, logger) - - // show aliased drive map - getSubstMappings.foreach((k, v) => logger.message(s"substMap $k: -> $v")) - - val finalEnv = - msvcEnv + - ("GRAALVM_ARGUMENT_VECTOR_PROGRAM_NAME" -> "native-image") - - logger.debug(s"msvc PATH entries:") - finalEnv.getOrElse("PATH", "").split(";").toSeq.foreach { entry => - logger.debug(s"$entry;") - } - Seq( - "VCToolsInstallDir", - "VCToolsVersion", - "VCINSTALLDIR", - "WindowsSdkDir", - "WindowsSdkVersion", - "INCLUDE", - "LIB", - "LIBPATH" - ).foreach { key => - logger.debug(s"""$key=${msvcEnv.getOrElse(key, "")}""") - } + // Use shortened working dir when path is too long; otherwise vcvars/native-image run with + // long cwd and GraalVM's "automatically set up Windows build environment" hits 260-char limit. + val (actualWorkingDir, driveToUnalias) = + if (workingDir.toString.length >= pathLengthLimit) { + val (driveLetter, shortPath) = getShortenedPath(workingDir, logger) + (shortPath, Some(driveLetter)) + } + else + (workingDir, None) - // Replace native-image.cmd with native-image.exe, if applicable - val updatedCommand: Seq[String] = - command.headOption match { - case Some(cmd) if cmd.toLowerCase.endsWith("native-image.cmd") => - val cmdPath = os.Path(cmd, os.pwd) - val graalHome = cmdPath / os.up / os.up - resolveNativeImage(graalHome) match { - case Some(exe) => - exe.toString +: command.tail - case None => - command // fall back to the .cmd wrapper + try { + val vcvOpt = vcvarsOpt(logger) + vcvOpt match { + case None => + logger.debug(s"not found: vcvars64.bat") + -1 + case Some(vcvars) => + logger.debug(s"Using vcvars script $vcvars") + + val msvcEnv: Map[String, String] = captureVcvarsEnv(vcvars, actualWorkingDir, logger) + + // Validate that critical MSVC variables were captured + // VSINSTALLDIR is what GraalVM native-image checks to detect pre-configured MSVC + val requiredVars = + Seq("VSINSTALLDIR", "VCINSTALLDIR", "VCToolsInstallDir", "INCLUDE", "LIB") + val missingVars = requiredVars.filterNot(msvcEnv.contains) + + if msvcEnv.isEmpty then + logger.error("MSVC environment capture failed - no environment variables captured") + logger.error("Please ensure Visual Studio 2022 with C++ build tools is installed") + logger.error(s"vcvars script used: $vcvars") + logger.error(s"working directory: $actualWorkingDir") + -1 + else if missingVars.nonEmpty then + logger.error(s"MSVC environment incomplete - missing: ${missingVars.mkString(", ")}") + logger.error( + "Please ensure Visual Studio 2022 with C++ build tools is properly installed" + ) + logger.error(s"vcvars script used: $vcvars") + logger.error(s"Captured environment has ${msvcEnv.size} variables") + -1 + else + // show aliased drive map + getSubstMappings.foreach((k, v) => logger.message(s"substMap $k: -> $v")) + + val finalEnv = + msvcEnv + + ("GRAALVM_ARGUMENT_VECTOR_PROGRAM_NAME" -> "native-image") + + logger.debug(s"msvc PATH entries:") + finalEnv.getOrElse("PATH", "").split(";").toSeq.foreach { entry => + logger.debug(s"$entry;") + } + Seq( + "VCToolsInstallDir", + "VCToolsVersion", + "VCINSTALLDIR", + "WindowsSdkDir", + "WindowsSdkVersion", + "INCLUDE", + "LIB", + "LIBPATH" + ).foreach { key => + logger.debug(s"""$key=${msvcEnv.getOrElse(key, "")}""") + } + + // Replace native-image.cmd with native-image.exe, if applicable + val updatedCommand: Seq[String] = + command.headOption match { + case Some(cmd) if cmd.toLowerCase.endsWith("native-image.cmd") => + val cmdPath = os.Path(cmd, os.pwd) + val graalHome = cmdPath / os.up / os.up + resolveNativeImage(graalHome) match { + case Some(exe) => + exe.toString +: command.tail + case None => + command // fall back to the .cmd wrapper + } + case _ => + command } - case _ => - command - } - logger.debug(s"native-image w/args: $updatedCommand") + logger.debug(s"native-image w/args: $updatedCommand") - val result = - os.proc(updatedCommand) - .call( - cwd = workingDir, - env = finalEnv, - stdout = os.Inherit, - stderr = os.Inherit - ) + val result = + os.proc(updatedCommand) + .call( + cwd = actualWorkingDir, + env = finalEnv, + stdout = os.Inherit, + stderr = os.Inherit + ) - result.exitCode + result.exitCode + } } + finally + driveToUnalias.foreach(unaliasDriveLetter) } // ========================= diff --git a/modules/integration/src/test/scala/scala/cli/integration/ExportMill10Tests212.scala b/modules/integration/src/test/scala/scala/cli/integration/ExportMill10Tests212.scala deleted file mode 100644 index 32b57506b1..0000000000 --- a/modules/integration/src/test/scala/scala/cli/integration/ExportMill10Tests212.scala +++ /dev/null @@ -1,3 +0,0 @@ -package scala.cli.integration - -class ExportMill10Tests212 extends ExportMillTestDefinitions with Test212 with TestMill10 diff --git a/modules/integration/src/test/scala/scala/cli/integration/ExportMill10Tests3Lts.scala b/modules/integration/src/test/scala/scala/cli/integration/ExportMill10Tests3Lts.scala deleted file mode 100644 index a6b9df90a0..0000000000 --- a/modules/integration/src/test/scala/scala/cli/integration/ExportMill10Tests3Lts.scala +++ /dev/null @@ -1,3 +0,0 @@ -package scala.cli.integration - -class ExportMill10Tests3Lts extends ExportMillTestDefinitions with Test3Lts with TestMill10 diff --git a/modules/integration/src/test/scala/scala/cli/integration/ExportMill10Tests3NextRc.scala b/modules/integration/src/test/scala/scala/cli/integration/ExportMill10Tests3NextRc.scala deleted file mode 100644 index 6983894c78..0000000000 --- a/modules/integration/src/test/scala/scala/cli/integration/ExportMill10Tests3NextRc.scala +++ /dev/null @@ -1,3 +0,0 @@ -package scala.cli.integration - -class ExportMill10Tests3NextRc extends ExportMillTestDefinitions with Test3NextRc with TestMill10 diff --git a/modules/integration/src/test/scala/scala/cli/integration/ExportMill10TestsDefault.scala b/modules/integration/src/test/scala/scala/cli/integration/ExportMill10TestsDefault.scala deleted file mode 100644 index 50287d035e..0000000000 --- a/modules/integration/src/test/scala/scala/cli/integration/ExportMill10TestsDefault.scala +++ /dev/null @@ -1,3 +0,0 @@ -package scala.cli.integration - -class ExportMill10TestsDefault extends ExportMillTestDefinitions with TestDefault with TestMill10 diff --git a/modules/integration/src/test/scala/scala/cli/integration/ExportMill1Tests212.scala b/modules/integration/src/test/scala/scala/cli/integration/ExportMill1Tests212.scala new file mode 100644 index 0000000000..0e5a41f0ec --- /dev/null +++ b/modules/integration/src/test/scala/scala/cli/integration/ExportMill1Tests212.scala @@ -0,0 +1,3 @@ +package scala.cli.integration + +class ExportMill1Tests212 extends ExportMillTestDefinitions with Test212 with TestMill1 diff --git a/modules/integration/src/test/scala/scala/cli/integration/ExportMill10Tests213.scala b/modules/integration/src/test/scala/scala/cli/integration/ExportMill1Tests213.scala similarity index 89% rename from modules/integration/src/test/scala/scala/cli/integration/ExportMill10Tests213.scala rename to modules/integration/src/test/scala/scala/cli/integration/ExportMill1Tests213.scala index a31bf6d49d..12021be121 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/ExportMill10Tests213.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/ExportMill1Tests213.scala @@ -1,6 +1,6 @@ package scala.cli.integration -class ExportMill10Tests213 extends ExportMillTestDefinitions with Test213 with TestMill10 { +class ExportMill1Tests213 extends ExportMillTestDefinitions with Test213 with TestMill1 { if runExportTests then { test(s"scalac options$commonTestDescriptionSuffix") { simpleTest( diff --git a/modules/integration/src/test/scala/scala/cli/integration/ExportMill1Tests3Lts.scala b/modules/integration/src/test/scala/scala/cli/integration/ExportMill1Tests3Lts.scala new file mode 100644 index 0000000000..34084736d7 --- /dev/null +++ b/modules/integration/src/test/scala/scala/cli/integration/ExportMill1Tests3Lts.scala @@ -0,0 +1,3 @@ +package scala.cli.integration + +class ExportMill1Tests3Lts extends ExportMillTestDefinitions with Test3Lts with TestMill1 diff --git a/modules/integration/src/test/scala/scala/cli/integration/ExportMill1Tests3NextRc.scala b/modules/integration/src/test/scala/scala/cli/integration/ExportMill1Tests3NextRc.scala new file mode 100644 index 0000000000..b5a209a2e8 --- /dev/null +++ b/modules/integration/src/test/scala/scala/cli/integration/ExportMill1Tests3NextRc.scala @@ -0,0 +1,3 @@ +package scala.cli.integration + +class ExportMill1Tests3NextRc extends ExportMillTestDefinitions with Test3NextRc with TestMill1 diff --git a/modules/integration/src/test/scala/scala/cli/integration/ExportMill1TestsDefault.scala b/modules/integration/src/test/scala/scala/cli/integration/ExportMill1TestsDefault.scala new file mode 100644 index 0000000000..b38b52c016 --- /dev/null +++ b/modules/integration/src/test/scala/scala/cli/integration/ExportMill1TestsDefault.scala @@ -0,0 +1,3 @@ +package scala.cli.integration + +class ExportMill1TestsDefault extends ExportMillTestDefinitions with TestDefault with TestMill1 diff --git a/modules/integration/src/test/scala/scala/cli/integration/ExportMillTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/ExportMillTestDefinitions.scala index 3b88723c1c..fa241b2763 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/ExportMillTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/ExportMillTestDefinitions.scala @@ -118,6 +118,6 @@ sealed trait TestMillVersion: trait TestMill012 extends TestMillVersion: self: ExportMillTestDefinitions => override def millVersion: String = Constants.mill012Version -trait TestMill10 extends TestMillVersion: +trait TestMill1 extends TestMillVersion: self: ExportMillTestDefinitions => - override def millVersion: String = Constants.mill10Version + override def millVersion: String = Constants.mill1Version diff --git a/project/deps/package.mill.scala b/project/deps/package.mill similarity index 97% rename from project/deps/package.mill.scala rename to project/deps/package.mill index 7ed29bfe81..43cacda018 100644 --- a/project/deps/package.mill.scala +++ b/project/deps/package.mill @@ -148,15 +148,14 @@ object Deps { def bloop = "2.0.17" def sbtVersion = "1.12.2" def mill012Version = "0.12.17" - def mill10Version = - if (BuildInfo.millVersion.startsWith("1.0.")) BuildInfo.millVersion else "1.0.6" - def mavenVersion = "3.8.1" - def mavenScalaCompilerPluginVersion = "4.9.1" - def mavenExecPluginVersion = "3.3.0" - def mavenAppArtifactId = "maven-app" - def mavenAppGroupId = "com.example" - def mavenAppVersion = "0.1-SNAPSHOT" - def scalafix = "0.14.4" + def mill1Version = BuildInfo.millVersion + def mavenVersion = "3.8.1" + def mavenScalaCompilerPluginVersion = "4.9.1" + def mavenExecPluginVersion = "3.3.0" + def mavenAppArtifactId = "maven-app" + def mavenAppGroupId = "com.example" + def mavenAppVersion = "0.1-SNAPSHOT" + def scalafix = "0.14.4" } // DO NOT hardcode a Scala version in this dependency string diff --git a/project/package.mill.scala b/project/package.mill similarity index 100% rename from project/package.mill.scala rename to project/package.mill diff --git a/project/publish/package.mill.scala b/project/publish/package.mill similarity index 98% rename from project/publish/package.mill.scala rename to project/publish/package.mill index 19bbd20dcd..f258a063b0 100644 --- a/project/publish/package.mill.scala +++ b/project/publish/package.mill @@ -1,7 +1,7 @@ package build.project.publish import build.project.settings -import com.lumidion.sonatype.central.client.core.{PublishingType, SonatypeCredentials} +import mill.javalib.publish.{PublishingType, SonatypeCredentials} import settings.{PublishLocalNoFluff, workspaceDirName} import mill.* import mill.javalib.publish.Artifact diff --git a/project/settings/package.mill.scala b/project/settings/package.mill similarity index 100% rename from project/settings/package.mill.scala rename to project/settings/package.mill diff --git a/project/utils/package.mill.scala b/project/utils/package.mill similarity index 100% rename from project/utils/package.mill.scala rename to project/utils/package.mill diff --git a/project/website/package.mill.scala b/project/website/package.mill similarity index 100% rename from project/website/package.mill.scala rename to project/website/package.mill diff --git a/website/docs/reference/cli-options.md b/website/docs/reference/cli-options.md index 6862b86ad8..a88c0dbf73 100644 --- a/website/docs/reference/cli-options.md +++ b/website/docs/reference/cli-options.md @@ -388,7 +388,7 @@ Version of SBT to be used for the export (1.12.2 by default) ### `--mill-version` -Version of Mill to be used for the export (1.0.6 by default) +Version of Mill to be used for the export (1.1.2 by default) ### `--mvn-version` From 29a8edb81412dcb1b808ac53bdfbc9be2f57daa7 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Fri, 30 Jan 2026 13:26:34 +0100 Subject: [PATCH 2/6] . --- .../scala/build/internals/NativeWrapper.scala | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/modules/core/src/main/scala/scala/build/internals/NativeWrapper.scala b/modules/core/src/main/scala/scala/build/internals/NativeWrapper.scala index 378ca904f7..ea34d4a2a8 100755 --- a/modules/core/src/main/scala/scala/build/internals/NativeWrapper.scala +++ b/modules/core/src/main/scala/scala/build/internals/NativeWrapper.scala @@ -48,7 +48,8 @@ object MsvcEnvironment { logger.debug(s"not found: vcvars64.bat") -1 case Some(vcvars) => - logger.debug(s"Using vcvars script $vcvars") + logger.message(s"Using vcvars script: $vcvars") + logger.message(s"Working directory: $actualWorkingDir") val msvcEnv: Map[String, String] = captureVcvarsEnv(vcvars, actualWorkingDir, logger) @@ -76,6 +77,49 @@ object MsvcEnvironment { // show aliased drive map getSubstMappings.foreach((k, v) => logger.message(s"substMap $k: -> $v")) + // Log critical MSVC variables at message level for diagnosis + val vsInstallDir = msvcEnv.get("VSINSTALLDIR").orElse(msvcEnv.get("vsinstalldir")) + val vcInstallDir = msvcEnv.get("VCINSTALLDIR").orElse(msvcEnv.get("vcinstalldir")) + logger.message(s"VSINSTALLDIR=${vsInstallDir.getOrElse("")}") + logger.message(s"VCINSTALLDIR=${vcInstallDir.getOrElse("")}") + logger.message(s"Environment has ${msvcEnv.size} variables") + + // Verify VSINSTALLDIR path exists + vsInstallDir.foreach { dir => + val path = os.Path(dir.stripSuffix("\\"), os.pwd) + logger.message(s"VSINSTALLDIR exists: ${os.exists(path)}") + } + + // Log PATH - check both casings and show what we find + val pathValue = + msvcEnv.get("PATH").orElse(msvcEnv.get("Path")).orElse(msvcEnv.get("path")) + logger.message( + s"PATH found: ${pathValue.isDefined}, length: ${pathValue.map(_.length).getOrElse(0)}" + ) + pathValue.foreach { pv => + val pathEntries = pv.split(";") + val msvcPathEntries = pathEntries.filter(_.toLowerCase.contains("msvc")) + val vcPathEntries = pathEntries.filter(_.toLowerCase.contains("\\vc\\")) + logger.message( + s"MSVC PATH entries: ${msvcPathEntries.length}, VC entries: ${vcPathEntries.length}" + ) + if (vcPathEntries.nonEmpty) + logger.message(s"First VC PATH: ${vcPathEntries.head}") + } + // Also check what keys look like PATH + val pathLikeKeys = msvcEnv.keys.filter(_.toLowerCase.contains("path")).toSeq + logger.message(s"PATH-like keys in env: ${pathLikeKeys.mkString(", ")}") + + // Direct check of TreeMap case-insensitive behavior + pathLikeKeys.headOption.foreach { actualKey => + val directGet = msvcEnv.get(actualKey) + val upperGet = msvcEnv.get(actualKey.toUpperCase) + val lowerGet = msvcEnv.get(actualKey.toLowerCase) + logger.message( + s"TreeMap lookup test - direct: ${directGet.isDefined}, upper: ${upperGet.isDefined}, lower: ${lowerGet.isDefined}" + ) + } + val finalEnv = msvcEnv + ("GRAALVM_ARGUMENT_VECTOR_PROGRAM_NAME" -> "native-image") @@ -113,6 +157,7 @@ object MsvcEnvironment { command } + logger.message(s"Running: ${updatedCommand.head}") logger.debug(s"native-image w/args: $updatedCommand") val result = @@ -190,6 +235,13 @@ object MsvcEnvironment { case _ => None } + // Debug: log if PATH was found during parsing + val pathInEnv = envLines.find(_.toLowerCase.startsWith("path=")) + if pathInEnv.isEmpty then + logger.message("WARNING: PATH not found in vcvars output!") + logger.message(s"First 10 env lines: ${envLines.take(10).mkString("; ")}") + logger.message(s"Last 10 env lines: ${envLines.takeRight(10).mkString("; ")}") + if logger.verbosity > 0 then debugLines.foreach(dbg => logger.debug(s"$dbg")) debugLines.find(_.contains("Writing post-execution environment to ")) match { From 7f56d7f8943271b9412894a090fb47eae7ae628e Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Thu, 12 Feb 2026 16:47:01 +0100 Subject: [PATCH 3/6] . --- .../scala/build/internals/NativeWrapper.scala | 186 ++++++------------ 1 file changed, 59 insertions(+), 127 deletions(-) diff --git a/modules/core/src/main/scala/scala/build/internals/NativeWrapper.scala b/modules/core/src/main/scala/scala/build/internals/NativeWrapper.scala index ea34d4a2a8..f9bc546b19 100755 --- a/modules/core/src/main/scala/scala/build/internals/NativeWrapper.scala +++ b/modules/core/src/main/scala/scala/build/internals/NativeWrapper.scala @@ -31,9 +31,10 @@ object MsvcEnvironment { workingDir: os.Path, logger: Logger ): Int = { - // Use shortened working dir when path is too long; otherwise vcvars/native-image run with - // long cwd and GraalVM's "automatically set up Windows build environment" hits 260-char limit. - val (actualWorkingDir, driveToUnalias) = + // Shorten the working dir for native-image only (it creates deeply nested internal paths + // that can exceed Windows 260-char MAX_PATH). We must NOT use the SUBST drive for vcvars + // capture: running vcvars64.bat with a SUBST-drive CWD causes its PATH setup to silently fail. + val (nativeImageWorkDir, driveToUnalias) = if (workingDir.toString.length >= pathLengthLimit) { val (driveLetter, shortPath) = getShortenedPath(workingDir, logger) (shortPath, Some(driveLetter)) @@ -48,128 +49,66 @@ object MsvcEnvironment { logger.debug(s"not found: vcvars64.bat") -1 case Some(vcvars) => - logger.message(s"Using vcvars script: $vcvars") - logger.message(s"Working directory: $actualWorkingDir") - - val msvcEnv: Map[String, String] = captureVcvarsEnv(vcvars, actualWorkingDir, logger) - - // Validate that critical MSVC variables were captured - // VSINSTALLDIR is what GraalVM native-image checks to detect pre-configured MSVC - val requiredVars = - Seq("VSINSTALLDIR", "VCINSTALLDIR", "VCToolsInstallDir", "INCLUDE", "LIB") - val missingVars = requiredVars.filterNot(msvcEnv.contains) - - if msvcEnv.isEmpty then - logger.error("MSVC environment capture failed - no environment variables captured") - logger.error("Please ensure Visual Studio 2022 with C++ build tools is installed") - logger.error(s"vcvars script used: $vcvars") - logger.error(s"working directory: $actualWorkingDir") - -1 - else if missingVars.nonEmpty then - logger.error(s"MSVC environment incomplete - missing: ${missingVars.mkString(", ")}") - logger.error( - "Please ensure Visual Studio 2022 with C++ build tools is properly installed" - ) - logger.error(s"vcvars script used: $vcvars") - logger.error(s"Captured environment has ${msvcEnv.size} variables") - -1 - else - // show aliased drive map - getSubstMappings.foreach((k, v) => logger.message(s"substMap $k: -> $v")) - - // Log critical MSVC variables at message level for diagnosis - val vsInstallDir = msvcEnv.get("VSINSTALLDIR").orElse(msvcEnv.get("vsinstalldir")) - val vcInstallDir = msvcEnv.get("VCINSTALLDIR").orElse(msvcEnv.get("vcinstalldir")) - logger.message(s"VSINSTALLDIR=${vsInstallDir.getOrElse("")}") - logger.message(s"VCINSTALLDIR=${vcInstallDir.getOrElse("")}") - logger.message(s"Environment has ${msvcEnv.size} variables") - - // Verify VSINSTALLDIR path exists - vsInstallDir.foreach { dir => - val path = os.Path(dir.stripSuffix("\\"), os.pwd) - logger.message(s"VSINSTALLDIR exists: ${os.exists(path)}") - } + logger.debug(s"Using vcvars script $vcvars") - // Log PATH - check both casings and show what we find - val pathValue = - msvcEnv.get("PATH").orElse(msvcEnv.get("Path")).orElse(msvcEnv.get("path")) - logger.message( - s"PATH found: ${pathValue.isDefined}, length: ${pathValue.map(_.length).getOrElse(0)}" - ) - pathValue.foreach { pv => - val pathEntries = pv.split(";") - val msvcPathEntries = pathEntries.filter(_.toLowerCase.contains("msvc")) - val vcPathEntries = pathEntries.filter(_.toLowerCase.contains("\\vc\\")) - logger.message( - s"MSVC PATH entries: ${msvcPathEntries.length}, VC entries: ${vcPathEntries.length}" - ) - if (vcPathEntries.nonEmpty) - logger.message(s"First VC PATH: ${vcPathEntries.head}") - } - // Also check what keys look like PATH - val pathLikeKeys = msvcEnv.keys.filter(_.toLowerCase.contains("path")).toSeq - logger.message(s"PATH-like keys in env: ${pathLikeKeys.mkString(", ")}") - - // Direct check of TreeMap case-insensitive behavior - pathLikeKeys.headOption.foreach { actualKey => - val directGet = msvcEnv.get(actualKey) - val upperGet = msvcEnv.get(actualKey.toUpperCase) - val lowerGet = msvcEnv.get(actualKey.toLowerCase) - logger.message( - s"TreeMap lookup test - direct: ${directGet.isDefined}, upper: ${upperGet.isDefined}, lower: ${lowerGet.isDefined}" - ) - } + // Capture vcvars env using the ORIGINAL (non-SUBST) workingDir as CWD. + // vcvars64.bat doesn't depend on CWD for its setup, but using a SUBST drive + // as CWD causes it to silently fail to add MSVC tools to PATH. + val msvcEnv: Map[String, String] = captureVcvarsEnv(vcvars, workingDir, logger) - val finalEnv = - msvcEnv + - ("GRAALVM_ARGUMENT_VECTOR_PROGRAM_NAME" -> "native-image") + // show aliased drive map + getSubstMappings.foreach((k, v) => logger.message(s"substMap $k: -> $v")) - logger.debug(s"msvc PATH entries:") - finalEnv.getOrElse("PATH", "").split(";").toSeq.foreach { entry => - logger.debug(s"$entry;") - } - Seq( - "VCToolsInstallDir", - "VCToolsVersion", - "VCINSTALLDIR", - "WindowsSdkDir", - "WindowsSdkVersion", - "INCLUDE", - "LIB", - "LIBPATH" - ).foreach { key => - logger.debug(s"""$key=${msvcEnv.getOrElse(key, "")}""") + val finalEnv = + msvcEnv + + ("GRAALVM_ARGUMENT_VECTOR_PROGRAM_NAME" -> "native-image") + + logger.debug(s"msvc PATH entries:") + finalEnv.getOrElse("PATH", "").split(";").toSeq.foreach { entry => + logger.debug(s"$entry;") + } + Seq( + "VCToolsInstallDir", + "VCToolsVersion", + "VCINSTALLDIR", + "WindowsSdkDir", + "WindowsSdkVersion", + "INCLUDE", + "LIB", + "LIBPATH" + ).foreach { key => + logger.debug(s"""$key=${msvcEnv.getOrElse(key, "")}""") + } + + // Replace native-image.cmd with native-image.exe, if applicable + val updatedCommand: Seq[String] = + command.headOption match { + case Some(cmd) if cmd.toLowerCase.endsWith("native-image.cmd") => + val cmdPath = os.Path(cmd, os.pwd) + val graalHome = cmdPath / os.up / os.up + resolveNativeImage(graalHome) match { + case Some(exe) => + exe.toString +: command.tail + case None => + command // fall back to the .cmd wrapper + } + case _ => + command } - // Replace native-image.cmd with native-image.exe, if applicable - val updatedCommand: Seq[String] = - command.headOption match { - case Some(cmd) if cmd.toLowerCase.endsWith("native-image.cmd") => - val cmdPath = os.Path(cmd, os.pwd) - val graalHome = cmdPath / os.up / os.up - resolveNativeImage(graalHome) match { - case Some(exe) => - exe.toString +: command.tail - case None => - command // fall back to the .cmd wrapper - } - case _ => - command - } - - logger.message(s"Running: ${updatedCommand.head}") - logger.debug(s"native-image w/args: $updatedCommand") - - val result = - os.proc(updatedCommand) - .call( - cwd = actualWorkingDir, - env = finalEnv, - stdout = os.Inherit, - stderr = os.Inherit - ) - - result.exitCode + logger.debug(s"native-image w/args: $updatedCommand") + + // Run native-image with the SHORTENED working dir to avoid 260-char path limit + val result = + os.proc(updatedCommand) + .call( + cwd = nativeImageWorkDir, + env = finalEnv, + stdout = os.Inherit, + stderr = os.Inherit + ) + + result.exitCode } } finally @@ -235,13 +174,6 @@ object MsvcEnvironment { case _ => None } - // Debug: log if PATH was found during parsing - val pathInEnv = envLines.find(_.toLowerCase.startsWith("path=")) - if pathInEnv.isEmpty then - logger.message("WARNING: PATH not found in vcvars output!") - logger.message(s"First 10 env lines: ${envLines.take(10).mkString("; ")}") - logger.message(s"Last 10 env lines: ${envLines.takeRight(10).mkString("; ")}") - if logger.verbosity > 0 then debugLines.foreach(dbg => logger.debug(s"$dbg")) debugLines.find(_.contains("Writing post-execution environment to ")) match { From 36e198277ff0fb7a0b8108d9a95f70de02950027 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Fri, 13 Feb 2026 08:15:41 +0100 Subject: [PATCH 4/6] . --- .../scala/build/internals/NativeWrapper.scala | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/modules/core/src/main/scala/scala/build/internals/NativeWrapper.scala b/modules/core/src/main/scala/scala/build/internals/NativeWrapper.scala index f9bc546b19..1a49bafb13 100755 --- a/modules/core/src/main/scala/scala/build/internals/NativeWrapper.scala +++ b/modules/core/src/main/scala/scala/build/internals/NativeWrapper.scala @@ -59,6 +59,14 @@ object MsvcEnvironment { // show aliased drive map getSubstMappings.foreach((k, v) => logger.message(s"substMap $k: -> $v")) + // Log whether vcvars captured a usable MSVC environment + val includeValue = msvcEnv.getOrElse("INCLUDE", "") + val pathValue = msvcEnv.getOrElse("PATH", msvcEnv.getOrElse("Path", "")) + val vcPathCount = pathValue.split(";").count(_.toLowerCase.contains("\\vc\\")) + logger.message( + s"vcvars: INCLUDE set=${includeValue.nonEmpty}, VC PATH entries=$vcPathCount, env size=${msvcEnv.size}" + ) + val finalEnv = msvcEnv + ("GRAALVM_ARGUMENT_VECTOR_PROGRAM_NAME" -> "native-image") @@ -127,22 +135,38 @@ object MsvcEnvironment { val vcvarsCmd = vcvars.toIO.getAbsolutePath val sentinel = "vcvSentinel_7f4a2b" - val cmd = Seq( - cmdExe, - "/c", - s"""set "VSCMD_DEBUG=1" & call "$vcvarsCmd" & echo $sentinel & where cl & where link & where lib & cl /Bv & set""" - ) + + // Write the vcvars invocation to a temp batch file instead of passing it inline + // to cmd.exe /c. This avoids Windows command-line quoting issues: Java's + // ProcessBuilder may escape embedded quotes in the /c argument, which breaks + // cmd.exe's interpretation of the chained commands (set, call, echo, etc.). + val batchContent = + s"""@set "VSCMD_DEBUG=1" + |@call "$vcvarsCmd" + |@echo $sentinel + |@where cl + |@where link + |@where lib + |@cl /Bv + |@set + |""".stripMargin + val batchFile = os.temp(suffix = ".bat", contents = batchContent) val out = new StringBuilder val err = new StringBuilder - val res = os.proc(cmd).call( - cwd = workingDir, - env = sys.env, - stdout = os.ProcessOutput.Readlines(line => out.append(line).append("\n")), - stderr = os.ProcessOutput.Readlines(line => err.append(line).append("\n")), - check = false - ) + val res = + try + os.proc(cmdExe, "/c", batchFile.toString).call( + cwd = workingDir, + env = sys.env, + stdout = os.ProcessOutput.Readlines(line => out.append(line).append("\n")), + stderr = os.ProcessOutput.Readlines(line => err.append(line).append("\n")), + check = false + ) + finally + try os.remove(batchFile) + catch { case _: Exception => } if res.exitCode != 0 then logger.error(s"vcvars call failed with exit code ${res.exitCode}") From 92d45f3090df1ab9721cb299e7588072da67b155 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Fri, 13 Feb 2026 10:17:17 +0100 Subject: [PATCH 5/6] . --- .../scala/build/internals/NativeWrapper.scala | 242 ++++-------------- 1 file changed, 45 insertions(+), 197 deletions(-) diff --git a/modules/core/src/main/scala/scala/build/internals/NativeWrapper.scala b/modules/core/src/main/scala/scala/build/internals/NativeWrapper.scala index 1a49bafb13..7425a56290 100755 --- a/modules/core/src/main/scala/scala/build/internals/NativeWrapper.scala +++ b/modules/core/src/main/scala/scala/build/internals/NativeWrapper.scala @@ -1,18 +1,18 @@ package scala.build.internals -import java.util.Locale import scala.annotation.tailrec import scala.build.Logger -import scala.collection.immutable.TreeMap import scala.io.Source import scala.util.Using /* - * Directly invoke `native-image.exe`: - * run `vcvarsall.bat` **only to capture environment** - * merge MSVC environment and base environments. - * directly spawn native-image.exe - * Avoids problematic Ctrl-C behavior of .bat / .cmd files. + * Invoke `native-image.exe` inside a vcvars-initialized cmd.exe session. + * + * A temp batch file calls `vcvars64.bat` to set up MSVC, then runs + * `native-image.exe` directly in the same session. This avoids the + * fragile pattern of capturing the vcvars environment via `set` and + * replaying it through Java's ProcessBuilder, which silently loses + * PATH entries on some JVM / Windows combinations. */ object MsvcEnvironment { @@ -23,7 +23,14 @@ object MsvcEnvironment { private val pathLengthLimit = 90 /* - * Call `native-image.exe` with captured vcvarsall.bat environment. + * Call `native-image.exe` inside a vcvars-initialized cmd.exe session. + * + * Rather than capturing the vcvars environment and replaying it (which is + * fragile — Java's ProcessBuilder env handling on Windows can silently lose + * PATH entries set by vcvars64.bat), we write a small batch file that: + * 1. calls vcvars64.bat (sets up MSVC in the session) + * 2. runs native-image.exe directly (inherits the live session env) + * * @return process exit code. */ def msvcNativeImageProcess( @@ -31,9 +38,8 @@ object MsvcEnvironment { workingDir: os.Path, logger: Logger ): Int = { - // Shorten the working dir for native-image only (it creates deeply nested internal paths - // that can exceed Windows 260-char MAX_PATH). We must NOT use the SUBST drive for vcvars - // capture: running vcvars64.bat with a SUBST-drive CWD causes its PATH setup to silently fail. + // Shorten the working dir for native-image (it creates deeply nested internal + // paths that can exceed the Windows 260-char MAX_PATH limit). val (nativeImageWorkDir, driveToUnalias) = if (workingDir.toString.length >= pathLengthLimit) { val (driveLetter, shortPath) = getShortenedPath(workingDir, logger) @@ -51,42 +57,10 @@ object MsvcEnvironment { case Some(vcvars) => logger.debug(s"Using vcvars script $vcvars") - // Capture vcvars env using the ORIGINAL (non-SUBST) workingDir as CWD. - // vcvars64.bat doesn't depend on CWD for its setup, but using a SUBST drive - // as CWD causes it to silently fail to add MSVC tools to PATH. - val msvcEnv: Map[String, String] = captureVcvarsEnv(vcvars, workingDir, logger) - // show aliased drive map getSubstMappings.foreach((k, v) => logger.message(s"substMap $k: -> $v")) - // Log whether vcvars captured a usable MSVC environment - val includeValue = msvcEnv.getOrElse("INCLUDE", "") - val pathValue = msvcEnv.getOrElse("PATH", msvcEnv.getOrElse("Path", "")) - val vcPathCount = pathValue.split(";").count(_.toLowerCase.contains("\\vc\\")) - logger.message( - s"vcvars: INCLUDE set=${includeValue.nonEmpty}, VC PATH entries=$vcPathCount, env size=${msvcEnv.size}" - ) - - val finalEnv = - msvcEnv + - ("GRAALVM_ARGUMENT_VECTOR_PROGRAM_NAME" -> "native-image") - - logger.debug(s"msvc PATH entries:") - finalEnv.getOrElse("PATH", "").split(";").toSeq.foreach { entry => - logger.debug(s"$entry;") - } - Seq( - "VCToolsInstallDir", - "VCToolsVersion", - "VCINSTALLDIR", - "WindowsSdkDir", - "WindowsSdkVersion", - "INCLUDE", - "LIB", - "LIBPATH" - ).foreach { key => - logger.debug(s"""$key=${msvcEnv.getOrElse(key, "")}""") - } + val vcvarsCmd = vcvars.toIO.getAbsolutePath // Replace native-image.cmd with native-image.exe, if applicable val updatedCommand: Seq[String] = @@ -104,167 +78,41 @@ object MsvcEnvironment { command } + // Quote arguments that contain batch-special characters + val quotedArgs = updatedCommand.map { arg => + if arg.exists(c => " &|^<>()".contains(c)) then s""""$arg"""" + else arg + }.mkString(" ") + + // Build a batch file that calls vcvars then runs native-image + // in the same session — no environment capture/replay needed. + val batchContent = + s"""@call "$vcvarsCmd" + |@if errorlevel 1 exit /b %ERRORLEVEL% + |@set GRAALVM_ARGUMENT_VECTOR_PROGRAM_NAME=native-image + |@$quotedArgs + |""".stripMargin + val batchFile = os.temp(suffix = ".bat", contents = batchContent) + logger.debug(s"native-image w/args: $updatedCommand") - // Run native-image with the SHORTENED working dir to avoid 260-char path limit - val result = - os.proc(updatedCommand) - .call( - cwd = nativeImageWorkDir, - env = finalEnv, - stdout = os.Inherit, - stderr = os.Inherit - ) - - result.exitCode + try + val result = os.proc(cmdExe, "/c", batchFile.toString).call( + cwd = nativeImageWorkDir, + stdout = os.Inherit, + stderr = os.Inherit, + check = false + ) + result.exitCode + finally + try os.remove(batchFile) + catch { case _: Exception => } } } finally driveToUnalias.foreach(unaliasDriveLetter) } - // ========================= - // Capture MSVC environment - // ========================= - private def captureVcvarsEnv( - vcvars: os.Path, - workingDir: os.Path, - logger: Logger - ): Map[String, String] = { - - val vcvarsCmd = vcvars.toIO.getAbsolutePath - - val sentinel = "vcvSentinel_7f4a2b" - - // Write the vcvars invocation to a temp batch file instead of passing it inline - // to cmd.exe /c. This avoids Windows command-line quoting issues: Java's - // ProcessBuilder may escape embedded quotes in the /c argument, which breaks - // cmd.exe's interpretation of the chained commands (set, call, echo, etc.). - val batchContent = - s"""@set "VSCMD_DEBUG=1" - |@call "$vcvarsCmd" - |@echo $sentinel - |@where cl - |@where link - |@where lib - |@cl /Bv - |@set - |""".stripMargin - val batchFile = os.temp(suffix = ".bat", contents = batchContent) - - val out = new StringBuilder - val err = new StringBuilder - - val res = - try - os.proc(cmdExe, "/c", batchFile.toString).call( - cwd = workingDir, - env = sys.env, - stdout = os.ProcessOutput.Readlines(line => out.append(line).append("\n")), - stderr = os.ProcessOutput.Readlines(line => err.append(line).append("\n")), - check = false - ) - finally - try os.remove(batchFile) - catch { case _: Exception => } - - if res.exitCode != 0 then - logger.error(s"vcvars call failed with exit code ${res.exitCode}") - Map.empty - else - def toVec(iter: Iterator[String]): Vector[String] = - iter.map(_.trim).filter(_.nonEmpty).toVector - val errlines = toVec( - err.result().linesIterator - ).filter(_ != "cl : Command line error D8003 : missing source filename") - val outlines = toVec(out.result().linesIterator) - - // Split at sentinel - val (debugLog, afterSentinel) = outlines.span(_ != sentinel) - - // Drop the sentinel itself - val envLines = afterSentinel.drop(1) - val debugLines = errlines ++ debugLog - - given Ordering[String] = - Ordering.by[String, String](_.toLowerCase(Locale.ROOT))(using Ordering.String) - - // Parse KEY=VALUE lines, preserving original key casing - val envMap = - TreeMap.empty[String, String] ++ - envLines.flatMap { line => - line.split("=", 2) match - case Array(k, v) => Some(k -> v) // preserve original spelling - case _ => None - } - - if logger.verbosity > 0 then - debugLines.foreach(dbg => logger.debug(s"$dbg")) - debugLines.find(_.contains("Writing post-execution environment to ")) match { - case None => - case Some(s) => - val envMapFile = s.replaceFirst(".* environment to ", "") - envReport(envMap, envMapFile, logger) - } - - envMap - } - - def envReport(captEnv: Map[String, String], logfile: String, logger: Logger): Unit = { - import java.nio.file.{Files, Paths} - import scala.jdk.CollectionConverters.* - - val path = Paths.get(logfile) - - if !Files.exists(path) then - logger.message(s"not found: $logfile") - else - // Parse KEY=VALUE lines from the file - val fileEnv: Map[String, String] = - Files.readAllLines(path).asScala.flatMap { line => - line.split("=", 2) match - case Array(k, v) => Some(k -> v) - case _ => None - }.toMap - - // Keys present in both but with different values - val differingValues = - for - (k, v1) <- captEnv - v2 <- fileEnv.get(k) - if v1 != v2 - yield (k, v1, v2) - - // Keys only in captured env - val onlyInCaptured = - captEnv.keySet.diff(fileEnv.keySet).map(k => k -> captEnv(k)) - - // Keys only in file env - val onlyInFile = - fileEnv.keySet.diff(captEnv.keySet).map(k => k -> fileEnv(k)) - - if differingValues.nonEmpty then - logger.debug("=== keys with different values ===") - differingValues.foreach { case (k, v1, v2) => - logger.debug(s"$k:\n captured = $v1\n file = $v2") - } - - if onlyInCaptured.nonEmpty then - logger.debug("=== only in captured env ===") - onlyInCaptured.foreach { case (k, v) => - logger.debug(s"$k = $v") - } - - if onlyInFile.nonEmpty then - logger.debug("=== only in file env ===") - onlyInFile.foreach { case (k, v) => - logger.debug(s"$k = $v") - } - - if differingValues.isEmpty && onlyInCaptured.isEmpty && onlyInFile.isEmpty then - logger.debug("envReport: no differences") - } - def getSubstMappings: Map[Char, String] = try val (exitCode, output) = execWindowsCmd(cmdExe, "/c", "subst") From 3d7e73ca2a498ac4b2eb8ca3418e7bc4df5aeafd Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Fri, 13 Feb 2026 15:17:43 +0100 Subject: [PATCH 6/6] . --- .../scala/build/internals/NativeWrapper.scala | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/modules/core/src/main/scala/scala/build/internals/NativeWrapper.scala b/modules/core/src/main/scala/scala/build/internals/NativeWrapper.scala index 7425a56290..2e07b4d98b 100755 --- a/modules/core/src/main/scala/scala/build/internals/NativeWrapper.scala +++ b/modules/core/src/main/scala/scala/build/internals/NativeWrapper.scala @@ -84,21 +84,35 @@ object MsvcEnvironment { else arg }.mkString(" ") - // Build a batch file that calls vcvars then runs native-image - // in the same session — no environment capture/replay needed. + // Build a batch file that: + // 1. calls vcvars64.bat (with the inherited, non-SUBST CWD) + // 2. locates cl.exe and passes it explicitly to native-image + // (works around GraalVM native-image not finding cl.exe via + // PATH when the process runs from a SUBST-drive CWD) + // 3. switches to the shortened SUBST working directory + // 4. runs native-image.exe val batchContent = s"""@call "$vcvarsCmd" |@if errorlevel 1 exit /b %ERRORLEVEL% |@set GRAALVM_ARGUMENT_VECTOR_PROGRAM_NAME=native-image - |@$quotedArgs + |@for /f "delims=" %%i in ('where cl.exe 2^>nul') do @set "CL_EXE=%%i" + |@if not defined CL_EXE ( + | echo cl.exe not found in PATH after vcvars 1>&2 + | exit /b 1 + |) + |@cd /d "$nativeImageWorkDir" + |@$quotedArgs --native-compiler-path="%CL_EXE%" |""".stripMargin val batchFile = os.temp(suffix = ".bat", contents = batchContent) logger.debug(s"native-image w/args: $updatedCommand") try + // Don't pass cwd here — let cmd.exe inherit the parent's real + // (non-SUBST) CWD so that vcvars64.bat runs without SUBST issues. + // The batch file does `cd /d` to the shortened workdir before + // launching native-image. val result = os.proc(cmdExe, "/c", batchFile.toString).call( - cwd = nativeImageWorkDir, stdout = os.Inherit, stderr = os.Inherit, check = false