1 /**
2  * When compiling on Windows with the Microsoft toolchain, try to detect the Visual Studio setup.
3  *
4  * Copyright:   Copyright (C) 1999-2023 by The D Language Foundation, All Rights Reserved
5  * Authors:     $(LINK2 https://www.digitalmars.com, Walter Bright)
6  * License:     $(LINK2 https://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
7  * Source:      $(LINK2 https://github.com/dlang/dmd/blob/master/src/dmd/link.d, _vsoptions.d)
8  * Documentation:  https://dlang.org/phobos/dmd_vsoptions.html
9  * Coverage:    https://codecov.io/gh/dlang/dmd/src/master/src/dmd/vsoptions.d
10  */
11 
12 module dmd.vsoptions;
13 
14 version (Windows):
15 import core.stdc.ctype;
16 import core.stdc.stdlib;
17 import core.stdc.string;
18 import core.stdc.wchar_;
19 import core.sys.windows.winbase;
20 import core.sys.windows.windef;
21 import core.sys.windows.winreg;
22 
23 import dmd.root.env;
24 import dmd.root.file;
25 import dmd.root.filename;
26 import dmd.common.outbuffer;
27 import dmd.root.rmem;
28 import dmd.root.string : toDString;
29 
30 private immutable supportedPre2017Versions = ["14.0", "12.0", "11.0", "10.0", "9.0"];
31 
32 extern(C++) struct VSOptions
33 {
34     // evaluated once at startup, reflecting the result of vcvarsall.bat
35     //  from the current environment or the latest Visual Studio installation
36     const(char)* WindowsSdkDir;
37     const(char)* WindowsSdkVersion;
38     const(char)* UCRTSdkDir;
39     const(char)* UCRTVersion;
40     const(char)* VSInstallDir;
41     const(char)* VCInstallDir;
42     const(char)* VCToolsInstallDir; // used by VS 2017+
43 
44     /**
45      * fill member variables from environment or registry
46      */
47     void initialize()
48     {
49         detectWindowsSDK();
50         detectUCRT();
51         detectVSInstallDir();
52         detectVCInstallDir();
53         detectVCToolsInstallDir();
54     }
55 
56     /**
57      * set all members to null. Used if we detect a VS installation but end up
58      * falling back on lld-link.exe
59      */
60     void uninitialize()
61     {
62         WindowsSdkDir = null;
63         WindowsSdkVersion = null;
64         UCRTSdkDir = null;
65         UCRTVersion = null;
66         VSInstallDir = null;
67         VCInstallDir = null;
68         VCToolsInstallDir = null;
69     }
70 
71     /**
72      * retrieve the name of the default C runtime library
73      * Params:
74      *   x64 = target architecture (x86 if false)
75      * Returns:
76      *   name of the default C runtime library
77      */
78     const(char)* defaultRuntimeLibrary(bool x64)
79     {
80         if (VCInstallDir is null)
81         {
82             detectVCInstallDir();
83             detectVCToolsInstallDir();
84         }
85         if (getVCLibDir(x64))
86             return "libcmt";
87         else
88             return "msvcrt120"; // mingw replacement
89     }
90 
91     /**
92      * retrieve options to be passed to the Microsoft linker
93      * Params:
94      *   x64 = target architecture (x86 if false)
95      * Returns:
96      *   allocated string of options to add to the linker command line
97      */
98     const(char)* linkOptions(bool x64)
99     {
100         OutBuffer cmdbuf;
101         if (auto vclibdir = getVCLibDir(x64))
102         {
103             cmdbuf.writestring(" /LIBPATH:\"");
104             cmdbuf.writestring(vclibdir);
105             cmdbuf.writeByte('\"');
106 
107             if (FileName.exists(FileName.combine(vclibdir, "legacy_stdio_definitions.lib")))
108             {
109                 // VS2015 or later use UCRT
110                 cmdbuf.writestring(" legacy_stdio_definitions.lib");
111                 if (auto p = getUCRTLibPath(x64))
112                 {
113                     cmdbuf.writestring(" /LIBPATH:\"");
114                     cmdbuf.writestring(p);
115                     cmdbuf.writeByte('\"');
116                 }
117             }
118         }
119         if (auto p = getSDKLibPath(x64))
120         {
121             cmdbuf.writestring(" /LIBPATH:\"");
122             cmdbuf.writestring(p);
123             cmdbuf.writeByte('\"');
124         }
125         if (auto p = getenv("DXSDK_DIR"w))
126         {
127             // support for old DX SDK installations
128             cmdbuf.writestring(" /LIBPATH:\"");
129             cmdbuf.writestring(p);
130             cmdbuf.writestring(x64 ? `\Lib\x64"` : `\Lib\x86"`);
131             mem.xfree(p);
132         }
133         return cmdbuf.extractChars();
134     }
135 
136     /**
137      * retrieve path to the Microsoft linker executable
138      * also modifies PATH environment variable if necessary to find conditionally loaded DLLs
139      * Params:
140      *   x64 = target architecture (x86 if false)
141      * Returns:
142      *   absolute path to link.exe, just "link.exe" if not found
143      */
144     const(char)* linkerPath(bool x64)
145     {
146         const(char)* addpath;
147         if (auto p = getVCBinDir(x64, addpath))
148         {
149             OutBuffer cmdbuf;
150             cmdbuf.writestring(p);
151             cmdbuf.writestring(r"\link.exe");
152             if (addpath)
153             {
154                 // debug info needs DLLs from $(VSInstallDir)\Common7\IDE for most linker versions
155                 //  so prepend it too the PATH environment variable
156                 char* path = getenv("PATH"w);
157                 const pathlen = strlen(path);
158                 const addpathlen = strlen(addpath);
159 
160                 const length = addpathlen + 1 + pathlen;
161                 char* npath = cast(char*)mem.xmalloc(length);
162                 memcpy(npath, addpath, addpathlen);
163                 npath[addpathlen] = ';';
164                 memcpy(npath + addpathlen + 1, path, pathlen);
165                 if (putenvRestorable("PATH", npath[0 .. length]))
166                     assert(0);
167                 mem.xfree(npath);
168                 mem.xfree(path);
169             }
170             return cmdbuf.extractChars();
171         }
172 
173         // try lld-link.exe alongside dmd.exe
174         char[MAX_PATH + 1] dmdpath = void;
175         const len = GetModuleFileNameA(null, dmdpath.ptr, dmdpath.length);
176         if (len <= MAX_PATH)
177         {
178             auto lldpath = FileName.replaceName(dmdpath[0 .. len], "lld-link.exe");
179             if (FileName.exists(lldpath))
180                 return lldpath.ptr;
181         }
182 
183         // search PATH to avoid createProcess preferring "link.exe" from the dmd folder
184         if (auto p = FileName.searchPath(getenv("PATH"w), "link.exe"[], false))
185             return p.ptr;
186         return "link.exe";
187     }
188 
189     /**
190     * retrieve path to the Microsoft compiler executable
191     * Params:
192     *   x64 = target architecture (x86 if false)
193     * Returns:
194     *   absolute path to cl.exe, just "cl.exe" if not found
195     */
196     const(char)* compilerPath(bool x64)
197     {
198         const(char)* addpath;
199         if (auto p = getVCBinDir(x64, addpath))
200         {
201             OutBuffer cmdbuf;
202             cmdbuf.writestring(p);
203             cmdbuf.writestring(r"\cl.exe");
204             return cmdbuf.extractChars();
205         }
206         return "cl.exe";
207     }
208 
209 private:
210     /**
211      * detect WindowsSdkDir and WindowsSDKVersion from environment or registry
212      */
213     void detectWindowsSDK()
214     {
215         if (WindowsSdkDir is null)
216             WindowsSdkDir = getenv("WindowsSdkDir"w);
217 
218         if (WindowsSdkDir is null)
219         {
220             WindowsSdkDir = GetRegistryString(r"Microsoft\Windows Kits\Installed Roots", "KitsRoot10"w);
221             if (WindowsSdkDir && !findLatestSDKDir(FileName.combine(WindowsSdkDir, "Include"), r"um\windows.h"))
222                 WindowsSdkDir = null;
223         }
224         if (WindowsSdkDir is null)
225         {
226             WindowsSdkDir = GetRegistryString(r"Microsoft\Microsoft SDKs\Windows\v8.1", "InstallationFolder"w);
227             if (WindowsSdkDir && !FileName.exists(FileName.combine(WindowsSdkDir, "Lib")))
228                 WindowsSdkDir = null;
229         }
230         if (WindowsSdkDir is null)
231         {
232             WindowsSdkDir = GetRegistryString(r"Microsoft\Microsoft SDKs\Windows\v8.0", "InstallationFolder"w);
233             if (WindowsSdkDir && !FileName.exists(FileName.combine(WindowsSdkDir, "Lib")))
234                 WindowsSdkDir = null;
235         }
236         if (WindowsSdkDir is null)
237         {
238             WindowsSdkDir = GetRegistryString(r"Microsoft\Microsoft SDKs\Windows", "CurrentInstallationFolder"w);
239             if (WindowsSdkDir && !FileName.exists(FileName.combine(WindowsSdkDir, "Lib")))
240                 WindowsSdkDir = null;
241         }
242 
243         if (WindowsSdkVersion is null)
244             WindowsSdkVersion = getenv("WindowsSdkVersion"w);
245 
246         if (WindowsSdkVersion is null && WindowsSdkDir !is null)
247         {
248             const(char)* rootsDir = FileName.combine(WindowsSdkDir, "Include");
249             WindowsSdkVersion = findLatestSDKDir(rootsDir, r"um\windows.h");
250         }
251     }
252 
253     /**
254      * detect UCRTSdkDir and UCRTVersion from environment or registry
255      */
256     void detectUCRT()
257     {
258         if (UCRTSdkDir is null)
259             UCRTSdkDir = getenv("UniversalCRTSdkDir"w);
260 
261         if (UCRTSdkDir is null)
262             UCRTSdkDir = GetRegistryString(r"Microsoft\Windows Kits\Installed Roots", "KitsRoot10"w);
263 
264         if (UCRTVersion is null)
265             UCRTVersion = getenv("UCRTVersion"w);
266 
267         if (UCRTVersion is null && UCRTSdkDir !is null)
268         {
269             const(char)* rootsDir = FileName.combine(UCRTSdkDir, "Lib");
270             UCRTVersion = findLatestSDKDir(rootsDir, r"ucrt\x86\libucrt.lib");
271         }
272     }
273 
274     /**
275      * detect VSInstallDir from environment or registry
276      */
277     void detectVSInstallDir()
278     {
279         if (VSInstallDir is null)
280             VSInstallDir = getenv("VSINSTALLDIR"w);
281 
282         if (VSInstallDir is null)
283             VSInstallDir = detectVSInstallDirViaCOM();
284 
285         if (VSInstallDir is null)
286             VSInstallDir = GetRegistryString(r"Microsoft\VisualStudio\SxS\VS7", "15.0"w); // VS2017
287 
288         if (VSInstallDir is null)
289         {
290             wchar[260] buffer = void;
291             // VS Build Tools 2017 (default installation path)
292             const numWritten = ExpandEnvironmentStringsW(r"%ProgramFiles(x86)%\Microsoft Visual Studio\2017\BuildTools"w.ptr, buffer.ptr, buffer.length);
293             if (numWritten <= buffer.length && exists(buffer.ptr))
294                 VSInstallDir = toNarrowStringz(buffer[0 .. numWritten - 1]).ptr;
295         }
296 
297         if (VSInstallDir is null)
298             foreach (ver; supportedPre2017Versions)
299             {
300                 VSInstallDir = GetRegistryString(FileName.combine(r"Microsoft\VisualStudio", ver), "InstallDir"w);
301                 if (VSInstallDir)
302                     break;
303             }
304     }
305 
306     /**
307      * detect VCInstallDir from environment or registry
308      */
309     void detectVCInstallDir()
310     {
311         if (VCInstallDir is null)
312             VCInstallDir = getenv("VCINSTALLDIR"w);
313 
314         if (VCInstallDir is null)
315             if (VSInstallDir && FileName.exists(FileName.combine(VSInstallDir, "VC")))
316                 VCInstallDir = FileName.combine(VSInstallDir, "VC");
317 
318         // detect from registry (build tools?)
319         if (VCInstallDir is null)
320             foreach (ver; supportedPre2017Versions)
321             {
322                 auto regPath = FileName.buildPath(r"Microsoft\VisualStudio", ver, r"Setup\VC");
323                 VCInstallDir = GetRegistryString(regPath, "ProductDir"w);
324                 FileName.free(regPath.ptr);
325                 if (VCInstallDir)
326                     break;
327             }
328     }
329 
330     /**
331      * detect VCToolsInstallDir from environment or registry (only used by VC 2017)
332      */
333     void detectVCToolsInstallDir()
334     {
335         if (VCToolsInstallDir is null)
336             VCToolsInstallDir = getenv("VCTOOLSINSTALLDIR"w);
337 
338         if (VCToolsInstallDir is null && VCInstallDir)
339         {
340             const(char)* defverFile = FileName.combine(VCInstallDir, r"Auxiliary\Build\Microsoft.VCToolsVersion.default.txt");
341             if (!FileName.exists(defverFile)) // file renamed with VS2019 Preview 2
342                 defverFile = FileName.combine(VCInstallDir, r"Auxiliary\Build\Microsoft.VCToolsVersion.v142.default.txt");
343             if (FileName.exists(defverFile))
344             {
345                 // VS 2017
346                 auto readResult = File.read(defverFile.toDString); // adds sentinel 0 at end of file
347                 if (readResult.success)
348                 {
349                     auto ver = cast(char*)readResult.buffer.data.ptr;
350                     // trim version number
351                     while (*ver && isspace(*ver))
352                         ver++;
353                     auto p = ver;
354                     while (*p == '.' || (*p >= '0' && *p <= '9'))
355                         p++;
356                     *p = 0;
357 
358                     if (ver && *ver)
359                         VCToolsInstallDir = FileName.buildPath(VCInstallDir.toDString, r"Tools\MSVC", ver.toDString).ptr;
360                 }
361             }
362         }
363     }
364 
365 public:
366     /**
367      * get Visual C bin folder
368      * Params:
369      *   x64 = target architecture (x86 if false)
370      *   addpath = [out] path that needs to be added to the PATH environment variable
371      * Returns:
372      *   folder containing the VC executables
373      *
374      * Selects the binary path according to the host and target OS, but verifies
375      * that link.exe exists in that folder and falls back to 32-bit host/target if
376      * missing
377      * Note: differences for the linker binaries are small, they all
378      * allow cross compilation
379      */
380     const(char)* getVCBinDir(bool x64, out const(char)* addpath) const
381     {
382         alias linkExists = returnDirIfContainsFile!"link.exe";
383 
384         const bool isHost64 = isWin64Host();
385         if (VCToolsInstallDir !is null)
386         {
387             const vcToolsInstallDir = VCToolsInstallDir.toDString;
388             if (isHost64)
389             {
390                 if (x64)
391                 {
392                     if (auto p = linkExists(vcToolsInstallDir, r"bin\HostX64\x64"))
393                         return p;
394                     // in case of missing linker, prefer other host binaries over other target architecture
395                 }
396                 else
397                 {
398                     if (auto p = linkExists(vcToolsInstallDir, r"bin\HostX64\x86"))
399                     {
400                         addpath = FileName.combine(vcToolsInstallDir, r"bin\HostX64\x64").ptr;
401                         return p;
402                     }
403                 }
404             }
405             if (x64)
406             {
407                 if (auto p = linkExists(vcToolsInstallDir, r"bin\HostX86\x64"))
408                 {
409                     addpath = FileName.combine(vcToolsInstallDir, r"bin\HostX86\x86").ptr;
410                     return p;
411                 }
412             }
413             if (auto p = linkExists(vcToolsInstallDir, r"bin\HostX86\x86"))
414                 return p;
415         }
416         if (VCInstallDir !is null)
417         {
418             const vcInstallDir = VCInstallDir.toDString;
419             if (isHost64)
420             {
421                 if (x64)
422                 {
423                     if (auto p = linkExists(vcInstallDir, r"bin\amd64"))
424                         return p;
425                     // in case of missing linker, prefer other host binaries over other target architecture
426                 }
427                 else
428                 {
429                     if (auto p = linkExists(vcInstallDir, r"bin\amd64_x86"))
430                     {
431                         addpath = FileName.combine(vcInstallDir, r"bin\amd64").ptr;
432                         return p;
433                     }
434                 }
435             }
436 
437             if (VSInstallDir)
438                 addpath = FileName.combine(VSInstallDir.toDString, r"Common7\IDE").ptr;
439             else
440                 addpath = FileName.combine(vcInstallDir, r"bin").ptr;
441 
442             if (x64)
443                 if (auto p = linkExists(vcInstallDir, r"x86_amd64"))
444                     return p;
445 
446             if (auto p = linkExists(vcInstallDir, r"bin\HostX86\x86"))
447                 return p;
448         }
449         return null;
450     }
451 
452     /**
453     * get Visual C Library folder
454     * Params:
455     *   x64 = target architecture (x86 if false)
456     * Returns:
457     *   folder containing the VC runtime libraries
458     */
459     const(char)* getVCLibDir(bool x64) const
460     {
461         const(char)* proposed;
462 
463         if (VCToolsInstallDir !is null)
464             proposed = FileName.combine(VCToolsInstallDir, x64 ? r"lib\x64" : r"lib\x86");
465         else if (VCInstallDir !is null)
466             proposed = FileName.combine(VCInstallDir, x64 ? r"lib\amd64" : "lib");
467 
468         // Due to the possibility of VS being installed with VC directory without the libraries
469         // we must check that the expected directory does exist.
470         // It is possible that this isn't the only location a file check is required.
471         return FileName.exists(proposed) ? proposed : null;
472     }
473 
474     /**
475      * get the path to the universal CRT libraries
476      * Params:
477      *   x64 = target architecture (x86 if false)
478      * Returns:
479      *   folder containing the universal CRT libraries
480      */
481     const(char)* getUCRTLibPath(bool x64) const
482     {
483         if (UCRTSdkDir && UCRTVersion)
484            return FileName.buildPath(UCRTSdkDir.toDString, "Lib", UCRTVersion.toDString, x64 ? r"ucrt\x64" : r"ucrt\x86").ptr;
485         return null;
486     }
487 
488     /**
489      * get the path to the Windows SDK CRT libraries
490      * Params:
491      *   x64 = target architecture (x86 if false)
492      * Returns:
493      *   folder containing the Windows SDK libraries
494      */
495     const(char)* getSDKLibPath(bool x64) const
496     {
497         if (WindowsSdkDir)
498         {
499             alias kernel32Exists = returnDirIfContainsFile!"kernel32.lib";
500 
501             const arch = x64 ? "x64" : "x86";
502             const sdk = FileName.combine(WindowsSdkDir.toDString, "lib");
503             if (WindowsSdkVersion)
504             {
505                 if (auto p = kernel32Exists(sdk, WindowsSdkVersion.toDString, "um", arch)) // SDK 10.0
506                     return p;
507             }
508             if (auto p = kernel32Exists(sdk, r"win8\um", arch)) // SDK 8.0
509                 return p;
510             if (auto p = kernel32Exists(sdk, r"winv6.3\um", arch)) // SDK 8.1
511                 return p;
512             if (x64)
513             {
514                 if (auto p = kernel32Exists(sdk, arch)) // SDK 7.1 or earlier
515                     return p;
516             }
517             else
518             {
519                 if (auto p = kernel32Exists(sdk)) // SDK 7.1 or earlier
520                     return p;
521             }
522         }
523 
524         // try mingw fallback relative to phobos library folder that's part of LIB
525         if (auto p = FileName.searchPath(getenv("LIB"w), r"mingw\kernel32.lib"[], false))
526             return FileName.path(p).ptr;
527 
528         return null;
529     }
530 
531 private:
532 extern(D):
533 
534     // iterate through subdirectories named by SDK version in baseDir and return the
535     //  one with the largest version that also contains the test file
536     static const(char)* findLatestSDKDir(const(char)* baseDir, string testfile)
537     {
538         import dmd.common.string : SmallBuffer, toWStringz;
539 
540         const(char)* pattern = FileName.combine(baseDir, "*");
541         wchar[1024] support = void;
542         auto buf = SmallBuffer!wchar(support.length, support);
543         wchar* wpattern = toWStringz(pattern.toDString, buf).ptr;
544         FileName.free(pattern);
545 
546         WIN32_FIND_DATAW fileinfo;
547         HANDLE h = FindFirstFileW(wpattern, &fileinfo);
548         if (h == INVALID_HANDLE_VALUE)
549             return null;
550 
551         const dBaseDir = baseDir.toDString;
552 
553         char* res;
554         do
555         {
556             if (fileinfo.cFileName[0] >= '1' && fileinfo.cFileName[0] <= '9')
557             {
558                 char[] name = toNarrowStringz(fileinfo.cFileName.ptr.toDString);
559                 // FIXME: proper version strings comparison
560                 if ((!res || strcmp(res, name.ptr) < 0) &&
561                     FileName.exists(FileName.buildPath(dBaseDir, name, testfile)))
562                 {
563                     if (res)
564                         mem.xfree(res);
565                     res = name.ptr;
566                 }
567                 else
568                     mem.xfree(name.ptr);
569             }
570         }
571         while(FindNextFileW(h, &fileinfo));
572 
573         if (!FindClose(h))
574             res = null;
575         return res;
576     }
577 
578     pragma(lib, "advapi32.lib");
579 
580     /**
581      * read a string from the 32-bit registry
582      * Params:
583      *  softwareKeyPath = path below HKLM\SOFTWARE
584      *  valueName       = name of the value to read
585      * Returns:
586      *  the registry value if it exists and has string type
587      */
588     const(char)* GetRegistryString(const(char)[] softwareKeyPath, wstring valueName) const
589     {
590         enum x64hive = false; // VS registry entries always in 32-bit hive
591 
592         version(Win64)
593             enum prefix = x64hive ? r"SOFTWARE\" : r"SOFTWARE\WOW6432Node\";
594         else
595             enum prefix = r"SOFTWARE\";
596 
597         char[260] regPath = void;
598         assert(prefix.length + softwareKeyPath.length < regPath.length);
599 
600         regPath[0 .. prefix.length] = prefix;
601         regPath[prefix.length .. prefix.length + softwareKeyPath.length] = softwareKeyPath;
602         regPath[prefix.length + softwareKeyPath.length] = 0;
603 
604         enum KEY_WOW64_64KEY = 0x000100; // not defined in core.sys.windows.winnt due to restrictive version
605         enum KEY_WOW64_32KEY = 0x000200;
606         HKEY key;
607         LONG lRes = RegOpenKeyExA(HKEY_LOCAL_MACHINE, regPath.ptr, (x64hive ? KEY_WOW64_64KEY : KEY_WOW64_32KEY), KEY_READ, &key);
608         if (FAILED(lRes))
609             return null;
610         scope(exit) RegCloseKey(key);
611 
612         wchar[260] buf = void;
613         DWORD size = buf.sizeof;
614         DWORD type;
615         int hr = RegQueryValueExW(key, valueName.ptr, null, &type, cast(ubyte*) buf.ptr, &size);
616         if (type != REG_SZ || size == 0)
617             return null;
618 
619         wchar* wideValue = buf.ptr;
620         scope(exit) wideValue != buf.ptr && mem.xfree(wideValue);
621         if (hr == ERROR_MORE_DATA)
622         {
623             wideValue = cast(wchar*) mem.xmalloc_noscan(size);
624             hr = RegQueryValueExW(key, valueName.ptr, null, &type, cast(ubyte*) wideValue, &size);
625         }
626         if (hr != 0)
627             return null;
628 
629         auto wideLength = size / wchar.sizeof;
630         if (wideValue[wideLength - 1] == 0) // may or may not be null-terminated
631             --wideLength;
632         return toNarrowStringz(wideValue[0 .. wideLength]).ptr;
633     }
634 
635     /***
636      * get architecture of host OS
637      */
638     static bool isWin64Host()
639     {
640         version (Win64)
641         {
642             return true;
643         }
644         else
645         {
646             // running as a 32-bit process on a 64-bit host?
647             alias fnIsWow64Process = extern(Windows) BOOL function(HANDLE, PBOOL);
648             __gshared fnIsWow64Process pIsWow64Process;
649 
650             if (!pIsWow64Process)
651             {
652                 //IsWow64Process is not available on all supported versions of Windows.
653                 pIsWow64Process = cast(fnIsWow64Process) GetProcAddress(GetModuleHandleA("kernel32"), "IsWow64Process");
654                 if (!pIsWow64Process)
655                     return false;
656             }
657             BOOL bIsWow64 = FALSE;
658             if (!pIsWow64Process(GetCurrentProcess(), &bIsWow64))
659                 return false;
660 
661             return bIsWow64 != 0;
662         }
663     }
664 }
665 
666 private:
667 
668 inout(wchar)[] toDString(inout(wchar)* s)
669 {
670     return s ? s[0 .. wcslen(s)] : null;
671 }
672 
673 extern(C) wchar* _wgetenv(const(wchar)* name);
674 
675 char* getenv(wstring name)
676 {
677     if (auto wide = _wgetenv(name.ptr))
678         return toNarrowStringz(wide.toDString).ptr;
679     return null;
680 }
681 
682 const(char)* returnDirIfContainsFile(string fileName)(scope const(char)[][] dirs...)
683 {
684     auto dirPath = FileName.buildPath(dirs);
685 
686     auto filePath = FileName.combine(dirPath, fileName);
687     scope(exit) FileName.free(filePath.ptr);
688 
689     if (FileName.exists(filePath))
690         return dirPath.ptr;
691 
692     FileName.free(dirPath.ptr);
693     return null;
694 }
695 
696 extern (C) int _waccess(const(wchar)* _FileName, int _AccessMode);
697 
698 bool exists(const(wchar)* path)
699 {
700     return _waccess(path, 0) == 0;
701 }
702 
703 ///////////////////////////////////////////////////////////////////////
704 // COM interfaces to find VS2017+ installations
705 import core.sys.windows.com;
706 import core.sys.windows.wtypes : BSTR;
707 import core.sys.windows.oleauto : SysFreeString, SysStringLen;
708 
709 pragma(lib, "ole32.lib");
710 pragma(lib, "oleaut32.lib");
711 
712 interface ISetupInstance : IUnknown
713 {
714     // static const GUID iid = uuid("B41463C3-8866-43B5-BC33-2B0676F7F42E");
715     static const GUID iid = { 0xB41463C3, 0x8866, 0x43B5, [ 0xBC, 0x33, 0x2B, 0x06, 0x76, 0xF7, 0xF4, 0x2E ] };
716 
717     int GetInstanceId(BSTR* pbstrInstanceId);
718     int GetInstallDate(LPFILETIME pInstallDate);
719     int GetInstallationName(BSTR* pbstrInstallationName);
720     int GetInstallationPath(BSTR* pbstrInstallationPath);
721     int GetInstallationVersion(BSTR* pbstrInstallationVersion);
722     int GetDisplayName(LCID lcid, BSTR* pbstrDisplayName);
723     int GetDescription(LCID lcid, BSTR* pbstrDescription);
724     int ResolvePath(LPCOLESTR pwszRelativePath, BSTR* pbstrAbsolutePath);
725 }
726 
727 interface IEnumSetupInstances : IUnknown
728 {
729     // static const GUID iid = uuid("6380BCFF-41D3-4B2E-8B2E-BF8A6810C848");
730 
731     int Next(ULONG celt, ISetupInstance* rgelt, ULONG* pceltFetched);
732     int Skip(ULONG celt);
733     int Reset();
734     int Clone(IEnumSetupInstances* ppenum);
735 }
736 
737 interface ISetupConfiguration : IUnknown
738 {
739     // static const GUID iid = uuid("42843719-DB4C-46C2-8E7C-64F1816EFD5B");
740     static const GUID iid = { 0x42843719, 0xDB4C, 0x46C2, [ 0x8E, 0x7C, 0x64, 0xF1, 0x81, 0x6E, 0xFD, 0x5B ] };
741 
742     int EnumInstances(IEnumSetupInstances* ppEnumInstances) ;
743     int GetInstanceForCurrentProcess(ISetupInstance* ppInstance);
744     int GetInstanceForPath(LPCWSTR wzPath, ISetupInstance* ppInstance);
745 }
746 
747 const GUID iid_SetupConfiguration = { 0x177F0C4A, 0x1CD3, 0x4DE7, [ 0xA3, 0x2C, 0x71, 0xDB, 0xBB, 0x9F, 0xA3, 0x6D ] };
748 
749 const(char)* detectVSInstallDirViaCOM()
750 {
751     CoInitialize(null);
752     scope(exit) CoUninitialize();
753 
754     ISetupConfiguration setup;
755     IEnumSetupInstances instances;
756     ISetupInstance instance;
757     DWORD fetched;
758 
759     HRESULT hr = CoCreateInstance(&iid_SetupConfiguration, null, CLSCTX_ALL, &ISetupConfiguration.iid, cast(void**) &setup);
760     if (hr != S_OK || !setup)
761         return null;
762     scope(exit) setup.Release();
763 
764     if (setup.EnumInstances(&instances) != S_OK)
765         return null;
766     scope(exit) instances.Release();
767 
768     static struct WrappedBString
769     {
770         BSTR ptr;
771         this(this) @disable;
772         ~this() { SysFreeString(ptr); }
773         bool opCast(T : bool)() const { return ptr !is null; }
774         size_t length() { return SysStringLen(ptr); }
775         void moveTo(ref WrappedBString other)
776         {
777             SysFreeString(other.ptr);
778             other.ptr = ptr;
779             ptr = null;
780         }
781     }
782 
783     WrappedBString versionString, installDir;
784 
785     while (instances.Next(1, &instance, &fetched) == S_OK && fetched)
786     {
787         WrappedBString thisVersionString, thisInstallDir;
788         if (instance.GetInstallationVersion(&thisVersionString.ptr) != S_OK ||
789             instance.GetInstallationPath(&thisInstallDir.ptr) != S_OK)
790             continue;
791 
792         // FIXME: proper version strings comparison
793         //        existing versions are 15.0 to 16.5 (May 2020), problems expected in distant future
794         if (versionString && wcscmp(thisVersionString.ptr, versionString.ptr) <= 0)
795             continue; // not a newer version, skip
796 
797         const installDirLength = thisInstallDir.length;
798         const vcInstallDirLength = installDirLength + 4;
799         auto vcInstallDir = (cast(wchar*) mem.xmalloc_noscan(vcInstallDirLength * wchar.sizeof))[0 .. vcInstallDirLength];
800         scope(exit) mem.xfree(vcInstallDir.ptr);
801         vcInstallDir[0 .. installDirLength] = thisInstallDir.ptr[0 .. installDirLength];
802         vcInstallDir[installDirLength .. $] = "\\VC\0"w;
803         if (!exists(vcInstallDir.ptr))
804             continue; // Visual C++ not included, skip
805 
806         thisVersionString.moveTo(versionString);
807         thisInstallDir.moveTo(installDir);
808     }
809 
810     return !installDir ? null : toNarrowStringz(installDir.ptr[0 .. installDir.length]).ptr;
811 }