#!/usr/bin/env python3
#
# venv_py2.py - Build a python2 virtualenv for running TortoiseHg or building
#               the installer.
#
# Copyright 2020 Matt Harbison <mharbison72@gmail.com>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.

import errno
import os
import pathlib
import shutil
import subprocess

from .downloads import (
    DOWNLOADS,
    download_entry,
)
from . import (
    util as thgutil,
    wix,
)
from .util import (
    extract_tar_to_directory,
    extract_zip_to_directory,
    process_install_rules,
    python_exe_info,
)


from . import venv


PY2_DOWNLOADS = {
    'openssl': {
        'url': 'https://www.openssl.org/source/openssl-1.0.2t.tar.gz',
        'size': 5355422,
        'sha256': '14cb464efe7ac6b54799b34456bd69558a749a4931ecfd9cf9f71d7881cac7bc',
        'version': '1.0.2t',
    },
    'pyqt5': {
        'url': 'https://www.riverbankcomputing.com/static/Downloads/PyQt5/5.13.2/PyQt5-5.13.2.zip',
        'size': 4372886,
        'sha256': '2dc98f339d9723414348b9684457fd2c2462d5504a34bb4e30f1c3643131e16b',
        'version': '5.13.2',
    },
    # NB: The latest QScintilla (2.11) has an issue that causes the GUI to
    # freeze for ~10 seconds when the commit and/or file content fields change
    # on Windows.
    'qscintilla': {
        'url': 'https://www.riverbankcomputing.com/static/Downloads/QScintilla/2.10.8/QScintilla_gpl-2.10.8.zip',
        'size': 3354209,
        'sha256': '4601470e4c32dc7633ccd42cc1a2fe4d606de863a192aa5f9438c6d914f4eef6',
        'version': '2.10.8',
    },
    'sip': {
        'url': 'https://www.riverbankcomputing.com/static/Downloads/sip/4.19.20/sip-4.19.20.zip',
        'size': 1166073,
        'sha256': '400920481432ad341ff899e6b3fe64a15f8bec95955fc126fab66cc59276f5ac',
        'version': '4.19.20',
    },
}

DOWNLOADS.update(PY2_DOWNLOADS)


def build_py2(
    source_dir: pathlib.Path,
    hg_source_dir: pathlib.Path,
    qt_bin_dir: pathlib.Path,
    build_dir: pathlib.Path,
    python_exe: pathlib.Path,
    output: pathlib.Path,
    clean: bool,
):
    """Creates a virtualenv at ``output`` with the necessary dependencies
    for running thg from source.

    The python version and architecture will match the characteristics of
    ``python_exe``.  Mercurial is built and installed from the source tree
    at ``hg_source_dir``.  The resulting virtualenv is capable of running
    from source, or running the ``winbuild`` script to build the installer.
    """
    build_dir.mkdir(exist_ok=True)

    venv_bin, venv_pip, venv_python = _make_venv(python_exe, build_dir, output)

    py_info = thgutil.python_exe_info(python_exe)
    vc_x64 = py_info['arch'] == '64bit'

    if os.name == 'nt':
        _install_py2exe(
            venv_python, build_dir, vc_x64, clean,
        )

    requirements_txt = (
        source_dir / 'contrib' / 'packaging' / 'venv_py2' / 'requirements.txt'
    )

    if os.name == 'nt':
        requirements_txt = requirements_txt.parent / 'requirements_win32.txt'

    subprocess.run([
        str(venv_pip),
        "install",
        "-r",
        str(requirements_txt),
        "--disable-pip-version-check",
    ], check=True)

    env = _get_msvc2017_env(vc_x64)
    env["CL"] = "/MP"  # Enable multiprocessor builds

    # "Activate" the virtualenv by putting its python first.  Also, put
    # preinstalled Qt5 binaries in PATH.
    env["PATH"] = "%s%s%s%s%s" % (
        venv_bin,
        os.pathsep,
        env["PATH"],
        os.pathsep,
        qt_bin_dir,
    )

    # The virtualenv doesn't have the libraries for the linker, so expose
    # the original python passed on the command line (and hope it isn't a
    # virtualenv).  The library is needed by PyQt and PyQScintilla.
    if os.name == 'nt':
        env["LIB"] = "%s%s%s" % (
            python_exe.parent / "libs",
            os.pathsep,
            env["LIB"],
        )

    venv.install_mercurial(venv_python, hg_source_dir)

    _install_openssl(venv_bin, build_dir, env, vc_x64, clean)

    _install_sip(venv_python, build_dir, env, vc_x64, clean)
    _install_pyqt5(venv_python, build_dir, env, vc_x64, clean)

    pyqt5_site = subprocess.check_output(
        [str(venv_python), '-c', 'import PyQt5; print(PyQt5.__path__[0])'],
        text=True,
    ).strip()

    pyqt5_install_dir = pathlib.Path(pyqt5_site)

    _install_qscintilla(
        venv_python,
        qt_bin_dir,
        pyqt5_install_dir,
        build_dir,
        env,
        vc_x64,
        clean,
    )

    _install_qt_libs(qt_bin_dir, pyqt5_install_dir)

    # Now for a couple of hacks for the sake of convenience, which really should
    # be part of an installer building module...

    # winbuild wants msgfmt.  For now, just extract it and put out a message.
    # In the future, a target that builds the installers should put this on
    # PATH automatically.
    gettext_pkg, gettext_entry = download_entry('gettext', build_dir)
    gettext_dep_pkg = download_entry('gettext-dep', build_dir)[0]
    gettext_root = build_dir / ('gettext-win-%s' % gettext_entry['version'])
    extract_zip_to_directory(gettext_pkg, gettext_root)
    extract_zip_to_directory(gettext_dep_pkg, gettext_root)

    # winbuild is hardcoded to look for the merge modules in the compiler
    # directory like
    # Leave it up to the user to stage them until we can build everything
    # locally.
    vcredist = build_dir / 'vcredist'
    if not vcredist.exists():
        vcredist.mkdir()
        wix.ensure_vc90_merge_modules(vcredist)

    print('The virtualenv has been created at %s' % output)
    print()
    print('If you are running winbuild, activate that and add this to PATH:')
    print('  %s' % str(gettext_root / 'bin'))
    print()
    print("In addition, you may need to stage these merge modules if")
    print("they aren't installed with the compiler.")
    print()
    print("  %s" % str(vcredist))


def _rm_dir(path):
    """A utility for ``rm -rf``."""

    def onerror(function, pth, excinfo):
        if isinstance(excinfo[1], OSError):
            if excinfo[1].errno == errno.ENOENT:
                return
        raise

    shutil.rmtree(path, onerror=onerror)


def _make_venv(
    python_exe: pathlib.Path,
    build_dir: pathlib.Path,
    venv_dir: pathlib.Path,
    clean=False,
):
    """Creates a virtualenv using ``python_exe``, with the root as ``venv_dir``.
    """
    virtualenv_pkg, virtualenv_entry = download_entry('virtualenv', build_dir)

    virtualenv_src_path = build_dir / (
        'virtualenv-%s' % virtualenv_entry['version']
    )
    virtualenv_py = virtualenv_src_path / 'virtualenv.py'

    if clean:
        _rm_dir(virtualenv_src_path)

    if os.name == "nt":
        venv_bin = venv_dir / "Scripts"
        venv_pip = venv_bin / "pip.exe"
        venv_python = venv_bin / "python.exe"
    else:
        venv_bin = venv_dir / "bin"
        venv_pip = venv_bin / "pip"
        venv_python = venv_bin / "python"

    if not virtualenv_src_path.exists():
        extract_tar_to_directory(virtualenv_pkg, build_dir)

    print()
    print('creating virtualenv with dependencies')

    subprocess.run(
        [str(python_exe), str(virtualenv_py), str(venv_dir)], check=True
    )

    return venv_bin, venv_pip, venv_python


def _install_py2exe(
    venv_python: pathlib.Path,
    build_dir: pathlib.Path,
    vc_x64: bool,
    clean=False,
):
    """Installs py2exe into the virtualenv, so that the virtualenv can be used
    to create the installer.
    """
    build_arch_dir = build_dir / ('x64' if vc_x64 else 'x86')

    py2exe_pkg, py2exe_entry = download_entry('py2exe', build_dir)
    py2exe_src_path = build_arch_dir / ('py2exe-%s' % py2exe_entry['version'])
    py2exe_py_path = (
        venv_python.parent.parent / 'Lib' / 'site-packages' / 'py2exe'
    )

    if clean:
        _rm_dir(py2exe_src_path)

    if not py2exe_py_path.exists():
        if not py2exe_src_path.exists():
            extract_zip_to_directory(py2exe_pkg, build_arch_dir)

        print()
        print('building py2exe')

        subprocess.run(
            [str(venv_python), 'setup.py', 'install'],
            cwd=py2exe_src_path,
            env=_get_msvc2008_env(vc_x64),
            check=True,
        )


def _install_openssl(
    venv_bin: pathlib.Path,
    build_dir: pathlib.Path,
    env: dict,
    vc_x64: bool,
    clean=False,
):
    """Build and installs the OpenSSL dynamic libraries into the virtualenv
    executables directory identified by ``venv_bin``.

    This requires a modern MSVC environment that matches the python architecture
    to be passed when building on Windows.  The download and build output is
    cached in ``build_dir`` prior to installing.
    """
    build_arch_dir = build_dir / ('x64' if vc_x64 else 'x86')
    openssl_pkg, openssl_entry = download_entry('openssl', build_arch_dir)
    openssl_src_path = build_arch_dir / (
        'openssl-%s' % openssl_entry['version']
    )

    if clean:
        _rm_dir(openssl_src_path)

    if not openssl_src_path.exists():
        extract_tar_to_directory(openssl_pkg, build_arch_dir)

    print()
    print("*** Building OpenSSL ***")

    make_exe = 'nmake' if os.name == 'nt' else 'make'

    # http://p-nand-q.com/programming/windows/building_openssl_with_visual_studio_2013.html

    subprocess.run(
        [
            'perl',
            'Configure',
            'no-krb5',
            'no-idea',
            'no-mdc2',
            'no-rc5',
            'no-zlib',
            'enable-tlsext',
            'no-ssl2',
            'no-ssl3',
            'no-rc4',
            'no-hw',
            'VC-WIN64A' if vc_x64 else 'VC-WIN32',
            '-D_BIND_TO_CURRENT_VCLIBS_VERSION=1',
            '--prefix=Release'
        ],
        cwd=openssl_src_path,
        env=env,
        check=True,
    )

    subprocess.run(
        'ms\\do_win64a' if vc_x64 else 'ms\\do_ms',
        cwd=openssl_src_path,
        env=env,
        shell=True,
        check=True
    )
    if not vc_x64:
        # Passing 'no-asm' to the configure step would avoid the need for this.
        # But then the dll link fails, complaining OPENSSL_rdtsc is unresolved.
        # To run this, put NASM on %PATH%.  64 bit MSVC apparently can handle
        # the assembly on its own.
        #
        # https://www.nasm.us/pub/nasm/releasebuilds/2.15.02/win32/
        subprocess.run(
            'ms\\do_nasm',
            cwd=openssl_src_path,
            env=env,
            shell=True,
            check=True
        )
    subprocess.run(
        [str(make_exe), '-f', 'ms\\ntdll.mak'],
        cwd=openssl_src_path, env=env, shell=True, check=True
    )
    subprocess.run(
        [str(make_exe), '-f', 'ms\\ntdll.mak', 'install'],
        cwd=openssl_src_path, env=env, shell=True, check=True
    )

    shutil.copy(openssl_src_path / "Release" / "bin" / "ssleay32.dll", venv_bin)
    shutil.copy(openssl_src_path / "Release" / "bin" / "libeay32.dll", venv_bin)


def _install_sip(
    venv_python: pathlib.Path,
    build_dir: pathlib.Path,
    env: dict,
    vc_x64: bool,
    clean=False,
):
    """Builds and installs ``sip`` into the virtualenv identified by
    ``venv_python``.

    This requires a modern MSVC environment that matches the python architecture
    to be passed when building on Windows.  The download and build output is
    cached in ``build_dir`` prior to installing.
    """
    build_arch_dir = build_dir / ('x64' if vc_x64 else 'x86')
    sip_pkg, sip_entry = download_entry('sip', build_dir)
    sip_src_path = build_arch_dir / ('sip-%s' % sip_entry['version'])

    if clean:
        _rm_dir(sip_src_path)

    if not sip_src_path.exists():
        extract_zip_to_directory(sip_pkg, build_arch_dir)

    print()
    print("*** Building SIP ***")

    make_exe = 'nmake' if os.name == 'nt' else 'make'

    subprocess.run(
        [str(venv_python), 'configure.py', '--sip-module', 'PyQt5.sip'],
        cwd=sip_src_path,
        env=env,
        check=True,
    )
    subprocess.run(make_exe, cwd=sip_src_path, env=env, shell=True, check=True)
    subprocess.run(
        [make_exe, 'install'], cwd=sip_src_path, env=env, shell=True, check=True
    )


def _install_pyqt5(
    venv_python: pathlib.Path,
    build_dir: pathlib.Path,
    env: dict,
    vc_x64: bool,
    clean=False,
):
    """Builds and installs ``PyQt5`` into the virtualenv identified by
    ``venv_python``.

    This requires a modern MSVC environment that matches the python architecture
    to be passed when building on Windows.  The download and build output is
    cached in ``build_dir`` prior to installing.
    """
    build_arch_dir = build_dir / ('x64' if vc_x64 else 'x86')
    pyqt5_pkg, pyqt5_entry = download_entry('pyqt5', build_dir)
    pyqt5_src_path = build_arch_dir / ('PyQt5-%s' % pyqt5_entry['version'])

    if clean:
        _rm_dir(pyqt5_src_path)

    if not pyqt5_src_path.exists():
        extract_zip_to_directory(pyqt5_pkg, build_arch_dir)

    print()
    print("*** Building PyQt5 ***")

    # These PyQt5 modules will be built: QtCore, QtGui, QtNetwork,
    # QtPrintSupport, QtSvg, QtWidgets, QtXml
    subprocess.run(
        [
            str(venv_python),
            'configure.py',
            '--confirm-license',
            '--no-designer-plugin',
            '--no-python-dbus',
            '--no-qml-plugin',
            '--no-qsci-api',
            # '--no-tools',  # tools created are *.bat files
            '--disable=QAxContainer',
            '--disable=QtBluetooth',
            '--disable=QtDBus',
            '--disable=QtDesigner',
            '--disable=QtHelp',
            '--disable=QtLocation',
            '--disable=QtMultimedia',
            '--disable=QtMultimediaWidgets',
            '--disable=QtNfc',  # Doesn't build under MSVC 2017
            '--disable=QtOpenGL',
            '--disable=QtPositioning',
            '--disable=QtQml',
            '--disable=QtQuick',
            '--disable=QtQuickWidgets',
            '--disable=QtSensors',
            '--disable=QtSerialPort',
            '--disable=QtSql',
            '--disable=QtTest',
            '--disable=QtWebChannel',
            '--disable=QtWebKit',
            '--disable=QtWebKitWidgets',
            '--disable=QtWebSockets',
            '--disable=QtWinExtras',
            '--disable=QtXmlPatterns',
            '--disable=QtX11Extras',
            '--disable=_QOpenGLFunctions_2_0',
            '--disable=_QOpenGLFunctions_2_1',
            '--disable=_QOpenGLFunctions_4_1_Core',
        ],
        cwd=pyqt5_src_path,
        env=env,
        check=True,
    )

    make_exe = 'nmake' if os.name == 'nt' else 'make'

    subprocess.run(
        make_exe, cwd=pyqt5_src_path, env=env, shell=True, check=True
    )
    subprocess.run(
        [make_exe, 'install'],
        cwd=pyqt5_src_path,
        env=env,
        shell=True,
        check=True,
    )


def _install_qscintilla(
    venv_python: pathlib.Path,
    qt_bin_dir: pathlib.Path,
    pyqt5_install_dir: pathlib.Path,
    build_dir: pathlib.Path,
    env: dict,
    vc_x64: bool,
    clean=False,
):
    """Builds and installs ``QScintilla`` into the virtualenv identified by
    ``venv_python``.

    This requires a modern MSVC environment that matches the python architecture
    to be passed when building on Windows.  The download and build output is
    cached in ``build_dir`` prior to installing.
    """
    build_arch_dir = build_dir / ('x64' if vc_x64 else 'x86')
    qsci_pkg, qsci_entry = download_entry('qscintilla', build_dir)
    qsci_src_path = build_arch_dir / (
        'QScintilla_gpl-%s' % qsci_entry['version']
    )

    if clean:
        _rm_dir(qsci_src_path)

    if not qsci_src_path.exists():
        extract_zip_to_directory(qsci_pkg, build_arch_dir)

    # Build and install QScintilla
    print()
    print("*** Building QScintilla ***")

    make_exe = 'nmake' if os.name == 'nt' else 'make'

    qt4qt5_source_path = qsci_src_path / "Qt4Qt5"

    subprocess.run(
        'qmake', cwd=qt4qt5_source_path, env=env, shell=True, check=True,
    )
    subprocess.run(
        make_exe, cwd=qt4qt5_source_path, env=env, shell=True, check=True
    )

    subprocess.run(
        [make_exe, 'install'],
        cwd=qt4qt5_source_path,
        env=env,
        shell=True,
        check=True,
    )

    # There doesn't seem to be an option to install to a specific directory.
    # Not even DESTDIR works on Windows.  So copy the library and translations
    # out of the system Qt5 installation.  PyQScintilla below is smart enough to
    # install into the python environment.
    shutil.copy(
        qt4qt5_source_path / 'release' / 'qscintilla2_qt5.dll',
        pyqt5_install_dir,
    )

    def _not_qt5_translation(src, names):
        # These match characteristics are from _build_translations() in
        # setup.py.
        # TODO: the install routine only places these 5 files.  Do we need to
        # copy over all of the rest?
        #     'qscintilla_cs.qm', 'qscintilla_de.qm', 'qscintilla_es.qm',
        #     'qscintilla_fr.qm', 'qscintilla_pt_br.qm'
        ignore = []
        for n in sorted(names):
            n = n.lower()
            if (
                not n.startswith(('qt_', 'qscintilla_'))
                or n.startswith('qt_help_')
                or not n.endswith('.qm')
            ):
                ignore.append(n)
        return ignore

    shutil.copytree(
        qt_bin_dir / '..' / 'translations',
        pyqt5_install_dir / 'translations',
        ignore=_not_qt5_translation,
    )

    ################################
    # Build and install PyQScintilla

    print()
    print("*** Building PyQScintilla ***")
    qt5_python_source_path = qsci_src_path / "Python"

    subprocess.run(
        [str(venv_python), 'configure.py', '--pyqt=PyQt5'],
        cwd=qt5_python_source_path,
        env=env,
        check=True,
    )
    subprocess.run(
        make_exe, cwd=qt5_python_source_path, env=env, shell=True, check=True
    )
    subprocess.run(
        [make_exe, 'install'],
        cwd=qt5_python_source_path,
        env=env,
        shell=True,
        check=True,
    )

    return pyqt5_install_dir


def _install_qt_libs(qt_bin_dir, pyqt5_install_dir):
    """Copies the necessary DLLs out of the Qt5 install at ``qt_bin_dir``
    into the PyQt5 package at ``pyqt5_install_dir`` so that py2exe can
    find them.
    """
    qt_plugins_dir = qt_bin_dir / '..' / 'plugins'
    qt_platform_dir = qt_plugins_dir / 'platforms'

    pyqt5_plugins_dir = pyqt5_install_dir / 'plugins'
    pyqt5_plugins_dir.mkdir(parents=True, exist_ok=True)

    # qwindows.dll
    shutil.copytree(qt_platform_dir, pyqt5_plugins_dir / 'platforms')

    # qico.dll
    qt_image_format_dir = qt_plugins_dir / 'imageformats'
    shutil.copytree(qt_image_format_dir, pyqt5_plugins_dir / 'imageformats')

    # Qt5*.dll, but skip the debug libraries
    for f in sorted(os.listdir(qt_bin_dir)):
        if not os.path.isfile(qt_bin_dir / f):
            continue
        f = f.lower()
        if (
            f.startswith('qt5')
            and f.endswith('.dll')
            and not f.endswith('d.dll')
        ):
            # TODO: also filter out dlls that were not configured above
            # (i.e. are not in {Qt5Core.dll, Qt5Gui, Qt5Network,
            # Qt5PrintSupport, Qt5Svg, Qt5Widgets, Qt5Xml}.
            shutil.copyfile(qt_bin_dir / f, pyqt5_install_dir / f)


def _get_msvc2017_env(vc_x64):
    """Return a dictionary containing a modern MSVC environment for building
    PyQt5 and its dependencies.

    If the current environment is a modern MSVC, a copy of it is returned to
    allow MSVC 2015 to be used.  Otherwise, an MSVC 2017 amd64 environment is
    returned for 64-bit python, and MSVC 2015 for 32-bit python.
    """

    # The 32-bit Qt5 install only has an MSVC 2015, while there are 64-bit
    # packages for MSVC 2015 and 2017.  If this is running from a Visual Studio
    # prompt, assume that this is the one to be used to allow alternate
    # compilers.  Otherwise, look for MSVC 2017.
    if (
        'VCINSTALLDIR' in os.environ
        and '9.0\\' not in os.environ['VCINSTALLDIR']
    ):
        libpath = os.environ.get('LIBPATH')
        if (
            not libpath
            or (not vc_x64 and 'x64' in libpath)
            or (vc_x64 and 'x64' not in libpath)
        ):
            raise Exception(
                "The current MSVC environment doesn't match python architecture"
            )

        return os.environ.copy()

    if not vc_x64:
        # 32-bit builds require MSVC 2015
        print()
        print('Searching for MSVC 2015')

        vcvarsall_bat = None
        vs140_comn_tools = os.environ.get('VS140COMNTOOLS')
        if vs140_comn_tools:
            vs140_comn_tools = pathlib.Path(vs140_comn_tools)

            vcvarsall_bat = (
                vs140_comn_tools.parent.parent / 'VC' / 'vcvarsall.bat'
            )

        if vcvarsall_bat and not vcvarsall_bat.exists():
            raise Exception(
                'unable to load Visual C++ build environment; '
                'execute the "Visual C++ 2015 Command Prompt" '
                'application shortcut or a vcsvars64.bat file'
            )

        msvc_env = _get_msvc_env(vcvarsall_bat, ['amd64' if vc_x64 else 'x86'])
        return _validate_legacy_msvc_env(msvc_env, vc_x64, "2015")

    print()
    print('Searching for MSVC 2017')

    vsdevcmd_bat = None
    vswhere = (
        pathlib.Path(os.environ['ProgramFiles(x86)'])
        / 'Microsoft Visual Studio'
        / 'Installer'
        / 'vswhere.exe'
    )

    if vswhere.exists():
        # https://github.com/microsoft/vswhere/wiki/Find-VC
        out = subprocess.run(
            [
                str(vswhere),
                '-latest',
                '-version',
                '[15.0,16.0)',
                '-products',
                '*',
                '-requires',
                'Microsoft.VisualStudio.Component.VC.Tools.x86.x64',
                '-property',
                'installationPath',
            ],
            text=True,
            capture_output=True,
        )

        install_dir = out.stdout.splitlines()[0]

        if install_dir:
            vsdevcmd_bat = (
                pathlib.Path(install_dir)
                / 'VC'
                / 'Auxiliary'
                / 'Build'
                / 'vcvars64.bat'
            )

    if not vsdevcmd_bat or not vsdevcmd_bat.exists():
        raise Exception(
            'unable to load Visual C++ build environment; '
            'execute the "Visual C++ 2017 Command Prompt" '
            'application shortcut or a vcsvars64.bat file'
        )

    # Trust that the bat file exits 1 on failure.
    return _get_msvc_env(vsdevcmd_bat)


def _get_msvc2008_env(vc_x64):
    """Return a dictionary containing the environment for Visual C++ for Python
    2.7 or MSVC 2008, depending on which is installed.

    This environment is useful for building py2exe under python 2.7, since it
    doesn't know how to find the compiler on its own.
    """
    local_app_data = os.environ['LOCALAPPDATA']
    vcvarsall_bat = (
        pathlib.Path(local_app_data)
        / 'Programs'
        / 'Common'
        / 'Microsoft'
        / 'Visual C++ for Python'
        / '9.0'
        / 'vcvarsall.bat'
    )

    if not vcvarsall_bat.exists():
        # Fall back to Visual Studio 2008
        vs90_comn_tools = os.environ.get('VS90COMNTOOLS')
        if vs90_comn_tools:
            vs90_comn_tools = pathlib.Path(vs90_comn_tools)

            vcvarsall_bat = (
                vs90_comn_tools.parent.parent / 'VC' / 'vcvarsall.bat'
            )

    if not vcvarsall_bat.exists():
        raise Exception('Unable to locate MSVC 2008 or Visual C++ for Python')

    msvc_env = _get_msvc_env(vcvarsall_bat, ['amd64' if vc_x64 else 'x86'])
    return _validate_legacy_msvc_env(msvc_env, vc_x64, "2008")


def _validate_legacy_msvc_env(msvc_env: dict, vc_x64: bool, msvc_ver: str):
    # The vcvarsall.bat file (at least for the MS python package) always exits
    # with 0, whether or not it errored out.  So do some basic sanity checking
    # that it printed the proper environment.  The bat file for MSVC 2017 looks
    # like it exits with 1 on errors.  The C++ for Python 2.7 environment has
    # 'x64' in the path; MSVC 2008 has 'amd64' in the 64 bit path.
    libpath = msvc_env.get('LIBPATH')
    if (
        not libpath
        or (not vc_x64 and ('x64' in libpath or 'amd64' in libpath))
        or (vc_x64 and 'x64' not in libpath and 'amd64' not in libpath)
    ):
        raise Exception('Unable to determine MSVC %s environment' % msvc_ver)

    # Visual C++ for Python has '\9.0\', but MSVC 2008 has
    # '\Microsoft Visual Studio 9.0\'.  MSVC 2015 is 14.0.
    if '9.0\\' not in libpath and '14.0\\' not in libpath:
        raise Exception('Unable to determine MSVC %s environment' % msvc_ver)

    return msvc_env


def _get_msvc_env(vcvarsall_bat: pathlib.Path, args=None):
    """Scrape the environment that would be created by running ``vcvarsall_bat``
    and return it in an environment dictionary.
    """
    arglist = ''
    if args:
        arglist = ' '.join(args)

    out = subprocess.check_output(
        'cmd.exe /c ""%s" %s > NUL" && set' % (vcvarsall_bat, arglist),
        text=True,
    )

    msvc_env = {}

    # Windows doesn't care about the case of environment variables, and some
    # key ones default to mixed case, like 'Path'.  Python does care because it
    # uses a `dict`, so uppercase the key like `os.environ` does to let the
    # rest of the code access things consistently.
    #
    # https://bugs.python.org/issue28824
    for line in out.splitlines():
        try:
            k, v = line.split('=', 1)
            msvc_env[k.upper()] = v
        except ValueError:
            pass  # PS1, when run under MSYS, has a few newlines by default

    return msvc_env
