Skip to content

Commit 2c28263

Browse files
authored
fix #4005 - Windows native-image compile failure caused by SUBST collision (#4006)
* this fixes native-image compiles in Windows caused by attempting to use SUBSTed drive: #4005 * add integration test for new availableDriveLetter * call native-image.exe directly bypassing .cmd and .bat files refactor MsvcEnvironment to hold windows-specific native-image code add logging to vcvars env capture add VSCMD_DEBUG=1 logging to vcvars64.bat capture * reworked sentinel to avoid localization problems * convert diagnostic log messages to debug * redesigned handling of SUBSTed drive letters to avoid accumlation of allocated letters
1 parent a423e79 commit 2c28263

File tree

4 files changed

+564
-128
lines changed

4 files changed

+564
-128
lines changed

modules/cli/src/main/scala/scala/cli/packaging/NativeImage.scala

Lines changed: 23 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ package scala.cli.packaging
22

33
import java.io.File
44

5-
import scala.annotation.tailrec
65
import scala.build.internal.{ManifestJar, Runner}
76
import scala.build.internals.ConsoleUtils.ScalaCliConsole.warnPrefix
8-
import scala.build.internals.EnvVar
7+
import scala.build.internals.MsvcEnvironment
8+
import scala.build.internals.MsvcEnvironment.*
99
import scala.build.{Build, Logger, Positioned, coursierVersion}
1010
import scala.cli.errors.GraalVMNativeImageError
1111
import scala.cli.graal.{BytecodeProcessor, TempCache}
@@ -37,95 +37,6 @@ object NativeImage {
3737
nativeImage
3838
}
3939

40-
private def vcVersions = Seq("2022", "2019", "2017")
41-
private def vcEditions = Seq("Enterprise", "Community", "BuildTools")
42-
private lazy val vcVarsCandidates: Iterable[String] =
43-
EnvVar.Misc.vcVarsAll.valueOpt ++ {
44-
for {
45-
isX86 <- Seq(false, true)
46-
version <- vcVersions
47-
edition <- vcEditions
48-
} yield {
49-
val programFiles = if (isX86) "Program Files (x86)" else "Program Files"
50-
"""C:\""" + programFiles + """\Microsoft Visual Studio\""" + version + "\\" + edition + """\VC\Auxiliary\Build\vcvars64.bat"""
51-
}
52-
}
53-
54-
private def vcvarsOpt: Option[os.Path] =
55-
vcVarsCandidates
56-
.iterator
57-
.map(os.Path(_, os.pwd))
58-
.filter(os.exists(_))
59-
.take(1)
60-
.toList
61-
.headOption
62-
63-
private def runFromVcvarsBat(
64-
command: Seq[String],
65-
vcvars: os.Path,
66-
workingDir: os.Path,
67-
logger: Logger
68-
): Int = {
69-
logger.debug(s"Using vcvars script $vcvars")
70-
val escapedCommand = command.map {
71-
case s if s.contains(" ") => "\"" + s + "\""
72-
case s => s
73-
}
74-
// chcp 437 sometimes needed, see https://github.com/oracle/graal/issues/2522
75-
// but must save and restore existing code page afterwards
76-
val script =
77-
s"""@echo off
78-
|rem Save current code page
79-
|for /f "tokens=2 delims=:." %%A in ('chcp') do set "OLDCP=%%A"
80-
|@echo on
81-
|set OLDCP=%OLDCP: =%
82-
|chcp 437
83-
|@call "$vcvars"
84-
|if %errorlevel% neq 0 exit /b %errorlevel%
85-
|@call ${escapedCommand.mkString(" ")}
86-
|rem Restore original code page
87-
|chcp %OLDCP% >nul
88-
|""".stripMargin
89-
logger.debug(s"Native image script: '$script'")
90-
val scriptPath = workingDir / "run-native-image.bat"
91-
logger.debug(s"Writing native image script at $scriptPath")
92-
os.write.over(scriptPath, script.getBytes, createFolders = true)
93-
94-
val finalCommand = Seq("cmd", "/c", scriptPath.toString)
95-
logger.debug(s"Running $finalCommand")
96-
val res = os.proc(finalCommand).call(
97-
cwd = os.pwd,
98-
check = false,
99-
stdin = os.Inherit,
100-
stdout = os.Inherit
101-
)
102-
logger.debug(s"Command $finalCommand exited with exit code ${res.exitCode}")
103-
104-
res.exitCode
105-
}
106-
107-
private lazy val mountedDrives: String = {
108-
val str = "HKEY_LOCAL_MACHINE/SYSTEM/MountedDevices".replace('/', '\\')
109-
val queryDrives = s"reg query $str"
110-
val lines = os.proc("cmd", "/c", queryDrives).call().out.lines()
111-
val dosDevices = lines.filter { s =>
112-
s.contains("DosDevices")
113-
}.map { s =>
114-
s.replaceAll(".DosDevices.", "").replaceAll(":.*", "")
115-
}
116-
dosDevices.mkString
117-
}
118-
private def availableDriveLetter(): Char = {
119-
120-
@tailrec
121-
def helper(from: Char): Char =
122-
if (from > 'Z') sys.error("Cannot find free drive letter")
123-
else if (mountedDrives.contains(from)) helper((from + 1).toChar)
124-
else from
125-
126-
helper('D')
127-
}
128-
12940
/** Alias currentHome to the root of a drive, so that its files can be accessed with shorter paths
13041
* (hopefully not going above the ~260 char limit of some Windows apps, such as cl.exe).
13142
*
@@ -139,39 +50,17 @@ object NativeImage {
13950
)(
14051
f: os.Path => T
14152
): T =
142-
// not sure about the 180 limit, we might need to lower it
14353
if (Properties.isWin && currentHome.toString.length >= 180) {
144-
val driveLetter = availableDriveLetter()
145-
// aliasing the parent dir, as it seems GraalVM native-image (as of 22.0.0)
146-
// isn't fine with being put at the root of a drive - it tries to look for
147-
// things like 'D:lib' (missing '\') at some point.
148-
val from = currentHome / os.up
149-
val drivePath = os.Path(s"$driveLetter:" + "\\")
150-
val newHome = drivePath / currentHome.last
151-
logger.debug(s"Aliasing $from to $drivePath")
152-
val setupCommand = s"""subst $driveLetter: "$from""""
153-
val disableScript = s"""subst $driveLetter: /d"""
154-
155-
os.proc("cmd", "/c", setupCommand).call(stdin = os.Inherit, stdout = os.Inherit)
156-
try f(newHome)
157-
finally {
158-
val res = os.proc("cmd", "/c", disableScript).call(
159-
stdin = os.Inherit,
160-
stdout = os.Inherit,
161-
check = false
162-
)
163-
if (res.exitCode == 0)
164-
logger.debug(s"Unaliased $drivePath")
165-
else if (os.exists(drivePath)) {
166-
// ignore errors?
167-
logger.debug(s"Unaliasing attempt exited with exit code ${res.exitCode}")
168-
throw new os.SubprocessException(res)
54+
val (driveLetter, newHome) = getShortenedPath(currentHome, logger)
55+
val savedCodepage: String = getCodePage(logger)
56+
val result =
57+
try
58+
f(newHome)
59+
finally {
60+
unaliasDriveLetter(driveLetter)
61+
setCodePage(savedCodepage)
16962
}
170-
else
171-
logger.debug(
172-
s"Failed to unalias $drivePath which seems not to exist anymore, ignoring it"
173-
)
174-
}
63+
result
17564
}
17665
else
17766
f(currentHome)
@@ -249,10 +138,16 @@ object NativeImage {
249138
}
250139
else (processedClassPath, Seq[os.Path](), Seq[String]())
251140

141+
def stripSuffixIgnoreCase(s: String, suffix: String): String =
142+
if (s.toLowerCase.endsWith(suffix.toLowerCase))
143+
s.substring(0, s.length - suffix.length)
144+
else
145+
s
146+
252147
try {
253148
val args = extraOptions ++ scala3extraOptions ++ Seq(
254149
s"-H:Path=${dest / os.up}",
255-
s"-H:Name=${dest.last.stripSuffix(".exe")}", // FIXME Case-insensitive strip suffix?
150+
s"-H:Name=${stripSuffixIgnoreCase(dest.last, ".exe")}", // Case-insensitive strip suffix
256151
"-cp",
257152
classPath.map(_.toString).mkString(File.pathSeparator),
258153
mainClass
@@ -265,10 +160,11 @@ object NativeImage {
265160

266161
val exitCode =
267162
if Properties.isWin then
268-
vcvarsOpt match {
269-
case Some(vcvars) => runFromVcvarsBat(command, vcvars, nativeImageWorkDir, logger)
270-
case None => Runner.run(command, logger).waitFor()
271-
}
163+
MsvcEnvironment.msvcNativeImageProcess(
164+
command = command,
165+
workingDir = nativeImageWorkDir,
166+
logger = logger
167+
)
272168
else Runner.run(command, logger).waitFor()
273169
if exitCode == 0 then {
274170
val actualDest =

0 commit comments

Comments
 (0)