From fbc01f95256212b67f2654cd89e8c5afc4786778 Mon Sep 17 00:00:00 2001 From: Fabio Niephaus Date: Wed, 20 Dec 2023 17:41:26 +0100 Subject: [PATCH] Use `vswhere` to detect VS installations. --- substratevm/CHANGELOG.md | 3 + .../driver/WindowsBuildEnvironmentUtil.java | 120 ++++++++++++------ .../hosted/c/codegen/CCompilerInvoker.java | 5 +- 3 files changed, 86 insertions(+), 42 deletions(-) diff --git a/substratevm/CHANGELOG.md b/substratevm/CHANGELOG.md index 9542ed34d5be..be60e1486810 100644 --- a/substratevm/CHANGELOG.md +++ b/substratevm/CHANGELOG.md @@ -2,6 +2,9 @@ This changelog summarizes major changes to GraalVM Native Image. +## GraalVM for JDK 23 (Internal Version 24.1.0) +* (GR-51053) Native Image now uses `vswhere` to find Visual Studio installations in non-standard locations. + ## GraalVM for JDK 22 (Internal Version 24.0.0) * (GR-48304) Red Hat added support for the JFR event ThreadAllocationStatistics. * (GR-48343) Red Hat added support for the JFR events AllocationRequiringGC and SystemGC. diff --git a/substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/WindowsBuildEnvironmentUtil.java b/substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/WindowsBuildEnvironmentUtil.java index 97f569d02d95..87adaec49446 100644 --- a/substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/WindowsBuildEnvironmentUtil.java +++ b/substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/WindowsBuildEnvironmentUtil.java @@ -43,66 +43,35 @@ * This utility helps set up a build environment for Windows users automatically. */ class WindowsBuildEnvironmentUtil { - private static final String VSINSTALLDIR_ENV = "VSINSTALLDIR"; - private static final Path[] KNOWN_VS_LOCATIONS = { - Paths.get("C:", "Program Files", "Microsoft Visual Studio"), - // prefer x64 location over x86 - Paths.get("C:", "Program Files (x86)", "Microsoft Visual Studio")}; - private static final String[] KNOWN_VS_EDITIONS = { - // prefer Enterprise over Professional over Community over BuildTools - "Enterprise", "Professional", "Community", "BuildTools"}; private static final String VCVARSALL = "vcvarsall.bat"; private static final Path VCVARSALL_SUBPATH = Paths.get("VC", "Auxiliary", "Build", VCVARSALL); - private static final String OUTPUT_SEPARATOR = "!NEXTCOMMAND!"; // Use another static field for minimum required version because CCompilerInvoker is hosted only private static final String VISUAL_STUDIO_MINIMUM_REQUIRED_VERSION = CCompilerInvoker.VISUAL_STUDIO_MINIMUM_REQUIRED_VERSION; + private static final String VISUAL_STUDIO_MINIMUM_REQUIRED_VERSION_TEXT = CCompilerInvoker.VISUAL_STUDIO_MINIMUM_REQUIRED_VERSION_TEXT; static void propagateEnv(Map environment) { if (isCCompilerOnPath()) { return; // nothing to do, build environment initialized by user } - Path vcVarsAllLocation = null; - for (Path visualStudioLocation : KNOWN_VS_LOCATIONS) { - if (!Files.isDirectory(visualStudioLocation)) { - continue; - } - List installationCandidates; - try (Stream pathStream = Files.list(visualStudioLocation)) { - installationCandidates = pathStream - // only keep sub-directories matching 20\d\d - .filter(p -> p.toFile().getName().matches("20\\d\\d")) - // sort years - .sorted(Comparator.comparing(p -> p.toFile().getName())) - // reverse order to ensure latest year is first - .sorted(Comparator.reverseOrder()) - .toList(); - } catch (IOException e) { - throw fail("Failed to traverse known Visual Studio locations.", e); - } - for (Path installation : installationCandidates) { - for (String edition : KNOWN_VS_EDITIONS) { - Path possibleLocation = installation.resolve(edition).resolve(VCVARSALL_SUBPATH); - if (Files.isRegularFile(possibleLocation) && Files.isReadable(possibleLocation)) { - vcVarsAllLocation = possibleLocation; - break; - } - } - } + Path vcVarsAllLocation = findVCVarsallWithVSWhere(); + if (vcVarsAllLocation == null) { + vcVarsAllLocation = findVCVarsallOnDisk(); } if (vcVarsAllLocation == null) { throw fail(String.format("Failed to find '%s' in a Visual Studio installation.", VCVARSALL)); } Map originalEnv = new HashMap<>(); int numSeenOutputSeparators = 0; + String outputSeparator = "!NEXTCOMMAND!"; try { // call `set`, then `vcvarsall.bat`, and then `set` again with separators in between String commandSequence = String.format("cmd.exe /c set && echo %s && \"%s\" x64 && echo %s && set", - OUTPUT_SEPARATOR, vcVarsAllLocation, OUTPUT_SEPARATOR); + outputSeparator, vcVarsAllLocation, outputSeparator); Process p = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", commandSequence}); try (BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()))) { String line; while ((line = reader.readLine()) != null) { - if (line.startsWith(OUTPUT_SEPARATOR)) { + if (line.startsWith(outputSeparator)) { numSeenOutputSeparators++; } else if (numSeenOutputSeparators == 0) { // collect environment variables from 1st `set` invocation @@ -122,11 +91,78 @@ static void propagateEnv(Map environment) { } catch (IOException | InterruptedException e) { throw fail("Failed to detect variables of Windows build environment.", e); } - if (!environment.containsKey(VSINSTALLDIR_ENV)) { + if (!environment.containsKey("VSINSTALLDIR_ENV")) { throw fail("Failed to automatically set up Windows build environment."); } } + private static Path findVCVarsallWithVSWhere() { + String programFilesX86 = System.getenv("ProgramFiles(x86)"); + if (programFilesX86 == null) { + return null; + } + Path vsWhereExe = Paths.get(programFilesX86, "Microsoft Visual Studio", "Installer", "vswhere.exe"); + if (!Files.exists(vsWhereExe)) { + return null; + } + try { + Process p = Runtime.getRuntime().exec(new String[]{vsWhereExe.toAbsolutePath().toString(), + "-version", VISUAL_STUDIO_MINIMUM_REQUIRED_VERSION, + "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", + "-property", "installationPath"}); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()))) { + Path installationPath = Paths.get(reader.readLine()); + Path possibleLocation = installationPath.resolve(VCVARSALL_SUBPATH); + if (isRegularReadableFile(possibleLocation)) { + return possibleLocation; + } + } + } catch (IOException e) { + throw fail("Failed to process vswhere.exe output.", e); + } + throw fail("Failed to find suitable version of Visual Studio with vswhere.exe."); + } + + private static Path findVCVarsallOnDisk() { + // prefer x64 location over x86 + String[] programFilesNames = {"ProgramFiles", "ProgramFiles(x86)"}; + // prefer Enterprise over Professional over Community over BuildTools + String[] knownVSEditions = {"Enterprise", "Professional", "Community", "BuildTools"}; + + for (String programFilesName : programFilesNames) { + String programFiles = System.getenv(programFilesName); + if (programFiles == null) { + continue; + } + Path visualStudioLocation = Paths.get(programFiles, "Microsoft Visual Studio"); + if (!Files.isDirectory(visualStudioLocation)) { + continue; + } + List installationCandidates; + try (Stream pathStream = Files.list(visualStudioLocation)) { + installationCandidates = pathStream + // only keep sub-directories matching 20\d\d + .filter(p -> p.toFile().getName().matches("20\\d\\d")) + // sort years + .sorted(Comparator.comparing(p -> p.toFile().getName())) + // reverse order to ensure latest year is first + .sorted(Comparator.reverseOrder()) + .toList(); + } catch (IOException e) { + throw fail("Failed to traverse known Visual Studio locations.", e); + } + for (Path installation : installationCandidates) { + for (String edition : knownVSEditions) { + Path possibleLocation = installation.resolve(edition).resolve(VCVARSALL_SUBPATH); + if (isRegularReadableFile(possibleLocation)) { + return possibleLocation; + } + } + } + } + return null; + } + private static boolean isCCompilerOnPath() { try { return Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", "where", "cl.exe"}).waitFor() == 0; @@ -135,6 +171,10 @@ private static boolean isCCompilerOnPath() { } } + private static boolean isRegularReadableFile(Path location) { + return Files.isRegularFile(location) && Files.isReadable(location); + } + private static void processLineWithKeyValue(String line, BiConsumer consumeKeyValue) { String[] parts = line.split("="); if (parts.length == 2) { @@ -150,6 +190,6 @@ private static Error fail(String reason, Throwable e) { throw NativeImage.showError(String.format("%s%nPlease make sure that %s or later is installed on your system. " + "You can download it at https://visualstudio.microsoft.com/downloads/. " + "If this error persists, please try and run GraalVM Native Image in an x64 Native Tools Command Prompt or file a ticket.", - reason, VISUAL_STUDIO_MINIMUM_REQUIRED_VERSION), e); + reason, VISUAL_STUDIO_MINIMUM_REQUIRED_VERSION_TEXT), e); } } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/c/codegen/CCompilerInvoker.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/c/codegen/CCompilerInvoker.java index 10deb97fc8c9..7236830f099d 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/c/codegen/CCompilerInvoker.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/c/codegen/CCompilerInvoker.java @@ -61,7 +61,8 @@ import jdk.vm.ci.riscv64.RISCV64; public abstract class CCompilerInvoker { - public static final String VISUAL_STUDIO_MINIMUM_REQUIRED_VERSION = "Visual Studio 2022 version 17.1.0"; + public static final String VISUAL_STUDIO_MINIMUM_REQUIRED_VERSION = "17.1"; + public static final String VISUAL_STUDIO_MINIMUM_REQUIRED_VERSION_TEXT = "Visual Studio 2022 version %s.0".formatted(VISUAL_STUDIO_MINIMUM_REQUIRED_VERSION); public final Path tempDirectory; public final CompilerInfo compilerInfo; @@ -188,7 +189,7 @@ protected void verify() { int minimumMinorVersion = 31; if (compilerInfo.versionMajor < minimumMajorVersion || compilerInfo.versionMinor0 < minimumMinorVersion) { UserError.abort("On Windows, GraalVM Native Image for JDK %s requires %s or later (C/C++ Optimizing Compiler Version %s.%s or later).%nCompiler info detected: %s", - JavaVersionUtil.JAVA_SPEC, VISUAL_STUDIO_MINIMUM_REQUIRED_VERSION, minimumMajorVersion, minimumMinorVersion, compilerInfo.getShortDescription()); + JavaVersionUtil.JAVA_SPEC, VISUAL_STUDIO_MINIMUM_REQUIRED_VERSION_TEXT, minimumMajorVersion, minimumMinorVersion, compilerInfo.getShortDescription()); } if (guessArchitecture(compilerInfo.targetArch) != AMD64.class) { String targetPrefix = compilerInfo.targetArch.matches("(.*x|i\\d)86$") ? "32-bit architecture " : "";