yum-mirror/slang

Making it easier to work with shaders

git clone https://git.yummers.dev/yum-mirror/slang

Ellie HermaszewskaCorrectly distinguish between windows and MSVC (#5851)48ac6f25f

master
16.7 KiB564 linesraw
1#include "slang-win-visual-studio-util.h"
2
3#include "../../core/slang-common.h"
4#include "../../core/slang-process-util.h"
5#include "../../core/slang-string-util.h"
6#include "../slang-json-parser.h"
7#include "../slang-json-value.h"
8#include "../slang-visual-studio-compiler-util.h"
9
10#ifdef _WIN32
11#include <shlobj.h>
12#include <windows.h>
13#pragma comment(lib, "advapi32")
14#pragma comment(lib, "Shell32")
15#endif
16
17// The method used to invoke VS was originally inspired by some ideas in
18// https://github.com/RuntimeCompiledCPlusPlus/RuntimeCompiledCPlusPlus/
19
20namespace Slang
21{
22
23// Information on VS versioning can be found here
24// https://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B#Internal_version_numbering
25
26
27namespace
28{ // anonymous
29
30struct RegistryInfo
31{
32    const char* regName; ///< The name of the entry in the registry
33    const char* pathFix; ///< With the value from the registry how to fix the path
34};
35
36struct VersionInfo
37{
38    SemanticVersion version; ///< The version
39    const char* name;        ///< The name of the registry key
40};
41
42} // namespace
43
44static SlangResult _readRegistryKey(const char* path, const char* keyName, String& outString)
45{
46    // https://docs.microsoft.com/en-us/windows/desktop/api/winreg/nf-winreg-regopenkeyexa
47    HKEY key;
48    LONG ret = RegOpenKeyExA(HKEY_LOCAL_MACHINE, path, 0, KEY_READ | KEY_WOW64_32KEY, &key);
49    if (ret != ERROR_SUCCESS)
50    {
51        return SLANG_FAIL;
52    }
53
54    char value[MAX_PATH];
55    DWORD size = MAX_PATH;
56
57    // https://docs.microsoft.com/en-us/windows/desktop/api/winreg/nf-winreg-regqueryvalueexa
58    ret = RegQueryValueExA(key, keyName, nullptr, nullptr, (LPBYTE)value, &size);
59    RegCloseKey(key);
60
61    if (ret != ERROR_SUCCESS)
62    {
63        return SLANG_FAIL;
64    }
65
66    outString = value;
67    return SLANG_OK;
68}
69
70// Make easier to set up the array
71
72[[maybe_unused]] static DownstreamCompilerMatchVersion _makeVersion(int main)
73{
74    DownstreamCompilerMatchVersion version;
75    version.type = SLANG_PASS_THROUGH_VISUAL_STUDIO;
76    version.matchVersion.set(main);
77    return version;
78}
79
80[[maybe_unused]] static DownstreamCompilerMatchVersion _makeVersion(int main, int dot)
81{
82    DownstreamCompilerMatchVersion version;
83    version.type = SLANG_PASS_THROUGH_VISUAL_STUDIO;
84    version.matchVersion.set(main, dot);
85    return version;
86}
87
88VersionInfo _makeVersionInfo(const char* name, int high, int dot = 0)
89{
90    VersionInfo info;
91    info.name = name;
92    info.version = SemanticVersion(high, dot);
93    return info;
94}
95
96// https://en.wikipedia.org/wiki/Microsoft_Visual_Studio
97static const VersionInfo s_versionInfos[] = {
98    _makeVersionInfo("VS 2005", 8),
99    _makeVersionInfo("VS 2008", 9),
100    _makeVersionInfo("VS 2010", 10),
101    _makeVersionInfo("VS 2012", 11),
102    _makeVersionInfo("VS 2013", 12),
103    _makeVersionInfo("VS 2015", 14),
104    _makeVersionInfo("VS 2017", 15),
105    _makeVersionInfo("VS 2019", 16),
106    _makeVersionInfo("VS 2022", 17),
107};
108
109// When trying to figure out how this stuff works by running regedit - care is needed,
110// because what regedit displays varies on which version of regedit is used.
111// In order to use the registry paths used here it's necessary to use Start/Run with
112// %systemroot%\syswow64\regedit to view 32 bit keys
113
114static const RegistryInfo s_regInfos[] = {
115    {"SOFTWARE\\Microsoft\\VisualStudio\\SxS\\VC7", ""},
116    {"SOFTWARE\\Microsoft\\VisualStudio\\SxS\\VS7", "VC\\Auxiliary\\Build\\"},
117};
118
119static bool _canUseVSWhere(SemanticVersion version)
120{
121    // If greater than 15.0 we can use vswhere tool
122    return version.m_major >= 15;
123}
124
125static int _getRegistryKeyIndex(const SemanticVersion& version)
126{
127    if (version.m_major >= 15)
128    {
129        return 1;
130    }
131    return 0;
132}
133
134static SlangResult _parseVersion(UnownedStringSlice versionString, SemanticVersion& outVersion)
135{
136    // We only want the first 2 semantic numbers as 3rd looks like a build number, and too large
137    List<UnownedStringSlice> slices;
138    StringUtil::split(versionString, '.', slices);
139    if (slices.getCount() >= 2)
140    {
141        versionString = UnownedStringSlice(versionString.begin(), slices[1].end());
142    }
143
144    // Extract the version
145    SemanticVersion semanticVersion;
146    return SemanticVersion::parse(versionString, outVersion);
147}
148
149
150/* static */ DownstreamCompilerMatchVersion WinVisualStudioUtil::getCompiledVersion()
151{
152#ifdef _MSC_VER
153    // Get the version of visual studio used to compile this source
154    // Not const, because otherwise we get an warning/error about constant expression...
155    uint32_t version = _MSC_VER;
156
157    switch (version)
158    {
159    case 1400:
160        return _makeVersion(8);
161    case 1500:
162        return _makeVersion(9);
163    case 1600:
164        return _makeVersion(10);
165    case 1700:
166        return _makeVersion(11);
167    case 1800:
168        return _makeVersion(12);
169    default:
170        break;
171    }
172
173    // Seems like versions go in runs of 10 at this point
174    // https://docs.microsoft.com/en-us/cpp/preprocessor/predefined-macros?view=msvc-170
175    // https://docs.microsoft.com/en-us/cpp/preprocessor/predefined-macros?redirectedfrom=MSDN&view=msvc-170
176    if (version >= 1900 && version < 1910)
177    {
178        return _makeVersion(14);
179    }
180    else if (version >= 1910 && version < 1920)
181    {
182        switch (version)
183        {
184        case 1910:
185            return _makeVersion(15, 0);
186        case 1911:
187            return _makeVersion(15, 3);
188        case 1912:
189            return _makeVersion(15, 5);
190        case 1913:
191            return _makeVersion(15, 6);
192        case 1914:
193            return _makeVersion(15, 7);
194        case 1915:
195            return _makeVersion(15, 8);
196        case 1916:
197            return _makeVersion(15, 9);
198        default:
199            return _makeVersion(15);
200        }
201    }
202    else if (version >= 1920 && version < 1930)
203    {
204        switch (version)
205        {
206        case 1920:
207            return _makeVersion(16, 0);
208        case 1921:
209            return _makeVersion(16, 1);
210        case 1922:
211            return _makeVersion(16, 2);
212        case 1923:
213            return _makeVersion(16, 3);
214        case 1924:
215            return _makeVersion(16, 4);
216        case 1925:
217            return _makeVersion(16, 5);
218        case 1926:
219            return _makeVersion(16, 6);
220        case 1927:
221            return _makeVersion(16, 7);
222        case 1928:
223            return _makeVersion(16, 9);
224        case 1929:
225            return _makeVersion(16, 11);
226        default:
227            return _makeVersion(16);
228        }
229    }
230    else if (version >= 1930 && version < 1940)
231    {
232        switch (version)
233        {
234        case 1930:
235            return _makeVersion(17, 0);
236        case 1931:
237            return _makeVersion(17, 1);
238        case 1932:
239            return _makeVersion(17, 2);
240        default:
241            return _makeVersion(17);
242        }
243    }
244    else if (version >= 1940)
245    {
246        // Its an unknown newer version
247        return DownstreamCompilerMatchVersion(
248            SLANG_PASS_THROUGH_VISUAL_STUDIO,
249            MatchSemanticVersion::makeFuture());
250    }
251#endif
252
253    // Unknown version
254    return DownstreamCompilerMatchVersion(SLANG_PASS_THROUGH_VISUAL_STUDIO, MatchSemanticVersion());
255}
256
257static SlangResult _parseJson(
258    const String& contents,
259    DiagnosticSink* sink,
260    JSONContainer* container,
261    JSONValue& outRoot)
262{
263    auto sourceManager = sink->getSourceManager();
264
265    SourceFile* sourceFile =
266        sourceManager->createSourceFileWithString(PathInfo::makeUnknown(), contents);
267    SourceView* sourceView = sourceManager->createSourceView(sourceFile, nullptr, SourceLoc());
268
269    JSONLexer lexer;
270    lexer.init(sourceView, sink);
271
272    JSONBuilder builder(container);
273
274    JSONParser parser;
275    SLANG_RETURN_ON_FAIL(parser.parse(&lexer, sourceView, &builder, sink));
276
277    outRoot = builder.getRootValue();
278    return SLANG_OK;
279}
280
281static void _orderVersions(List<WinVisualStudioUtil::VersionPath>& ioVersions)
282{
283    typedef WinVisualStudioUtil::VersionPath VersionPath;
284    // Put into increasing version order, from oldest to newest
285    ioVersions.sort(
286        [&](const VersionPath& a, const VersionPath& b) -> bool { return a.version < b.version; });
287}
288
289static SlangResult _findVersionsWithVSWhere(
290    const VersionInfo* versionInfo,
291    List<WinVisualStudioUtil::VersionPath>& outVersions)
292{
293    typedef WinVisualStudioUtil::VersionPath VersionPath;
294
295    CommandLine cmd;
296
297    // Lookup directly %ProgramFiles(x86)% path
298    // https://docs.microsoft.com/en-us/windows/desktop/api/shlobj_core/nf-shlobj_core-shgetfolderpatha
299    HWND hwnd = GetConsoleWindow();
300
301    char programFilesPath[_MAX_PATH];
302    SHGetFolderPathA(hwnd, CSIDL_PROGRAM_FILESX86, NULL, 0, programFilesPath);
303
304    String vswherePath = programFilesPath;
305    vswherePath.append("\\Microsoft Visual Studio\\Installer\\vswhere");
306
307    cmd.setExecutableLocation(ExecutableLocation(vswherePath));
308
309    // Using -? we can find out vswhere options.
310
311    // Previous args - works but returns multiple versions, without listing what version is
312    // associated with which path or the order.
313    // String args[] = { "-version", versionName, "-requires",
314    // "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", ""-property", "installationPath" };
315
316    // Use JSON parsing, we can verify the versions for a path, otherwise multiple versions are
317    // returned not just the version specified. The ordering isn't defined (and -sort doesn't appear
318    // to work)
319
320    SemanticVersion requiredVersion;
321    if (versionInfo)
322    {
323        StringBuilder versionName;
324        versionInfo->version.append(versionName);
325
326        cmd.addArg("-version");
327        cmd.addArg(versionName);
328    }
329
330    // Add other args
331    {
332        // TODO(JS):
333        // For arm targets will probably need something different for tooling
334        String args[] = {
335            "-format",
336            "json",
337            "-utf8",
338            "-requires",
339            "Microsoft.VisualStudio.Component.VC.Tools.x86.x64"};
340        cmd.addArgs(args, SLANG_COUNT_OF(args));
341    }
342
343    // We are going to use JSON parser to extract the info
344    SourceManager sourceManager;
345    sourceManager.initialize(nullptr, nullptr);
346    DiagnosticSink sink(&sourceManager, nullptr);
347
348    RefPtr<JSONContainer> container = new JSONContainer(&sourceManager);
349
350    ExecuteResult exeRes;
351    SLANG_RETURN_ON_FAIL(ProcessUtil::execute(cmd, exeRes));
352
353    JSONValue jsonRoot;
354    SLANG_RETURN_ON_FAIL(_parseJson(exeRes.standardOutput, &sink, container, jsonRoot));
355
356    // Search through the array...
357    if (jsonRoot.getKind() != JSONValue::Kind::Array)
358    {
359        return SLANG_FAIL;
360    }
361
362    auto arr = container->getArray(jsonRoot);
363
364    const auto pathKey = container->getKey(UnownedStringSlice::fromLiteral("installationPath"));
365    const auto versionKey =
366        container->getKey(UnownedStringSlice::fromLiteral("installationVersion"));
367
368    // Find all the versions, that match
369    for (auto elem : arr)
370    {
371        // Get the path and the name
372        if (elem.getKind() != JSONValue::Kind::Object)
373        {
374            continue;
375        }
376
377        auto pathJsonValue = container->findObjectValue(elem, pathKey);
378        auto versionJsonValue = container->findObjectValue(elem, versionKey);
379
380        if (!pathJsonValue.isValid() || !versionJsonValue.isValid())
381        {
382            continue;
383        }
384
385        auto pathString = container->getString(pathJsonValue);
386        auto versionString = container->getString(versionJsonValue).trim();
387
388        // Extract the version
389        SemanticVersion semanticVersion;
390        if (SLANG_SUCCEEDED(_parseVersion(versionString, semanticVersion)))
391        {
392            if (!requiredVersion.isSet() || requiredVersion.m_major == semanticVersion.m_major)
393            {
394                WinVisualStudioUtil::VersionPath versionPath;
395
396                versionPath.vcvarsPath = pathString;
397                versionPath.vcvarsPath.append("\\VC\\Auxiliary\\Build\\");
398                versionPath.version = semanticVersion;
399
400                outVersions.add(versionPath);
401            }
402        }
403    }
404
405    return SLANG_OK;
406}
407
408static SlangResult _findVersionsWithRegistery(List<WinVisualStudioUtil::VersionPath>& outVersions)
409{
410    typedef WinVisualStudioUtil::VersionPath VersionPath;
411
412    const int versionCount = SLANG_COUNT_OF(s_versionInfos);
413
414    for (int i = versionCount - 1; i >= 0; --i)
415    {
416        const auto versionInfo = s_versionInfos[i];
417
418        auto version = versionInfo.version;
419
420        // Try locating via the registry
421        const Int keyIndex = _getRegistryKeyIndex(version);
422        if (keyIndex >= 0)
423        {
424            SLANG_ASSERT(keyIndex < SLANG_COUNT_OF(s_regInfos));
425
426            // Try reading the key
427            const auto& keyInfo = s_regInfos[keyIndex];
428
429            StringBuilder keyName;
430            versionInfo.version.append(keyName);
431
432            String value;
433            if (SLANG_SUCCEEDED(_readRegistryKey(keyInfo.regName, keyName.getBuffer(), value)))
434            {
435                VersionPath versionPath;
436                versionPath.version = versionInfo.version;
437                versionPath.vcvarsPath = value;
438
439                // Append
440                if (keyInfo.pathFix && keyInfo.pathFix[0] != 0)
441                {
442                    versionPath.vcvarsPath.append(keyInfo.pathFix);
443                }
444
445                outVersions.add(versionPath);
446            }
447        }
448    }
449
450    return SLANG_OK;
451}
452
453/* static */ SlangResult WinVisualStudioUtil::find(List<VersionPath>& outVersionPaths)
454{
455    outVersionPaths.clear();
456
457    List<VersionPath> regVersions;
458
459    // Find all versions with vswhere
460    _findVersionsWithVSWhere(nullptr, outVersionPaths);
461    // Find all with the registry
462    _findVersionsWithRegistery(regVersions);
463
464    // Merge
465    for (const auto& regVersion : regVersions)
466    {
467        Index foundIndex = -1;
468        if (_canUseVSWhere(regVersion.version))
469        {
470            // If there is a major version already from vswhere, we don't need to merge
471            const auto majorVersion = regVersion.version.m_major;
472            foundIndex = outVersionPaths.findFirstIndex(
473                [&](const VersionPath& cur) -> bool
474                { return cur.version.m_major == majorVersion; });
475        }
476        else
477        {
478            // See if we can find the exact version
479            foundIndex = outVersionPaths.findFirstIndex(
480                [&](const VersionPath& cur) -> bool { return cur.version == regVersion.version; });
481        }
482
483        // If it wasn't found add it.
484        if (foundIndex < 0)
485        {
486            outVersionPaths.add(regVersion);
487        }
488    }
489    // Sort
490    _orderVersions(outVersionPaths);
491    return SLANG_OK;
492}
493
494/* static */ SlangResult WinVisualStudioUtil::find(DownstreamCompilerSet* set)
495{
496    List<VersionPath> versionPaths;
497    SLANG_RETURN_ON_FAIL(find(versionPaths));
498
499    for (const auto& versionPath : versionPaths)
500    {
501        // Turn into a desc
502        const DownstreamCompilerDesc desc(SLANG_PASS_THROUGH_VISUAL_STUDIO, versionPath.version);
503
504        // If not in set add it
505        if (!set->getCompiler(desc))
506        {
507            auto compiler = new VisualStudioDownstreamCompiler(desc);
508            ComPtr<IDownstreamCompiler> compilerIntf(compiler);
509            calcExecuteCompilerArgs(versionPath, compiler->m_cmdLine);
510            set->addCompiler(compilerIntf);
511        }
512    }
513
514    return SLANG_OK;
515}
516
517/* static */ void WinVisualStudioUtil::calcExecuteCompilerArgs(
518    const VersionPath& versionPath,
519    CommandLine& outCmdLine)
520{
521    // To invoke cl we need to run the suitable vcvars. In order to run this we have to have MS
522    // CommandLine. So here we build up a cl command line that is run by first running vcvars, and
523    // then executing cl with the parameters as passed to commandLine
524
525    // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessa
526    // To run a batch file, you must start the command interpreter; set lpApplicationName to cmd.exe
527    // and set lpCommandLine to the following arguments: /c plus the name of the batch file.
528
529    CommandLine cmdLine;
530    cmdLine.setExecutableLocation(ExecutableLocation(ExecutableLocation::Type::Name, "cmd.exe"));
531
532    {
533        String options[] = {"/q", "/c", "@prompt", "$"};
534        cmdLine.addArgs(options, SLANG_COUNT_OF(options));
535    }
536
537    cmdLine.addArg("&&");
538    cmdLine.addArg(Path::combine(versionPath.vcvarsPath, "vcvarsall.bat"));
539
540#if SLANG_PTR_IS_32
541    cmdLine.addArg("x86");
542#else
543    cmdLine.addArg("x86_amd64");
544#endif
545
546    cmdLine.addArg("&&");
547    cmdLine.addArg("cl");
548
549    outCmdLine = cmdLine;
550}
551
552/* static */ SlangResult WinVisualStudioUtil::executeCompiler(
553    const VersionPath& versionPath,
554    const CommandLine& commandLine,
555    ExecuteResult& outResult)
556{
557    CommandLine cmdLine;
558    calcExecuteCompilerArgs(versionPath, cmdLine);
559    // Append the command line options
560    cmdLine.addArgs(commandLine.m_args.getBuffer(), commandLine.m_args.getCount());
561    return ProcessUtil::execute(cmdLine, outResult);
562}
563
564} // namespace Slang