#!/usr/bin/env bash
# ################################################################################################ #
# MetaStack Solutions Ltd.                                                                         #
# ################################################################################################ #
# Microsoft C Compiler Environment Detection Script                                                #
# ################################################################################################ #
# Copyright (c) 2016, 2017, 2018, 2019, 2020 MetaStack Solutions Ltd.                              #
# ################################################################################################ #
# Author: David Allsopp                                                                            #
# 16-Feb-2016                                                                                      #
# ################################################################################################ #
# Redistribution and use in source and binary forms, with or without modification, are permitted   #
# provided that the following two conditions are met:                                              #
#     1. Redistributions of source code must retain the above copyright notice, this list of       #
#        conditions and the following disclaimer.                                                  #
#     2. Neither the name of MetaStack Solutions Ltd. nor the names of its contributors may be     #
#        used to endorse or promote products derived from this software without specific prior     #
#        written permission.                                                                       #
#                                                                                                  #
# This software is provided by the Copyright Holder 'as is' and any express or implied warranties  #
# including, but not limited to, the implied warranties of merchantability and fitness for a       #
# particular purpose are disclaimed. In no event shall the Copyright Holder be liable for any      #
# direct, indirect, incidental, special, exemplary, or consequential damages (including, but not   #
# limited to, procurement of substitute goods or services; loss of use, data, or profits; or       #
# business interruption) however caused and on any theory of liability, whether in contract,       #
# strict liability, or tort (including negligence or otherwise) arising in any way out of the use  #
# of this software, even if advised of the possibility of such damage.                             #
# ################################################################################################ #

VERSION=0.4.1

# debug [level=2] message
debug ()
{
  if [[ -z ${2+x} ]] ; then
    DEBUG_LEVEL=2
  else
    DEBUG_LEVEL=$1
    shift
  fi

  if [[ $DEBUG -ge $DEBUG_LEVEL ]] ; then
    echo "$1">&2
  fi
}

# warning message
warning ()
{
  if [[ $DEBUG -gt 0 ]] ; then
    echo "Warning: $1">&2
  fi
}

# reg_string key value
# Retrieves a REG_SZ value from the registry (redirected on WOW64)
reg_string ()
{
  reg query "$1" /v "$2" 2>/dev/null | tr -d '\r' | sed -ne "s/ *$2 *REG_SZ *//p"
}

# reg64_string key value
# As reg_string, but without WOW64 redirection (i.e. guaranteed access to 64-bit registry)
reg64_string ()
{
  $REG64 query "$1" /v "$2" 2>/dev/null | tr -d '\r' | sed -ne "s/ *$2 *REG_SZ *//p"
}

# find_in list file
# Increments $RET if file does not exist in any of the directories in the *-separated list
find_in ()
{
  debug 4 "Looking for $2 in $1"
  if [[ -z $1 ]] ; then
    STATUS=1
  else
    IFS=*
    STATUS=1
    for f in $1; do
      if [[ -e "$f/$2" ]] ; then
        STATUS=0
        break
      fi
    done
    unset IFS
  fi
  if [[ $STATUS -eq 1 ]] ; then
    debug 4 "$2 not found"
  fi
  ((RET+=STATUS))
}

# check_environment PATH INC LIB name arch
# By checking for the presence of various files, verifies that PATH, INC and LIB provide a complete
# compiler and indicates this in its return status. RET is assumed to be zero on entry. $ASSEMBLER
# will contain the name of assembler for this compiler series (ml.exe or ml64.exe).
# The following files are checked:
#   cl.exe        PATH  Microsoft C compiler
#   kernel32.lib  LIB   Implies Windows SDK present
#   link.exe      PATH  Microsoft Linker
#   ml[64].exe    PATH  Microsoft Assembler (ml.exe or ml64.exe)
#   msvcrt.lib    LIB   Implies C Runtime Libraries present
#   mt.exe        PATH  Microsoft Manifest Tool
#   oldnames.lib  LIB   Implies C Runtime Libraries present
#   rc.exe        PATH  Microsoft Resource Compiler (implies tools present)
#   stdlib.h      INC   Implies Microsoft C Runtime Libraries present
#   windows.h     INC   Implies Windows SDK present
# oldnames.lib is included, because certain SDKs and older versions don't correctly install the
# entire runtime if only some options (e.g. Dynamic Runtime and not Static) are selected.
check_environment ()
{
  debug 4 "Checking $4 ($5)"
  for tool in cl rc link ; do
    find_in "$1" $tool.exe
  done

  if [[ $RET -gt 0 ]] ; then
    warning "Microsoft C Compiler tools not all found - $4 ($5) excluded"
    return 1
  fi

  RET=0
  find_in "$2" windows.h
  find_in "$3" kernel32.lib
  if [[ $RET -gt 0 ]] ; then
    warning "Windows SDK not all found - $4 ($5) excluded"
    return 1
  fi

  RET=0
  find_in "$2" stdlib.h
  find_in "$3" msvcrt.lib
  find_in "$3" oldnames.lib
  if [[ $RET -gt 0 ]] ; then
    warning "Microsoft C runtime library not all found - $4 ($5) excluded"
    return 1
  fi

  ASSEMBLER=ml${5#x}
  ASSEMBLER=${ASSEMBLER%86}.exe
  if [[ $ML_REQUIRED -eq 1 ]] ; then
    RET=0
    find_in "$1" $ASSEMBLER
    if [[ $RET -gt 0 ]] ; then
      warning "Microsoft Assembler ($ASSEMBLER) not found - $4 ($5)"
      return 1
    fi
  fi

  if [[ $MT_REQUIRED -eq 1 ]] ; then
    RET=0
    find_in "$1" mt.exe
    if [[ $RET -gt 0 ]] ; then
      warning "Microsoft Manifest Tool not found - $4 ($5)"
      return 1
    fi
  fi

  return 0
}

# output VAR value arch
# Outputs a command for setting VAR to value based on $OUTPUT. If $ENV_ARCH is arch, then an empty
# value (i.e. no change) is output.
output ()
{
  if [[ $3 = $ENV_ARCH ]] ; then
    VALUE=
  else
    VALUE=$2
  fi
  case "$OUTPUT" in
    0)
      echo "$1='${VALUE//\'/\'\"\'\"\'}'";;
    1)
      VALUE=${VALUE//#/\\\#}
      echo "$1=${VALUE//\$/\$\$}";;
  esac
}

# DEBUG       Debugging level
# MODE        Operation mode
#               0 - Normal
#               1 - --all
#               2 - --help
#               3 - --version
# OUTPUT      --output option
#               0 - =shell
#               1 - =make
# MT_REQUIRED --with-mt
# ML_REQUIRED --with-assembler
# TARGET_ARCH Normalised --arch (x86, x64 or blank for both)
# LEFT_ARCH   \ If $TARGET_ARCH is blank, these will be x86 and x64 respectively, otherwise they
# RIGHT_ARCH  / equal $TARGET_ARCH
# SCAN_ENV    Controls from parsing whether the environment should be queried for a compiler
DEBUG=0
MODE=0
OUTPUT=0
MT_REQUIRED=0
ML_REQUIRED=0
TARGET_ARCH=
SCAN_ENV=0

# Various PATH messing around means it's sensible to know where tools are now
WHICH=$(which which)

if [[ $(uname --operating-system 2>/dev/null) = "Msys" ]] ; then
  # Prevent MSYS from translating command line switches to paths
  SWITCH_PREFIX='//'
else
  SWITCH_PREFIX='/'
fi

# Parse command-line. At the moment, the short option which usefully combines with anything is -d,
# so for the time being, combining short options is not permitted, as the loop becomes even less
# clear with getopts. GNU getopt isn't installed by default on Cygwin...
if [[ $@ != "" ]] ; then
  while true ; do
    case "$1" in
      # Mode settings ($MODE)
      -a|--all)
        MODE=1
        shift 1;;
      -h|--help)
        MODE=2
        shift;;
      -v|--version)
        MODE=3
        shift;;

      # Simple flags ($MT_REQUIRED and $ML_REQUIRED)
      --with-mt)
        MT_REQUIRED=1
        shift;;
      --with-assembler)
        ML_REQUIRED=1
        shift;;

      # -o, --output ($OUTPUT)
      -o|--output)
        case "$2" in
          shell)
            ;;
          make)
            OUTPUT=1;;
          *)
            echo "$0: unrecognised option for $1: '$2'">&2
            exit 2;;
        esac
        shift 2;;
      -oshell|--output=shell)
        shift;;
      -omake|--output=make)
        OUTPUT=1
        shift;;
      -o*)
        echo "$0: unrecognised option for -o: '${1#-o}'">&2
        exit 2;;
      --output=*)
        echo "$0: unrecognised option for --output: '${1#--output=}'">&2
        exit 2;;

      # -x, --arch ($TARGET_ARCH)
      -x|--arch)
        case "$2" in
          86|x86)
            TARGET_ARCH=x86;;
          64|x64)
            TARGET_ARCH=x64;;
          *)
            echo "$0: unrecognised option for $1: '$2'">&2
            exit 2
        esac
        shift 2;;
      -x86|-xx86|--arch=x86|--arch=86)
        TARGET_ARCH=x86
        shift;;
      -x64|-xx64|--arch=x64|--arch=64)
        TARGET_ARCH=x64
        shift;;
      -x*)
        echo "$0: unrecognised option for -x: '${1#-x}'">&2
        exit 2;;
      --arch=*)
        echo "$0: unrecognised option for --arch: '${1#--arch}'">&2
        exit 2;;

      # -d, --debug ($DEBUG)
      -d*)
        DEBUG=${1#-d}
        if [[ -z $DEBUG ]] ; then
          DEBUG=1
        fi
        shift;;
      --debug=*)
        DEBUG=${1#*=}
        shift;;
      --debug)
        DEBUG=1
        shift;;

      # End of option marker
      --)
        shift
        break;;

      # Invalid options
      --*)
        echo "$0: unrecognised option: '${1%%=*}'">&2
        exit 2;;
      -*)
        echo "$0: unrecognised option: '${1:1:1}'">&2
        exit 2;;

      # MSVS_PREFERENCE (without end-of-option marker)
      *)
        break;;
    esac
  done

  if [[ -n ${1+x} ]] ; then
    if [[ $MODE -eq 1 ]] ; then
      echo "$0: cannot specify MSVS_PREFERENCE and --all">&2
      exit 2
    else
      MSVS_PREFERENCE="$@"
    fi
  fi
fi

# Options sanitising
if [[ $MODE -eq 1 ]] ; then
  if [[ -n $TARGET_ARCH ]] ; then
    echo "$0: --all and --arch are mutually exclusive">&2
    exit 2
  fi
  MSVS_PREFERENCE=
  SCAN_ENV=1
elif [[ -z ${MSVS_PREFERENCE+x} ]] ; then
  MSVS_PREFERENCE='@;VS16.*;VS15.*;VS14.0;VS12.0;VS11.0;10.0;9.0;8.0;7.1;7.0'
fi

MSVS_PREFERENCE=${MSVS_PREFERENCE//;/ }

if [[ -z $TARGET_ARCH ]] ; then
  LEFT_ARCH=x86
  RIGHT_ARCH=x64
else
  LEFT_ARCH=$TARGET_ARCH
  RIGHT_ARCH=$TARGET_ARCH
fi

# Command line parsing complete (MSVS_PREFERENCE pending)

NAME="Microsoft C Compiler Environment Detection Script"
case $MODE in
  2)
    echo "$NAME"
    echo "Queries the environment and registry to locate Visual Studio / Windows SDK"
    echo "installations and uses their initialisation scripts (SetEnv.cmd, vcvarsall.bat,"
    echo "etc.) to determine INCLUDE, LIB and PATH alterations."
    echo
    echo "Usage:"
    echo "  $0 [OPTIONS] [--] [MSVS_PREFERENCE]"
    echo
    echo "Options:"
    echo "  -a, --all            Display all available compiler packages"
    echo "  -x, --arch=ARCH      Only consider packages for ARCH (x86 or x64). Default is"
    echo "                       to return packages containing both architectures"
    echo "  -d, --debug[=LEVEL]  Set debug messages level"
    echo "  -h, --help           Display this help screen"
    echo "  -o, --output=OUTPUT  Set final output. Default is shell. Valid values:"
    echo "                         shell - shell assignments, for use with eval"
    echo "                         make  - make assignments, for inclusion in a Makefile"
    echo "  -v, --version        Display the version"
    echo "      --with-mt        Only consider packages including the Manifest Tool"
    echo "      --with-assembler Only consider packages including an assembler"
    echo
    echo "If MSVS_PREFERENCE is not given, then the environment variable MSVS_PREFERENCE"
    echo "is read. MSVS_PREFERENCE is a semicolon separated list of preferred versions."
    echo "Three kinds of version notation are supported:"
    echo "  1. @ - which refers to the C compiler found in PATH (if it can be identified)"
    echo "     (this allows the C compiler corresponding to the opposite architecture to"
    echo "     be selected, if possible)."
    echo "  2. mm.n - which refers to a Visual Studio version (e.g. 14.0, 7.1) but which"
    echo "     also allows an SDK to provide the compiler (e.g. Windows SDK 7.1 provides"
    echo "     10.0). Visual Studio packages are always preferred ahead of SDKs."
    echo "  3. SPEC - an actual package specification. Visual Studio packages are VSmm.n"
    echo "     (e.g. VS14.0, VS7.1) and SDK packages are SDKm.n (e.g. SDK7.1)."
    echo "     Any Visual Studio 2017 update can be selected with VS15.*"
    echo "The default behaviour is to match the environment compiler followed by the most"
    echo "recent version of the compiler."
    exit 0;;
  3)
    echo "$NAME"
    echo "Version $VERSION"
    exit 0;;
esac

# Known compiler packages. Visual Studio .NET 2002 onwards. Detection is in place for Visual Studio
# 2005 Express, but because it doesn't include a Windows SDK, it can only ever be detected if the
# script has been launched from within a Platform SDK command prompt (this provides the Windows
# Headers and Libraries which allows this script to detect the rest).
# Each element is either a Visual Studio or SDK package and the value is the syntax for a bash
# associative array to be eval'd. Each of these contains the following properties:
#   NAME           - the friendly name of the package
#   ENV            - (VS only) the version-specific portion of the VSCOMNTOOLS environment variable
#   VERSION        - (VS only) version number of the package
#   ARCH           - Lists the architectures available in this version
#   ARCH_SWITCHES  - The script is assumed to accept x86 and x64 to indicate architecture. This key
#                    contains another eval'd associative array allowing alternate values to be given
#   SETENV_RELEASE - (SDK only) script switch necessary to select release than debugging versions
#   EXPRESS        - (VS only) the prefix to the registry key to detect the Express edition
#   EXPRESS_ARCH   - (VS only) overrides ARCH if Express edition is detected
#   EXPRESS_ARCH_SWITCHES - (VS only) overrides ARCH_SWITCHES if Express edition is detected
#   VC_VER         - (SDK only) specifies the version of the C Compilers included in the SDK (SDK
#                    equivalent of the VERSION key)
#   REG_KEY        - (SDK only) registry key to open to identify this package installation
#   REG_VALUE      - (SDK only) registry value to query to identify this package installation
#   VSWHERE        - (VS 2017+) is 1 if the compiler can only be detected using vswhere
# For a while, Windows SDKs followed a standard pattern which is stored in the SDK element and
# copied to the appropriate version. SDKs after 7.1 do not include compilers, and so are not
# captured (as of Visual Studio 2015, the Windows SDK is official part of Visual Studio).
declare -A COMPILERS
SDK52_KEY='HKLM\SOFTWARE\Microsoft\MicrosoftSDK\InstalledSDKs\8F9E5EF3-A9A5-491B-A889-C58EFFECE8B3'
COMPILERS=(
  ["VS7.0"]='(
    ["NAME"]="Visual Studio .NET 2002"
    ["ENV"]=""
    ["VERSION"]="7.0"
    ["ARCH"]="x86")'
  ["VS7.1"]='(
    ["NAME"]="Visual Studio .NET 2003"
    ["ENV"]="71"
    ["VERSION"]="7.1"
    ["ARCH"]="x86")'
  ["VS8.0"]='(
    ["NAME"]="Visual Studio 2005"
    ["ENV"]="80"
    ["VERSION"]="8.0"
    ["EXPRESS"]="VC"
    ["ARCH"]="x86 x64"
    ["EXPRESS_ARCH"]="x86")'
  ["VS9.0"]='(
    ["NAME"]="Visual Studio 2008"
    ["ENV"]="90"
    ["VERSION"]="9.0"
    ["EXPRESS"]="VC"
    ["ARCH"]="x86 x64"
    ["EXPRESS_ARCH"]="x86")'
  ["VS10.0"]='(
    ["NAME"]="Visual Studio 2010"
    ["ENV"]="100"
    ["VERSION"]="10.0"
    ["EXPRESS"]="VC"
    ["ARCH"]="x86 x64"
    ["EXPRESS_ARCH"]="x86")'
  ["VS11.0"]='(
    ["NAME"]="Visual Studio 2012"
    ["ENV"]="110"
    ["VERSION"]="11.0"
    ["EXPRESS"]="WD"
    ["ARCH"]="x86 x64"
    ["EXPRESS_ARCH_SWITCHES"]="([\"x64\"]=\"x86_amd64\")")'
  ["VS12.0"]='(
    ["NAME"]="Visual Studio 2013"
    ["ENV"]="120"
    ["VERSION"]="12.0"
    ["EXPRESS"]="WD"
    ["ARCH"]="x86 x64"
    ["EXPRESS_ARCH_SWITCHES"]="([\"x64\"]=\"x86_amd64\")")'
  ["VS14.0"]='(
    ["NAME"]="Visual Studio 2015"
    ["ENV"]="140"
    ["VERSION"]="14.0"
    ["ARCH"]="x86 x64")'
  ["VS15.*"]='(
    ["NAME"]="Visual Studio 2017"
    ["VSWHERE"]="1")'
  ["VS16.*"]='(
    ["NAME"]="Visual Studio 2019"
    ["VSWHERE"]="1")'
  ["SDK5.2"]='(
    ["NAME"]="Windows Server 2003 SP1 SDK"
    ["VC_VER"]="8.0"
    ["REG_KEY"]="$SDK52_KEY"
    ["REG_VALUE"]="Install Dir"
    ["SETENV_RELEASE"]="/RETAIL"
    ["ARCH"]="x64"
    ["ARCH_SWITCHES"]="([\"x64\"]=\"/X64\")")'
  ["SDK"]='(
    ["NAME"]="Generalised Windows SDK"
    ["SETENV_RELEASE"]="/Release"
    ["ARCH"]="x86 x64"
    ["ARCH_SWITCHES"]="([\"x86\"]=\"/x86\" [\"x64\"]=\"/x64\")")'
  ["SDK6.1"]='(
    ["NAME"]="Windows Server 2008 with .NET 3.5 SDK"
    ["VC_VER"]="9.0")'
  ["SDK7.0"]='(
    ["NAME"]="Windows 7 with .NET 3.5 SP1 SDK"
    ["VC_VER"]="9.0")'
  ["SDK7.1"]='(
    ["NAME"]="Windows 7 with .NET 4 SDK"
    ["VC_VER"]="10.0")'
)

# FOUND is ultimately an associative array containing installed compiler packages. It's
# hijacked here as part of MSVS_PREFERENCE validation.
# Ultimately, it contains a copy of the value from COMPILERS with the following extra keys:
#   IS_EXPRESS - (VS only) indicates whether the Express edition was located
#   SETENV     - (SDK only) the full location of the SetEnv.cmd script
#   ASSEMBLER  - the name of the assembler (ml or ml64)
#   MSVS_PATH \
#   MSVS_INC   > prefix values for PATH, INCLUDE and LIB determined by running the scripts.
#   MSVS_LIB  /
declare -A FOUND

# Check that MSVS_PREFERENCE is valid and contains no repetitions.
for v in $MSVS_PREFERENCE ; do
  if [[ -n ${FOUND[$v]+x} ]] ; then
    echo "$0: corrupt MSVS_PREFERENCE: repeated '$v'">&2
    exit 2
  fi
  if [[ $v != "@" ]] ; then
    if [[ -z ${COMPILERS[$v]+x} && -z ${COMPILERS["VS$v"]+x} && -z ${COMPILERS[${v%.*}.*]+x} ]] ; then
      echo "$0: corrupt MSVS_PREFERENCE: unknown compiler '$v'">&2
      exit 2
    fi
  else
    SCAN_ENV=1
  fi
  FOUND["$v"]=""
done

# Reset FOUND for later use.
FOUND=()

# Scan the environment for a C compiler, and check that it's valid. Throughout the rest of the
# script, it is assumed that if ENV_ARCH is set then there is a valid environment compiler.
if [[ $SCAN_ENV -eq 1 ]] ; then
  if "$WHICH" cl >/dev/null 2>&1 ; then
    # Determine its architecture from the Microsoft Logo line.
    ENV_ARCH=$(cl 2>&1 | head -1 | tr -d '\r')
    case "${ENV_ARCH#* for }" in
      x64|AMD64)
        ENV_ARCH=x64;;
      80x86|x86)
        ENV_ARCH=x86;;
      *)
        echo "Unable to identify C compiler architecture from '${ENV_ARCH#* for }'">&2
        echo "Environment C compiler discarded">&2
        unset ENV_ARCH;;
    esac

    # Environment variable names are a bit of a nightmare on Windows - they are actually case
    # sensitive (at the kernel level) but not at the user level! To compound the misery is that SDKs
    # use Include and Lib where vcvars32 tends to use INCLUDE and LIB. Windows versions also contain
    # a mix of Path and PATH, but fortunately Cygwin normalises that to PATH for us! For this
    # reason, use env to determine the actual case of the LIB and INCLUDE variables.
    if [[ -n ${ENV_ARCH+x} ]] ; then
      RET=0
      ENV_INC=$(env | sed -ne 's/^\(INCLUDE\)=.*/\1/pi')
      ENV_LIB=$(env | sed -ne 's/^\(LIB\)=.*/\1/pi')
      if [[ -z $ENV_INC || -z $ENV_LIB ]] ; then
        warning "Microsoft C Compiler Include and/or Lib not set - Environment C compiler ($ENV_ARCH) excluded"
        unset ENV_ARCH
      else
        if check_environment "${PATH//:/*}" \
                             "${!ENV_INC//;/*}" \
                             "${!ENV_LIB//;/*}" \
                             "Environment C compiler" \
                             "$ENV_ARCH" ; then
          ENV_CL=$("$WHICH" cl)
          ENV_cl=${ENV_CL,,}
          ENV_cl=${ENV_cl/bin\/*_/bin\/}
          debug "Environment appears to include a compiler at $ENV_CL"
          if [[ -n $TARGET_ARCH && $TARGET_ARCH != $ENV_ARCH ]] ; then
            debug "But architecture doesn't match required value"
            unset ENV_ARCH
          fi
        else
          unset ENV_ARCH
        fi
      fi
    fi
  fi
fi

# Even if launched from a 64-bit Command Prompt, Cygwin is usually 32-bit and so the scripts
# executed will inherit that fact. This is a problem when querying the registry, but fortunately
# WOW64 provides a mechanism to break out of the 32-bit environment by mapping $WINDIR/sysnative to
# the real 64-bit programs.
# Thus:
#   MS_ROOT is the 32-bit Microsoft Registry key (all Visual Studio keys are located there)
#   REG64 is the processor native version of the reg utility (allowing 64-bit keys to be read for
#         the SDKs)
if [[ -n ${PROCESSOR_ARCHITEW6432+x} ]] ; then
  debug "WOW64 detected"
  MS_ROOT='HKLM\SOFTWARE\Microsoft'
  REG64=$WINDIR/sysnative/reg
else
  MS_ROOT='HKLM\SOFTWARE\Wow6432Node\Microsoft'
  REG64=reg
fi

# COMPILER contains each eval'd element from COMPILERS
declare -A COMPILER

# Scan the registry for compiler package (vswhere is later)
for i in "${!COMPILERS[@]}" ; do
  eval COMPILER=${COMPILERS[$i]}

  if [[ -n ${COMPILER["ENV"]+x} ]] ; then
    # Visual Studio package - test for its environment variable
    ENV=VS${COMPILER["ENV"]}COMNTOOLS
    if [[ -n ${!ENV+x} ]] ; then
      debug "$ENV is a candidate"
      TEST_PATH=${!ENV%\"}
      TEST_PATH=$(cygpath -u -f - <<< ${TEST_PATH#\"})
      if [[ -e $TEST_PATH/vsvars32.bat ]] ; then
        debug "Directory pointed to by $ENV contains vsvars32.bat"
        EXPRESS=0
        # Check for the primary Visual Studio registry value indicating installation
        INSTALL_DIR=$(reg_string "$MS_ROOT\\VisualStudio\\${COMPILER["VERSION"]}" InstallDir)
        if [[ -z $INSTALL_DIR ]] ; then
          if [[ -n ${COMPILER["EXPRESS"]+x} ]] ; then
            TEST_KEY="$MS_ROOT\\${COMPILER["EXPRESS"]}Express\\${COMPILER["VERSION"]}"
            INSTALL_DIR=$(reg_string "$TEST_KEY" InstallDir)
            # Exception for Visual Studio 2005 Express, which doesn't set the registry correctly, so
            # set INSTALL_DIR to a fake value to pass the next test.
            if [[ ${COMPILER["VERSION"]} = "8.0" ]] ; then
              INSTALL_DIR=$(cygpath -w "$TEST_PATH")
              EXPRESS=1
            else
              if [[ -z $INSTALL_DIR ]] ; then
                warning "vsvars32.bat found, but registry value not located (Exp or Pro)"
              else
                EXPRESS=1
              fi
            fi
          else
            warning "vsvars32.bat found, but registry value not located"
          fi
        fi

        if [[ -n $INSTALL_DIR ]] ; then
          if [[ ${TEST_PATH%/} = $(cygpath -u "$INSTALL_DIR\\..\\Tools") ]] ; then
            RESULT=${COMPILERS[$i]%)}
            DISPLAY=${COMPILER["NAME"]}
            if [[ $EXPRESS -eq 1 ]] ; then
              DISPLAY="$DISPLAY Express"
            fi
            FOUND+=(["$i"]="$RESULT [\"DISPLAY\"]=\"$DISPLAY\" [\"IS_EXPRESS\"]=\"$EXPRESS\")")
            debug "${COMPILER["NAME"]} accepted for further detection"
          else
            warning "$ENV doesn't agree with registry"
          fi
        else
          warning "vsvars32.bat found, but registry settings not found"
        fi
      else
        warning "$ENV set, but vsvars32.bat not found"
      fi
    fi
  elif [[ -n ${COMPILER["REG_KEY"]+x} ]] ; then
    # SDK with explicit registry detection value
    INSTALL_DIR=$(reg64_string "${COMPILER["REG_KEY"]}" "${COMPILER["REG_VALUE"]}")
    if [[ -n $INSTALL_DIR ]] ; then
      TEST_PATH=$(cygpath -u "$INSTALL_DIR")
      if [[ -e $TEST_PATH/SetEnv.cmd ]] ; then
        RESULT=${COMPILERS[$i]%)}
        FOUND+=(["$i"]="$RESULT [\"DISPLAY\"]=\"${COMPILER["NAME"]}\" [\"SETENV\"]=\"$INSTALL_DIR\\SetEnv.cmd\")")
        debug "${COMPILER["NAME"]} accepted for further detection"
      else
        warning "Registry set for Windows Server 2003 SDK, but SetEnv.cmd not found"
      fi
    fi
  fi
done

# Now enumerate installed SDKs for v6.0+
SDK_ROOT='HKLM\SOFTWARE\Microsoft\Microsoft SDKs\Windows'
for i in $(reg query "$SDK_ROOT" 2>/dev/null | tr -d '\r' | sed -ne '/Windows\\v/s/.*\\//p') ; do
  debug "Analysing SDK key $SDK_ROOT\\$i"
  INSTALL_DIR=$(reg_string "$SDK_ROOT\\$i" InstallationFolder)
  if [[ -n $INSTALL_DIR ]] ; then
    TEST_PATH=$(cygpath -u "$INSTALL_DIR")
    if [[ -e $TEST_PATH/Bin/SetEnv.cmd ]] ; then
      if [[ -z ${COMPILERS["SDK${i#v}"]+x} ]] ; then
        warning "SDK $i is not known to this script - assuming compatibility"
        DISPLAY="Windows SDK $i"
      else
        eval COMPILER=${COMPILERS["SDK${i#v}"]}
        DISPLAY=${COMPILER['NAME']}
      fi
      RESULT=${COMPILERS['SDK']%)}
      FOUND+=(["SDK${i/v/}"]="$RESULT [\"DISPLAY\"]=\"$DISPLAY\" [\"SETENV\"]=\"$INSTALL_DIR\\Bin\\SetEnv.cmd\")")
    else
      if [[ -n ${COMPILERS["SDK${i#v}"]+x} ]] ; then
        warning "Registry set for Windows SDK $i, but SetEnv.cmd not found"
      fi
    fi
  else
    warning "Registry key for Windows SDK $i doesn't contain expected InstallationFolder value"
  fi
done

# Now enumerate Visual Studio 2017+ instances
VSWHERE=$(dirname $(realpath $0))/vswhere.exe
if [[ ! -x $VSWHERE ]] ; then
  VSWHERE="$(printenv 'ProgramFiles(x86)')\\Microsoft Visual Studio\\Installer\\vswhere.exe"
  VSWHERE=$(echo $VSWHERE| cygpath -f -)
fi
if [[ -x $VSWHERE ]] ; then
  debug "$VSWHERE found"
  while IFS= read -r line; do
    case ${line%: *} in
      instanceId)
        INSTANCE=${line#*: };;
      installationPath)
        INSTANCE_PATH=${line#*: };;
      installationVersion)
        INSTANCE_VER=${line#*: }
        INSTANCE_VER=${INSTANCE_VER%.*}
        INSTANCE_VER=${INSTANCE_VER%.*};;
      displayName)
        INSTANCE_NAME=${line#*: }
        debug "Looking at $INSTANCE in $INSTANCE_PATH ($INSTANCE_VER $INSTANCE_NAME)"
        if [[ -e "$(echo $INSTANCE_PATH| cygpath -f -)/VC/Auxiliary/Build/vcvarsall.bat" ]] ; then
          debug "vcvarsall.bat found"
          FOUND+=(["VS$INSTANCE_VER"]="([\"DISPLAY\"]=\"$INSTANCE_NAME\" [\"ARCH\"]=\"x86 x64\" [\"SETENV\"]=\"$INSTANCE_PATH\\VC\\Auxiliary\\Build\\vcvarsall.bat\" [\"SETENV_RELEASE\"]=\"\")")
        else
          warning "vcvarsall.bat not found for $INSTANCE"
        fi;;
    esac
  done < <("$VSWHERE" -all -nologo | tr -d '\r')
fi

if [[ $DEBUG -gt 1 ]] ; then
  for i in "${!FOUND[@]}" ; do
    echo "Inspect $i">&2
  done
fi

# Basic scanning is complete, now interrogate the packages which seem to be installed and ensure
# that they pass the check_environment tests.

# CANDIDATES is a hash table of the keys of FOUND. The result of the next piece of processing is to
# derive two arrays PREFERENCE and TEST. TEST will contain a list of the keys of FOUND in the order
# in which they should be evaluated. PREFERENCE contains a parsed version of MSVS_PREFERENCE but
# filtered on the basis of the compiler packages already identified. The current "hoped for"
# preference is stored in $pref (the index into PREFERENCE) and $PREF (which is
# ${PREFERENCE[$pref]}). These two arrays together allow testing to complete quickly if the desired
# version is found (note that often this won't be possible as the @ environment option requires all
# packages to be tested in order to be sure that the environment compiler is not ambiguous).
declare -A CANDIDATES
for i in "${!FOUND[@]}" ; do
  CANDIDATES[$i]="";
done

# For --all, act as though MSVS_PREFERENCE were "@" because this causes all packages to be tested.
if [[ $MODE -eq 1 ]] ; then
  PREFER_ENV=1
  PREFERENCE=("@")
else
  PREFER_ENV=0
  PREFERENCE=()
fi

TEST=()
for i in $MSVS_PREFERENCE ; do
  if [[ $i = "@" ]] ; then
    if [[ -n ${ENV_ARCH+x} ]] ; then
      PREFERENCE+=("@")
      PREFER_ENV=1
    else
      debug "Preference @ ignored since no environment compiler selected"
    fi
  else
    if [[ -n ${COMPILERS[$i]+x} || -n ${COMPILERS[${i%.*}.*]+x} ]] ; then
      if [[ -n ${CANDIDATES[$i]+x} ]] ; then
        unset CANDIDATES[$i]
        TEST+=($i)
        PREFERENCE+=($i)
      elif [[ ${i#*.} = "*" ]] ; then
        INSTANCES=
        for j in "${!CANDIDATES[@]}" ; do
          if [[ "${j%.*}.*" = $i ]] ; then
            unset CANDIDATES[$j]
            INSTANCES="$INSTANCES $j"
          fi
        done
        INSTANCES="$(sort -r <<< "${INSTANCES// /$'\n'}")"
        eval TEST+=($INSTANCES)
        eval PREFERENCE+=($INSTANCES)
      fi
    else
      if [[ -n ${CANDIDATES["VS$i"]+x} ]] ; then
        unset CANDIDATES["VS$i"]
        TEST+=("VS$i")
        PREFERENCE+=("VS$i")
      fi
      SDKS=
      for j in "${!COMPILERS[@]}" ; do
        eval COMPILER=${COMPILERS[$j]}
        if [[ -n ${COMPILER["VC_VER"]+x} ]] ; then
          if [[ $i = ${COMPILER["VC_VER"]} && -n ${CANDIDATES[$j]+x} ]] ; then
            unset CANDIDATES[$j]
            SDKS="$j $SDKS"
          fi
        fi
      done
      SDKS=${SDKS% }
      SDKS="$(sort -r <<< "${SDKS// /$'\n'}")"
      SDKS=${SDKS//$'\n'/ }
      eval TEST+=($SDKS)
      eval PREFERENCE+=($SDKS)
    fi
  fi
done

# If MSVS_PREFERENCE includes @, add any remaining items from CANDIDATES to TEST, otherwise remove
# them from FOUND so that they don't accidentally get reported on later.
for i in "${!CANDIDATES[@]}" ; do
  if [[ $PREFER_ENV -eq 1 ]] ; then
    TEST+=($i)
  else
    unset FOUND[$i]
  fi
done

# Initialise pref and PREF to ${PREFERENCE[0]}
pref=0
PREF=${PREFERENCE[0]}

if [[ $DEBUG -gt 1 ]] ; then
  for i in "${!TEST[@]}" ; do
    echo "Test ${TEST[$i]}">&2
  done
fi


# Now run each compiler's environment script and then test whether it is suitable. During this loop,
# attempt to identify the environment C compiler (if one was found). The environment C compiler is
# strongly identified if the full location of cl matches the one in PATH and both LIB and INCLUDE
# contain the strings returned by the script in an otherwise empty environment (if one or both of
# the LIB and INCLUDE variables do not contain the string returned, then the compiler is weakly
# identified). If the environment compiler is strongly identified by more than one package, then it
# is not identified at all; if it is strongly identified by no packages but weakly identified by
# exactly 1, then we grudgingly accept that that's probably the one.
ENV_COMPILER=
WEAK_ENV=

# ARCHINFO contains the appropriate ARCH_SWITCHES associative array for each compiler.
declare -A ARCHINFO

for i in "${TEST[@]}" ; do
  CURRENT=${FOUND[$i]}
  eval COMPILER=$CURRENT
  # At the end of this process, the keys of FOUND will be augmented with the architecture found in
  # each case (so if "VS14.0" was in FOUND from the scan and both the x86 and x64 compilers are
  # valid, then at the end of this loop FOUND will contain "VS14.0-x86" and "VS14.0-x64").
  unset FOUND[$i]

  if [[ ${COMPILER["IS_EXPRESS"]}0 -gt 0 && -n ${COMPILER["EXPRESS_ARCH_SWITCHES"]+x} ]] ; then
    eval ARCHINFO=${COMPILER["EXPRESS_ARCH_SWITCHES"]}
  elif [[ -n ${COMPILER["ARCH_SWITCHES"]+x} ]] ; then
    eval ARCHINFO=${COMPILER["ARCH_SWITCHES"]}
  else
    ARCHINFO=()
  fi

  # Determine the script to be executed and any non-architecture specific switches needed.
  # $ENV is will contain the value of the environment variable for the compiler (empty for an SDK)
  # which is required for Visual Studio 7.x shim later.
  if [[ -n ${COMPILER["ENV"]+x} ]] ; then
    ENV=VS${COMPILER["ENV"]}COMNTOOLS
    ENV=${!ENV%\"}
    ENV=${ENV#\"}
    if [[ ${COMPILER["ENV"]}0 -ge 800 ]] ; then
      SCRIPT="$(cygpath -d -f - <<< $ENV)\\..\\..\\VC\\vcvarsall.bat"
      SCRIPT_SWITCHES=
    else
      SCRIPT="$(cygpath -d -f - <<< $ENV)\\vsvars32.bat"
      SCRIPT_SWITCHES=
    fi
  else
    ENV=
    SCRIPT=${COMPILER["SETENV"]}
    SCRIPT_SWITCHES=${COMPILER["SETENV_RELEASE"]}
  fi
  # For reasons of escaping, the script is executed using its basename so the directory needs
  # prepending to PATH.
  DIR=$(dirname "$SCRIPT" | cygpath -u -f -)

  if [[ ${COMPILER["IS_EXPRESS"]} -gt 0 && -n ${COMPILER["EXPRESS_ARCH"]+x} ]] ; then
    ARCHS=${COMPILER["EXPRESS_ARCH"]}
  else
    ARCHS=${COMPILER["ARCH"]}
  fi

  for arch in $ARCHS ; do
    # Determine the command line switch for this architecture
    if [[ -n ${ARCHINFO[$arch]+x} ]] ; then
      ARCH_SWITCHES=${ARCHINFO[$arch]}
    else
      ARCH_SWITCHES=$arch
    fi

    # Run the script in order to determine changes made to PATH, INCLUDE and LIB. These scripts
    # always prepend changes to the environment variables.
    MSVS_PATH=
    MSVS_LIB=
    MSVS_INC=

    COMMAND='%EXEC_SCRIPT% && echo XMARKER && echo !PATH! && echo !LIB! && echo !INCLUDE!'

    # Note that EXEC_SCRIPT must have ARCH_SWITCHES first for older Platform SDKs (newer ones parse
    # arguments properly)
    if [[ $DEBUG -gt 3 ]] ; then
      printf "Scanning %s... " "$(basename "$SCRIPT") $ARCH_SWITCHES $SCRIPT_SWITCHES">&2
    fi
    num=0
    while IFS= read -r line; do
      case $num in
        0)
          MSVS_PATH=${line%% };;
        1)
          MSVS_LIB=${line%% };;
        2)
          MSVS_INC=${line%% };;
      esac
      ((num++))
    done < <(INCLUDE='' LIB='' PATH="?msvs-detect?:$DIR:$PATH" ORIGINALPATH='' \
             EXEC_SCRIPT="$(basename "$SCRIPT") $ARCH_SWITCHES $SCRIPT_SWITCHES" \
             $(cygpath "$COMSPEC") ${SWITCH_PREFIX}v:on ${SWITCH_PREFIX}c $COMMAND 2>/dev/null | grep -F XMARKER -A 3 | tr -d '\015' | tail -3)
    if [[ $DEBUG -gt 3 ]] ; then
      echo done>&2
    fi

    if [[ -n $MSVS_PATH ]] ; then
      # Translate MSVS_PATH back to Cygwin notation (/cygdrive, etc. and colon-separated)
      MSVS_PATH=$(cygpath "$MSVS_PATH" -p)
      # Remove any trailing / from elements of MSVS_PATH
      MSVS_PATH=$(echo "$MSVS_PATH" | sed -e 's|\([^:]\)/\+\(:\|$\)|\1\2|g;s/?msvs-detect?.*//')
      # Guarantee that MSVS_PATH ends with a single :
      MSVS_PATH="${MSVS_PATH%%:}:"
    fi
    # Ensure that both variables end with a semi-colon (it doesn't matter if for some erroneous
    # reason they have come back blank, because check_environment will shortly fail)
    MSVS_LIB="${MSVS_LIB%%;};"
    MSVS_INC="${MSVS_INC%%;};"

    # Visual Studio .NET 2002 and 2003 do not include mt in PATH, for not entirely clear reasons.
    # This shim detects that scenario and adds the winnt folder to MSVS_PATH.
    RET=0
    if [[ ${i/.*/} = "VS7" ]] ; then
      find_in "${MSVS_PATH//:/*}" mt.exe
      if [[ $RET -eq 1 ]] ; then
        MSVS_PATH="$MSVS_PATH$(cygpath -u -f - <<< $ENV\\Bin\\winnt):"
        RET=0
      fi
    fi

    # Ensure that these derived values give a valid compiler.
    if check_environment "${MSVS_PATH//:/*}" "${MSVS_INC//;/*}" "${MSVS_LIB//;/*}" "$i" $arch ; then
      # Put the package back into FOUND, but augmented with the architecture name and with the
      # derived values.
      FOUND["$i-$arch"]="${CURRENT%)} [\"MSVS_PATH\"]=\"$MSVS_PATH\" \
                                      [\"MSVS_INC\"]=\"$MSVS_INC\" \
                                      [\"MSVS_LIB\"]=\"$MSVS_LIB\" \
                                      [\"ASSEMBLER\"]=\"$ASSEMBLER\")" #"# fixes vim syn match error

      # Check to see if this is a match for the environment C compiler.
      if [[ -n ${ENV_ARCH+x} ]] ; then
        TEST_cl=$(PATH="$MSVS_PATH:$PATH" "$WHICH" cl)
        TEST_cl=${TEST_cl,,}
        TEST_cl=${TEST_cl/bin\/*_/bin\/}
        if [[ $TEST_cl = $ENV_cl ]] ; then
          if [[ ${!ENV_INC/"$MSVS_INC"/} != "${!ENV_INC}" && \
                ${!ENV_LIB/"$MSVS_LIB"/} != "${!ENV_LIB}" ]] ; then
            debug "$i-$arch is a strong candidate for the Environment C compiler"
            if [[ -n ${ENV_COMPILER+x} ]] ; then
              if [[ -z ${ENV_COMPILER} ]] ; then
                ENV_COMPILER=$i-$arch
                unset WEAK_ENV
              else
                # More than one strong candidate - no fall back available
                unset ENV_COMPILER
                unset WEAK_ENV
              fi
            fi
          else
            debug "$i-$arch is a weak candidate for the Environment C compiler"
            if [[ -n ${WEAK_ENV+x} ]] ; then
              if [[ -z ${WEAK_ENV} ]] ; then
                WEAK_ENV=$i-$arch
              else
                # More than one weak candidate - no fall back available
                unset WEAK_ENV
              fi
            fi
          fi
        fi
      fi
    fi
  done

  # Does this package match the current preference? Note that PREFERENCE and TEST are constructed in
  # a cunning (and hopefully not too "You are not expected to understand this" way) such that $PREF
  # will always equal $i, unless $PREF = "@".
  if [[ $PREF = $i ]] ; then
    # In which case, check that the architecture(s)s were found
    if [[ -n ${FOUND["$i-$LEFT_ARCH"]+x} && -n ${FOUND["$i-$RIGHT_ARCH"]+x} ]] ; then
      debug "Solved TARGET_ARCH=$TARGET_ARCH with $i"
      SOLUTION=$i
      break
    fi
  fi

  if [[ $PREF != "@" ]] ; then
    ((pref++))
    PREF=${PREFERENCE[$pref]}
  fi
done

# If we got this far, then either we failed to find a compiler at all, or we were looking for the
# environment compiler (or --all was specified).

# Adopt a weak match for the environment compiler, if that's the best we can do.
if [[ -n ${ENV_COMPILER+x} && -z ${ENV_COMPILER} && -n ${WEAK_ENV} ]] ; then
  warning "Assuming Environment C compiler is $WEAK_ENV"
  ENV_COMPILER=$WEAK_ENV
fi

declare -A FLIP
FLIP=(["x86"]="x64" ["x64"]="x86")

if [[ $MODE -eq 0 ]] ; then
  if [[ $PREF = "@" && -n ${ENV_COMPILER} ]] ; then
    SOLUTION=${ENV_COMPILER%-$ENV_ARCH}
    # If --arch wasn't specified, then ensure that the other architecture was also found. If --arch
    # was specified, then validate that the compiler was valid. This should always happen, unless
    # something went wrong running the script to get MSVS_PATH, MSVS_LIB and MSVS_INC.
    if [[ -n ${FOUND["$SOLUTION-${FLIP[$ENV_ARCH]}"]+x} ||
          -n ${FOUND["$SOLUTION-$TARGET_ARCH"]+x} ]] ; then
      debug "Solved with $SOLUTION"
    else
      unset SOLUTION
      unset ENV_ARCH
    fi
  fi

  if [[ -z ${SOLUTION+x} ]] ; then
    ((pref++))
    debug "Search remaining: ${PREFERENCE[*]}"
    TEST_ARCH=$TARGET_ARCH
    for i in "${PREFERENCE[@]:$pref}" ; do
      if [[ -n ${FOUND["$i-$LEFT_ARCH"]+x} && -n ${FOUND["$i-$RIGHT_ARCH"]+x} ]] ; then
        debug "Solved TARGET_ARCH='$TARGET_ARCH' with $i"
        SOLUTION=$i
        break
      fi
    done
  fi
fi

debug "Solution: $SOLUTION"

if [[ -n ${ENV_COMPILER} && $MODE -eq 1 ]] ; then
  echo "Identified Environment C compiler as $ENV_COMPILER"
fi

if [[ $MODE -eq 1 ]] ; then
  echo "Installed and usable packages:"
  for i in "${!FOUND[@]}" ; do
    echo "  $i"
  done | sort
  exit 0
fi

if [[ -n $SOLUTION ]] ; then
  eval COMPILER=${FOUND[$SOLUTION-$LEFT_ARCH]}
  output MSVS_NAME "${COMPILER["DISPLAY"]}" $LEFT_ARCH
  output MSVS_PATH "${COMPILER["MSVS_PATH"]}" $LEFT_ARCH
  output MSVS_INC "${COMPILER["MSVS_INC"]}" $LEFT_ARCH
  output MSVS_LIB "${COMPILER["MSVS_LIB"]}" $LEFT_ARCH
  if [[ $ML_REQUIRED -eq 1 ]] ; then
    output MSVS_ML "${COMPILER["ASSEMBLER"]%.exe}" always
  fi
  if [[ -z $TARGET_ARCH ]] ; then
    eval COMPILER=${FOUND[$SOLUTION-$RIGHT_ARCH]}
    output MSVS64_PATH "${COMPILER["MSVS_PATH"]}" $RIGHT_ARCH
    output MSVS64_INC "${COMPILER["MSVS_INC"]}" $RIGHT_ARCH
    output MSVS64_LIB "${COMPILER["MSVS_LIB"]}" $RIGHT_ARCH
    if [[ $ML_REQUIRED -eq 1 ]] ; then
      output MSVS64_ML "${COMPILER["ASSEMBLER"]%.exe}" always
    fi
  fi
  exit 0
else
  exit 1
fi
