summary refs log tree commit diff stats
path: root/tools/vccexe
diff options
context:
space:
mode:
Diffstat (limited to 'tools/vccexe')
-rw-r--r--tools/vccexe/vccenv.nim47
-rw-r--r--tools/vccexe/vccexe.nim237
-rw-r--r--tools/vccexe/vccvswhere.nim47
-rw-r--r--tools/vccexe/vcvarsall.nim102
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