diff options
Diffstat (limited to 'tools/vccexe')
-rw-r--r-- | tools/vccexe/vccenv.nim | 47 | ||||
-rw-r--r-- | tools/vccexe/vccexe.nim | 237 | ||||
-rw-r--r-- | tools/vccexe/vccvswhere.nim | 47 | ||||
-rw-r--r-- | tools/vccexe/vcvarsall.nim | 102 |
4 files changed, 433 insertions, 0 deletions
diff --git a/tools/vccexe/vccenv.nim b/tools/vccexe/vccenv.nim new file mode 100644 index 000000000..216760e54 --- /dev/null +++ b/tools/vccexe/vccenv.nim @@ -0,0 +1,47 @@ +## VCC compiler backend installation discovery using Visual Studio common tools +## environment variables. + +import os + +type + VccEnvVersion* = enum ## The version of the Visual Studio C/C++ Developer Environment to load + ## Valid versions are Versions of Visual Studio that permanently set a COMNTOOLS + ## environment variable. That includes Visual Studio version up to and including + ## Visual Studio 2015 + vsUndefined = (0, ""), ## Version not specified, use latest recognized version on the system + vs90 = (90, "VS90COMNTOOLS"), ## Visual Studio 2008 + vs100 = (100, "VS100COMNTOOLS"), ## Visual Studio 2010 + vs110 = (110, "VS110COMNTOOLS"), ## Visual Studio 2012 + vs120 = (120, "VS120COMNTOOLS"), ## Visual Studio 2013 + vs140 = (140, "VS140COMNTOOLS") ## Visual Studio 2015 + +const + vcvarsallRelativePath = joinPath("..", "..", "VC", "vcvarsall.bat") ## Relative path from the COMNTOOLS path to the vcvarsall file. + +proc vccEnvVcVarsAllPath*(version: VccEnvVersion = vsUndefined): string = + ## Returns the path to the VCC Developer Command Prompt executable for the specified VCC version. + ## + ## Returns `nil` if the specified VCC compiler backend installation was not found. + ## + ## If the `version` parameter is omitted or set to `vsUndefined`, `vccEnvVcVarsAllPath` searches + ## for the latest recognizable version of the VCC tools it can find. + ## + ## `vccEnvVcVarsAllPath` uses the COMNTOOLS environment variables to find the Developer Command Prompt + ## executable path. The COMNTOOLS environment variable are permanently set when Visual Studio is installed. + ## Each version of Visual Studio has its own COMNTOOLS environment variable. E.g.: Visual Studio 2015 sets + ## The VS140COMNTOOLS environment variable. + ## + ## Note: Beginning with Visual Studio 2017, the installers no longer set environment variables to allow for + ## multiple side-by-side installations of Visual Studio. Therefore, `vccEnvVcVarsAllPath` cannot be used + ## to detect the VCC Developer Command Prompt executable path for Visual Studio 2017 and later. + + if version == vsUndefined: + for tryVersion in [vs140, vs120, vs110, vs100, vs90]: + let tryPath = vccEnvVcVarsAllPath(tryVersion) + if tryPath.len > 0: + return tryPath + else: # Specific version requested + let key = $version + let val = getEnv key + if val.len > 0: + result = try: expandFilename(joinPath(val, vcvarsallRelativePath)) except OSError: "" diff --git a/tools/vccexe/vccexe.nim b/tools/vccexe/vccexe.nim new file mode 100644 index 000000000..2a43f7422 --- /dev/null +++ b/tools/vccexe/vccexe.nim @@ -0,0 +1,237 @@ +import strutils, strtabs, os, osproc, vcvarsall, vccenv, vccvswhere + +type + VccVersion* = enum ## VCC compiler backend versions + vccUndefined = 0, ## VCC version undefined, resolves to the latest recognizable VCC version + vcc90 = vs90 ## Visual Studio 2008 (Version 9.0) + vcc100 = vs100 ## Visual Studio 2010 (Version 10.0) + vcc110 = vs110 ## Visual Studio 2012 (Version 11.0) + vcc120 = vs120 ## Visual Studio 2013 (Version 12.0) + vcc140 = vs140 ## Visual Studio 2015 (Version 14.0) + +proc discoverVccVcVarsAllPath*(version: VccVersion = vccUndefined): string = + ## Returns the path to the vcvarsall utility of the specified VCC compiler backend. + ## + ## version + ## The specific version of the VCC compiler backend to discover. + ## Defaults to the latest recognized VCC compiler backend that is found on the system. + ## + ## Returns `nil` if the VCC compiler backend discovery failed. + + # Attempt discovery using vswhere utility (VS 2017 and later) if no version specified. + if version == vccUndefined: + result = vccVswhereVcVarsAllPath() + if result.len > 0: + return + + # Attempt discovery through VccEnv + # (Trying Visual Studio Common Tools Environment Variables) + result = vccEnvVcVarsAllPath(cast[VccEnvVersion](version)) + if result.len > 0: + return + + # All attempts to discover vcc failed + +const + vccversionPrefix = "--vccversion" + printPathPrefix = "--printPath" + vcvarsallPrefix = "--vcvarsall" + commandPrefix = "--command" + noCommandPrefix = "--noCommand" + platformPrefix = "--platform" + sdktypePrefix = "--sdktype" + sdkversionPrefix = "--sdkversion" + vctoolsetPrefix = "--vctoolset" + verbosePrefix = "--verbose" + + vccversionSepIdx = vccversionPrefix.len + vcvarsallSepIdx = vcvarsallPrefix.len + commandSepIdx = commandPrefix.len + platformSepIdx = platformPrefix.len + sdktypeSepIdx = sdktypePrefix.len + sdkversionSepIdx = sdkversionPrefix.len + vctoolsetSepIdx = vctoolsetPrefix.len + + vcvarsallDefaultPath = "vcvarsall.bat" + + helpText = """ ++-----------------------------------------------------------------+ +| Microsoft C/C++ compiler wrapper for Nim | +| & | +| Microsoft C/C++ Compiler Discovery Utility | +| (c) 2017 Fredrik Hoeisaether Rasch | ++-----------------------------------------------------------------+ + +Usage: + vccexe [options] [compileroptions] +Options: + --vccversion:<v> Optionally specify the VCC version to discover + <v>: 0, 90, 100, 110, 120, 140 + If <v> is omitted, attempts to discover the latest + installed version. <v>: 0, 90, 100, 110, 120, 140 + A value of 0 will discover the latest installed SDK + Multiple values can be specified, separated by , + --printPath Print the discovered path of the vcvarsall utility + of the VCC version specified with the --vccversion argument. + For each specified version the utility prints a line with the + following format: <version>: <path> + --noCommand Flag to suppress VCC secondary command execution + Useful in conjunction with --vccversion and --printPath to + only perform VCC discovery, but without executing VCC tools + --vcvarsall:<path> Path to the Developer Command Prompt utility vcvarsall.bat that selects + the appropriate development settings. + Usual path for Visual Studio 2015 and below: + %VSInstallDir%\VC\vcvarsall + Usual path for Visual Studio 2017 and above: + %VSInstallDir%\VC\Auxiliary\Build\vcvarsall + --command:<exec> Specify the command to run once the development environment is loaded. + <exec> can be any command-line argument. Any arguments not recognized by vccexe + are passed on as arguments to this command. + cl.exe is invoked by default if this argument is omitted. + --platform:<arch> Specify the Compiler Platform Tools architecture + <arch>: x86 | amd64 | arm | x86_amd64 | x86_arm | amd64_x86 | amd64_arm + Values with two architectures (like x86_amd64) specify the architecture + of the cross-platform compiler (e.g. x86) and the target it compiles to (e.g. amd64). + --sdktype:<type> Specify the SDK flavor to use. Defaults to the Desktop SDK. + <type>: {empty} | store | uwp | onecore + --sdkversion:<v> Use a specific Windows SDK version: + <v> is either the full Windows 10 SDK version number or + "8.1" to use the windows 8.1 SDK + --verbose Echoes the command line for loading the Developer Command Prompt + and the command line passed on to the secondary command. + --vctoolset Optionally specifies the Visual Studio compiler toolset to use. + By default, the environment is set to use the current Visual Studio compiler toolset. + +Other command line arguments are passed on to the +secondary command specified by --command or to the +Microsoft (R) C/C++ Optimizing Compiler if no secondary +command was specified +""" + +proc parseVccexeCmdLine(argseq: seq[string], + vccversionArg: var seq[string], printPathArg: var bool, + vcvarsallArg: var string, commandArg: var string, noCommandArg: var bool, + platformArg: var VccArch, sdkTypeArg: var VccPlatformType, + sdkVersionArg: var string, vctoolsetArg: var string, verboseArg: var bool, + clArgs: var seq[string]) = + ## Cannot use usual command-line argument parser here + ## Since vccexe command-line arguments are intermingled + ## with the secondary command-line arguments which have + ## a syntax that is not supported by the default nim + ## argument parser. + for wargv in argseq: + # Check whether the current argument contains -- prefix + if wargv.startsWith("@"): # Check for response file prefix + let + responsefilename = wargv.substr(1) + responsefilehandle = open(responsefilename) + responsecontent = responsefilehandle.readAll() + responseargs = parseCmdLine(responsecontent) + parseVccexeCmdLine(responseargs, vccversionArg, printPathArg, + vcvarsallArg, commandArg, noCommandArg, platformArg, sdkTypeArg, + sdkVersionArg, vctoolsetArg, verboseArg, clArgs) + elif wargv.startsWith(vccversionPrefix): # Check for vccversion + vccversionArg.add(wargv.substr(vccversionSepIdx + 1)) + elif wargv.cmpIgnoreCase(printPathPrefix) == 0: # Check for printPath + printPathArg = true + elif wargv.startsWith(vcvarsallPrefix): # Check for vcvarsall + vcvarsallArg = wargv.substr(vcvarsallSepIdx + 1) + elif wargv.startsWith(commandPrefix): # Check for command + commandArg = wargv.substr(commandSepIdx + 1) + elif wargv.cmpIgnoreCase(noCommandPrefix) == 0: # Check for noCommand + noCommandArg = true + elif wargv.startsWith(platformPrefix): # Check for platform + platformArg = parseEnum[VccArch](wargv.substr(platformSepIdx + 1)) + elif wargv.startsWith(sdktypePrefix): # Check for sdktype + sdkTypeArg = parseEnum[VccPlatformType](wargv.substr(sdktypeSepIdx + 1)) + elif wargv.startsWith(sdkversionPrefix): # Check for sdkversion + sdkVersionArg = wargv.substr(sdkversionSepIdx + 1) + elif wargv.startsWith(vctoolsetPrefix): # Check for vctoolset + vctoolsetArg = wargv.substr(vctoolsetSepIdx + 1) + elif wargv.startsWith(verbosePrefix): + verboseArg = true + else: # Regular cl.exe argument -> store for final cl.exe invocation + if (wargv.len == 2) and (wargv[1] == '?'): + echo helpText + clArgs.add(wargv) + +when isMainModule: + var vccversionArg: seq[string] = @[] + var printPathArg: bool = false + var vcvarsallArg: string + var commandArg: string + var noCommandArg: bool = false + var platformArg: VccArch + var sdkTypeArg: VccPlatformType + var sdkVersionArg: string + var vctoolsetArg: string + var verboseArg: bool = false + + var clArgs: seq[string] = @[] + + let wrapperArgs = commandLineParams() + parseVccexeCmdLine(wrapperArgs, vccversionArg, printPathArg, vcvarsallArg, + commandArg, noCommandArg, platformArg, sdkTypeArg, sdkVersionArg, vctoolsetArg, + verboseArg, + clArgs) + + # Support for multiple specified versions. Attempt VCC discovery for each version + # specified, first successful discovery wins + var vccversionValue: VccVersion = vccUndefined + for vccversionItem in vccversionArg: + try: + vccversionValue = cast[VccVersion](parseInt(vccversionItem)) + except ValueError: + continue + vcvarsallArg = discoverVccVcVarsAllPath(vccversionValue) + if vcvarsallArg.len > 0: + break + # VCC version not specified, discover latest (call discover without args) + if vcvarsallArg.len < 1 and vccversionArg.len < 1: + vccversionValue = vccUndefined + vcvarsallArg = discoverVccVcVarsAllPath() + + if vcvarsallArg == "": + # Assume that default executable is in current directory or in PATH + vcvarsallArg = findExe(vcvarsallDefaultPath) + + if printPathArg: + var head = $vccversionValue + if head.len < 1: + head = "latest" + echo "$1: $2" % [head, vcvarsallArg] + + # Call vcvarsall to get the appropriate VCC process environment + var vcvars = vccVarsAll(vcvarsallArg, platformArg, sdkTypeArg, sdkVersionArg, vctoolsetArg, verboseArg) + if vcvars != nil: + for vccEnvKey, vccEnvVal in vcvars: + putEnv(vccEnvKey, vccEnvVal) + + var vccOptions = {poParentStreams} + if verboseArg: + vccOptions.incl poEchoCmd + + let currentDir = getCurrentDir() + for arg in clArgs.mitems: + if fileExists(arg): + arg = relativePath(arg, currentDir) + + # Default to the cl.exe command if no secondary command was specified + if commandArg.len < 1: + commandArg = "cl.exe" + + if not noCommandArg: + # Run VCC command with the VCC process environment + try: + let vccProcess = startProcess( + commandArg, + args = clArgs, + options = vccOptions + ) + quit vccProcess.waitForExit() + except: + if vcvarsallArg.len != 0: + echo "Hint: using " & vcvarsallArg + else: + echo "Hint: vcvarsall.bat was not found" + raise diff --git a/tools/vccexe/vccvswhere.nim b/tools/vccexe/vccvswhere.nim new file mode 100644 index 000000000..8f62f06ca --- /dev/null +++ b/tools/vccexe/vccvswhere.nim @@ -0,0 +1,47 @@ +## VCC compiler discovery using vswhere (https://github.com/Microsoft/vswhere) + +import os, osproc, strformat, strutils + +const + vswhereRelativePath = joinPath("Microsoft Visual Studio", "Installer", "vswhere.exe") + vswhereArgs = "-latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath" + vcvarsRelativePath = joinPath("VC", "Auxiliary", "Build", "vcvarsall.bat") + +proc vccVswhereExtractVcVarsAllPath(vswherePath: string): string = + ## Returns the path to `vcvarsall.bat` if Visual Studio location was obtained by executing `vswhere.exe`. + ## + ## `vcvarsall.bat` is supposed to exist under {vsPath}\VC\Auxiliary\Build directory. + ## + ## Returns "" if no appropriate `vcvarsall.bat` file was found. + + # For more detail on vswhere arguments, refer to https://github.com/microsoft/vswhere/wiki/Find-VC + let vsPath = execProcess(&"\"{vswherePath}\" {vswhereArgs}").strip() + if vsPath.len > 0: + let vcvarsallPath = joinPath(vsPath, vcvarsRelativePath) + if fileExists(vcvarsallPath): + return vcvarsallPath + +proc vccVswhereGeneratePath(envName: string): string = + ## Generate a path to vswhere.exe under "Program Files" or "Program Files (x86)" depending on envName. + ## + ## Returns "" if an environment variable specified by envName was not available. + + let val = getEnv(envName) + if val.len > 0: + result = try: expandFilename(joinPath(val, vswhereRelativePath)) except OSError: "" + +proc vccVswhereVcVarsAllPath*(): string = + ## Returns the path to `vcvarsall.bat` for the latest Visual Studio (2017 and later). + ## + ## Returns "" if no recognizable Visual Studio installation was found. + ## + ## Note: Beginning with Visual Studio 2017, the installers no longer set environment variables to allow for + ## multiple side-by-side installations of Visual Studio. Therefore, `vccEnvVcVarsAllPath` cannot be used + ## to detect the VCC Developer Command Prompt executable path for Visual Studio 2017 and later. + + for tryEnv in ["ProgramFiles(x86)", "ProgramFiles"]: + let vswherePath = vccVswhereGeneratePath(tryEnv) + if vswherePath.len > 0 and fileExists(vswherePath): + let vcVarsAllPath = vccVswhereExtractVcVarsAllPath(vswherePath) + if vcVarsAllPath.len > 0: + return vcVarsAllPath diff --git a/tools/vccexe/vcvarsall.nim b/tools/vccexe/vcvarsall.nim new file mode 100644 index 000000000..73b103e3c --- /dev/null +++ b/tools/vccexe/vcvarsall.nim @@ -0,0 +1,102 @@ +## VCC Developer Command Prompt Loader +## +## In order for the VCC compiler backend to work properly, it requires numerous +## environment variables to be set properly for the desired architecture and compile target. +## For that purpose the VCC compiler ships with the vcvarsall utility which is an executable +## batch script that can be used to properly set up an Command Prompt environment. + +import strtabs, strutils, os, osproc + +const + comSpecEnvKey = "ComSpec" ## Environment Variable that specifies the command-line application path in Windows + ## Usually set to cmd.exe + +type + VccArch* = enum ## The VCC compile target architectures + vccarchUnspecified = "", + vccarchX86 = "x86", ## VCC for compilation against the x86 architecture. + vccarchAmd64 = "amd64", ## VCC for compilation against the amd64 architecture. + vccarchX86Amd64 = "x86_amd64", ## VCC cross-compilation tools using x86 VCC for compilation against the amd64 architecture. + vccarchX86Arm = "x86_arm", ## VCC cross-compilation tools using x86 VCC for compilation against the ARM architecture. + vccarchX86Arm64 = "x86_arm64", ## VCC cross-compilation tools using x86 VCC for compilation against the ARM (64-bit) architecture. + vccarchAmd64X86 = "amd64_x86", ## VCC cross-compilation tools using amd64 VCC for compilation against the x86 architecture. + vccarchAmd64Arm = "amd64_arm", ## VCC cross-compilation tools using amd64 VCC for compilation against the ARM architecture. + vccarchAmd64Arm64 = "amd64_arm64", ## VCC cross-compilation tools using amd64 VCC for compilation against the ARM (64-bit) architecture. + vccarchX64 = "x64", ## VCC for compilation against the x64 architecture. + vccarchX64X86 = "x64_x86", ## VCC cross-compilation tools using x64 VCC for compilation against the x86 architecture. + vccarchX64Arm = "x64_arm", ## VCC cross-compilation tools using x64 VCC for compilation against the ARM architecture. + vccarchX64Arm64 = "x64_arm64" ## VCC cross-compilation tools using x64 VCC for compilation against the ARM (64-bit) architecture. + + VccPlatformType* = enum ## The VCC platform type of the compile target + vccplatEmpty = "", ## Default (i.e. Desktop) Platfor Type + vccplatStore = "store", ## Windows Store Application + vccplatUWP = "uwp", ## Universal Windows Platform (UWP) Application + vccplatOneCore = "onecore" # Undocumented platform type in the Windows SDK, probably XBox One SDK platform type. + +proc vccVarsAll*(path: string, arch: VccArch = vccarchUnspecified, platform_type: VccPlatformType = vccplatEmpty, sdk_version, vctoolset: string = "", verbose: bool = false): StringTableRef = + ## Returns a string table containing the proper process environment to successfully execute VCC compile commands for the specified SDK version, CPU architecture and platform type. + ## + ## path + ## The path to the vcvarsall utility for VCC compiler backend. + ## arch + ## The compile target CPU architecture. Starting with Visual Studio 2017, this value must be specified and must not be set to `vccarchUnspecified`. + ## platform_type + ## The compile target Platform Type. Defaults to the Windows Desktop platform, i.e. a regular Windows executable binary. + ## sdk_version + ## The Windows SDK version to use. + ## vctoolset + ## Visual Studio compiler toolset to use. + ## verbose + ## Echo the command-line passed on to the system to load the VCC environment. Defaults to `false`. + + if path == "": + return nil + + let vccvarsallpath = path + var args: seq[string] = @[] + + let archStr: string = $arch + if archStr.len > 0: + args.add(archStr) + + let platStr: string = $platform_type + if platStr.len > 0: + args.add(platStr) + + if sdk_version.len > 0: + args.add(sdk_version) + + if vctoolset.len > 0: + args.add("-vcvars_ver="&vctoolset) + + let argStr = args.join " " + + var vcvarsExec: string + if argStr.len > 0: + vcvarsExec = "\"$1\" $2" % [vccvarsallpath, argStr] + else: + vcvarsExec = "\"$1\"" % vccvarsallpath + + var comSpecCmd = getenv comSpecEnvKey + if comSpecCmd.len < 1: + comSpecCmd = "cmd" + + # Run the Windows Command Prompt with the /C argument + # Execute vcvarsall with its command-line arguments + # and then execute the SET command to list all environment variables + let comSpecExec = "\"$1\" /C \"$2 && SET\"" % [comSpecCmd, vcvarsExec] + var comSpecOpts = {poEvalCommand, poDaemon, poStdErrToStdOut} + if verbose: + comSpecOpts.incl poEchoCmd + let comSpecOut = execProcess(comSpecExec, options = comSpecOpts) + + result = newStringTable(modeCaseInsensitive) + + # Parse the output of the final SET command to construct a String Table + # with the appropriate environment variables + for line in comSpecOut.splitLines: + let idx = line.find('=') + if idx > 0: + result[line[0..(idx - 1)]] = line[(idx + 1)..(line.len - 1)] + elif verbose: + echo line |