DEVenv/win_portable_msvc.py

491 lines
18 KiB
Python

#!/usr/bin/env python3
# This script has been gratefully sourced from Martins Mozeiko of HMN
# https://gist.github.com/mmozeiko/7f3162ec2988e81e56d5c4e22cde9977
#
# Further modifications by https://github.com/doy-lee with the primary purpose
# of facilitating multiple versions to be stored in the same root directory.
# ('Redist' in the SDK was unversioned, store it versioned like all other
# folders, skip the downloading of MSVC or the SDK if we only need one of them).
# We have some other useful features ..
#
# - Cache the downloads to ./msvc/Installers instead of a temporary directory
# meaning downloads that are interrupted can be resumed/reused.
# - Allow downloading of _just_ the SDK or _just_ the VC toolchain.
# - Create scripts for each individual component,
#
# msvc-{version}.bat
# win-sdk-{version}.bat
#
# - Allow multiple versions to co-exist in the same installation root
#
# Changelog
# 2024-03-14
# - Fold in mmozeiko's latest changes from revision 2024-03-14
# a6303b82561a4061c71e8838976143f06f295cc9a8a60cf53fc170cb5b9f3f1d
# - SSL checks
# - Removal of vctip.exe (telemetry)
# - DIA SDK DLL
#
# 2024-02-21
# - Add more environment variables that Windows may use, `WindowsSDKVersion`
# `WindowsSDKDir`, `VSCMD_ARG_TGT_ARCH`
# - Cache the downloads into the `msvc/Installers` directory to allow reusing
# or resuming an installation that has failed or been terminated prematurely.
# - Fix output scripts using MSVC/SDK version arguments which could be empty
# if defaulted.
#
# 2023-04-15
# - Fix "msvc-{version}.bat" script generating trailing "\;" on
# VCToolsInstallDir environment variable. clang-cl relies on this variable to
# identify the location of the Visual Studio toolchain or otherwise reports a
# program invocation error during the linking stage.
#
# 2023-01-30
# - Generate the short-hand version of the msvc-{version}.bat and
# win-sdk-{version}.bat using the versions passed as the argument parameter.
# - Fix win-sdk-{version}.bat overwriting the INCLUDE and LIB environment
# variables instead of appending.
#
# 2023-01-28
# - Inital revision from mmozeiko
# https://gist.github.com/mmozeiko/7f3162ec2988e81e56d5c4e22cde9977/6863f19cb98b933c7535acf3d59ac64268c6bd1b
# - Add individual scripts to source variables for MSVC and Windows 10
# separately "msvc-{version}.bat" and "win-sdk-{version}.bat"
# - Add '--no-sdk' and '--no-msvc' to prevent the download and installation of
# the Windows SDK and MSVC respectively.
# - Installation used to create 'Windows Kit/10/Redist' and unpack a D3D and MBN
# folder without being versioned. These folders are now placed under
# a versioned sub-directory to preserve the binaries and allow subsequent
# side-by-side installation of other versions of the SDK.
import io
import os
import sys
import json
import shutil
import hashlib
import zipfile
import tempfile
import argparse
import subprocess
import urllib.request
import urllib.error
from pathlib import Path
OUTPUT = Path("msvc") # output folder
# other architectures may work or may not - not really tested
HOST = "x64" # or x86
TARGET = "x64" # or x86, arm, arm64
MANIFEST_URL = "https://aka.ms/vs/17/release/channel"
ssl_context = None
def download(url):
with urllib.request.urlopen(url, context=ssl_context) as res:
return res.read()
def download_progress(url, check, name, f):
data = io.BytesIO()
with urllib.request.urlopen(url, context=ssl_context) as res:
total = int(res.headers["Content-Length"])
size = 0
while True:
block = res.read(1<<20)
if not block:
break
f.write(block)
data.write(block)
size += len(block)
perc = size * 100 // total
print(f"\r{name} ... {perc}%", end="")
print()
data = data.getvalue()
digest = hashlib.sha256(data).hexdigest()
if check.lower() != digest:
exit(f"Hash mismatch for f{pkg}")
return data
# super crappy msi format parser just to find required .cab files
def get_msi_cabs(msi):
index = 0
while True:
index = msi.find(b".cab", index+4)
if index < 0:
return
yield msi[index-32:index+4].decode("ascii")
def first(items, cond):
return next(item for item in items if cond(item))
### parse command-line arguments
ap = argparse.ArgumentParser()
ap.add_argument("--show-versions", const=True, action="store_const", help="Show available MSVC and Windows SDK versions")
ap.add_argument("--accept-license", const=True, action="store_const", help="Automatically accept license")
ap.add_argument("--msvc-version", help="Get specific MSVC version")
ap.add_argument("--sdk-version", help="Get specific Windows SDK version")
ap.add_argument("--no-msvc", const=True, action="store_const", help="Skip download and installing of msvc")
ap.add_argument("--no-sdk", const=True, action="store_const", help="Skip download and installing of Windows SDK")
args = ap.parse_args()
### get main manifest
try:
manifest = json.loads(download(MANIFEST_URL))
except urllib.error.URLError as err:
import ssl
if isinstance(err.args[0], ssl.SSLCertVerificationError):
# for more info about Python & issues with Windows certificates see https://stackoverflow.com/a/52074591
print("ERROR: ssl certificate verification error")
try:
import certifi
except ModuleNotFoundError:
print("ERROR: please install 'certifi' package to use Mozilla certificates")
print("ERROR: or update your Windows certs, see instructions here: https://woshub.com/updating-trusted-root-certificates-in-windows-10/#h2_3")
exit()
print("NOTE: retrying with certifi certificates")
ssl_context = ssl.create_default_context(cafile=certifi.where())
manifest = json.loads(download(MANIFEST_URL))
else:
raise
### download VS manifest
vs = first(manifest["channelItems"], lambda x: x["id"] == "Microsoft.VisualStudio.Manifests.VisualStudio")
payload = vs["payloads"][0]["url"]
vsmanifest = json.loads(download(payload))
### find MSVC & WinSDK versions
packages = {}
for p in vsmanifest["packages"]:
packages.setdefault(p["id"].lower(), []).append(p)
msvc = {}
sdk = {}
for pid,p in packages.items():
if pid.startswith("Microsoft.VisualStudio.Component.VC.".lower()) and pid.endswith(".x86.x64".lower()):
pver = ".".join(pid.split(".")[4:6])
if pver[0].isnumeric():
msvc[pver] = pid
elif pid.startswith("Microsoft.VisualStudio.Component.Windows10SDK.".lower()) or \
pid.startswith("Microsoft.VisualStudio.Component.Windows11SDK.".lower()):
pver = pid.split(".")[-1]
if pver.isnumeric():
sdk[pver] = pid
if args.show_versions:
print("MSVC versions:", " ".join(sorted(msvc.keys())))
print("Windows SDK versions:", " ".join(sorted(sdk.keys())))
exit(0)
install_sdk = not args.no_sdk
install_msvc = not args.no_msvc
if args.no_sdk and args.no_msvc:
exit()
msvc_ver_key = args.msvc_version or max(sorted(msvc.keys()))
sdk_ver_key = args.sdk_version or max(sorted(sdk.keys()))
msvc_ver = msvc_ver_key
sdk_ver = sdk_ver_key
info_line = "Downloading"
if install_msvc:
if msvc_ver in msvc:
msvc_pid = msvc[msvc_ver]
msvc_ver = ".".join(msvc_pid.split(".")[4:-2])
else:
exit(f"Unknown MSVC version: {args.msvc_version}")
info_line += f" MSVC v{msvc_ver}"
if install_sdk:
if sdk_ver in sdk:
sdk_pid = sdk[sdk_ver]
else:
exit(f"Unknown Windows SDK version: {args.sdk_version}")
info_line += f" Windows SDK v{sdk_ver}"
print(info_line)
### agree to license
tools = first(manifest["channelItems"], lambda x: x["id"] == "Microsoft.VisualStudio.Product.BuildTools")
resource = first(tools["localizedResources"], lambda x: x["language"] == "en-us")
license = resource["license"]
if not args.accept_license:
accept = input(f"Do you accept Visual Studio license at {license} [Y/N] ? ")
if not accept or accept[0].lower() != "y":
exit(0)
OUTPUT.mkdir(exist_ok=True)
total_download = 0
OUTPUT_CACHE_DIR = OUTPUT / "Installers"
OUTPUT_CACHE_DIR.mkdir(exist_ok=True)
OUTPUT_CACHE_MSVC_DIR = OUTPUT_CACHE_DIR / f"msvc-{msvc_ver}-{HOST}-{TARGET}"
OUTPUT_CACHE_WIN_SDK_DIR = OUTPUT_CACHE_DIR / f"win-sdk-{sdk_ver}-{HOST}-{TARGET}"
### download MSVC
if install_msvc:
msvc_packages = [
# MSVC binaries
f"microsoft.vc.{msvc_ver}.tools.host{HOST}.target{TARGET}.base",
f"microsoft.vc.{msvc_ver}.tools.host{HOST}.target{TARGET}.res.base",
# MSVC headers
f"microsoft.vc.{msvc_ver}.crt.headers.base",
# MSVC libs
f"microsoft.vc.{msvc_ver}.crt.{TARGET}.desktop.base",
f"microsoft.vc.{msvc_ver}.crt.{TARGET}.store.base",
# MSVC runtime source
f"microsoft.vc.{msvc_ver}.crt.source.base",
# ASAN
f"microsoft.vc.{msvc_ver}.asan.headers.base",
f"microsoft.vc.{msvc_ver}.asan.{TARGET}.base",
# MSVC redist
#f"microsoft.vc.{msvc_ver}.crt.redist.x64.base",
]
OUTPUT_CACHE_MSVC_DIR.mkdir(exist_ok=True)
for pkg in msvc_packages:
p = first(packages[pkg], lambda p: p.get("language") in (None, "en-US"))
for payload in p["payloads"]:
pkg_path = OUTPUT_CACHE_MSVC_DIR / pkg
if not os.path.exists(pkg_path):
with tempfile.NamedTemporaryFile(delete=False) as f:
data = download_progress(payload["url"], payload["sha256"], pkg, f)
total_download += len(data)
shutil.move(f.name, pkg_path)
with zipfile.ZipFile(pkg_path) as z:
for name in z.namelist():
if name.startswith("Contents/"):
out = OUTPUT / Path(name).relative_to("Contents")
out.parent.mkdir(parents=True, exist_ok=True)
out.write_bytes(z.read(name))
### download Windows SDK
if install_sdk:
sdk_packages = [
# Windows SDK tools (like rc.exe & mt.exe)
f"Windows SDK for Windows Store Apps Tools-x86_en-us.msi",
# Windows SDK headers
f"Windows SDK for Windows Store Apps Headers-x86_en-us.msi",
f"Windows SDK Desktop Headers x86-x86_en-us.msi",
# Windows SDK libs
f"Windows SDK for Windows Store Apps Libs-x86_en-us.msi",
f"Windows SDK Desktop Libs {TARGET}-x86_en-us.msi",
# CRT headers & libs
f"Universal CRT Headers Libraries and Sources-x86_en-us.msi",
# CRT redist
#"Universal CRT Redistributable-x86_en-us.msi",
]
sdk_pkg = packages[sdk_pid][0]
sdk_pkg = packages[first(sdk_pkg["dependencies"], lambda x: True).lower()][0]
OUTPUT_CACHE_WIN_SDK_DIR.mkdir(exist_ok=True)
msi = []
cabs = []
# download msi files
for pkg in sdk_packages:
payload = first(sdk_pkg["payloads"], lambda p: p["fileName"] == f"Installers\\{pkg}")
msi_path = OUTPUT_CACHE_WIN_SDK_DIR / pkg
msi.append(msi_path)
if not os.path.exists(msi_path):
with tempfile.NamedTemporaryFile(delete=False) as f:
data = download_progress(payload["url"], payload["sha256"], pkg, f)
total_download += len(data)
shutil.move(f.name, msi_path)
with open(msi_path, "rb") as f:
cabs += list(get_msi_cabs(f.read()))
# download .cab files
for pkg in cabs:
payload = first(sdk_pkg["payloads"], lambda p: p["fileName"] == f"Installers\\{pkg}")
cab_path = OUTPUT_CACHE_WIN_SDK_DIR / pkg
if not os.path.exists(cab_path):
with tempfile.NamedTemporaryFile(delete=False) as f:
download_progress(payload["url"], payload["sha256"], pkg, f)
shutil.move(f.name, cab_path)
print("Unpacking msi files...")
# run msi installers
for m in msi:
subprocess.check_call(["msiexec.exe", "/a", m, "/quiet", "/qn", f"TARGETDIR={OUTPUT.resolve()}"])
### versions
msvcv = ""
sdkv = ""
if install_msvc:
msvcv = list((OUTPUT / "VC/Tools/MSVC").glob(f"{msvc_ver_key}*"))[0].name
if install_sdk:
sdkv = list((OUTPUT / "Windows Kits/10/bin").glob(f"*{sdk_ver_key}*"))[0].name
if install_msvc:
# place debug CRT runtime into MSVC folder (not what real Visual Studio installer does... but is reasonable)
dst = OUTPUT / "VC/Tools/MSVC" / msvcv / f"bin/Host{HOST}/{TARGET}"
pkg = "microsoft.visualcpp.runtimedebug.14"
dbg = first(packages[pkg], lambda p: p["chip"] == HOST)
for payload in dbg["payloads"]:
name = payload["fileName"]
pkg_dir = OUTPUT_CACHE_MSVC_DIR /pkg
pkg_dir.mkdir(exist_ok=True)
pkg_path = pkg_dir / name
if not os.path.exists(pkg_path):
with tempfile.NamedTemporaryFile(delete=False) as f:
data = download_progress(payload["url"], payload["sha256"], f"{pkg}/{name}", f)
total_download += len(data)
shutil.move(f.name, pkg_path)
msi = OUTPUT_CACHE_MSVC_DIR / pkg / first(dbg["payloads"], lambda p: p["fileName"].endswith(".msi"))["fileName"]
with tempfile.TemporaryDirectory() as d2:
subprocess.check_call(["msiexec.exe", "/a", str(msi), "/quiet", "/qn", f"TARGETDIR={d2}"])
for f in first(Path(d2).glob("System*"), lambda x: True).iterdir():
f.replace(dst / f.name)
# download DIA SDK and put msdia140.dll file into MSVC folder
pkg = "microsoft.visualc.140.dia.sdk.msi"
dia = packages[pkg][0]
for payload in dia["payloads"]:
name = payload["fileName"]
pkg_dir = OUTPUT_CACHE_MSVC_DIR /pkg
pkg_dir.mkdir(exist_ok=True)
pkg_path = OUTPUT_CACHE_MSVC_DIR / pkg / name
if not os.path.exists(pkg_path):
with tempfile.NamedTemporaryFile(delete=False) as f:
data = download_progress(payload["url"], payload["sha256"], f"{pkg}/{name}", f)
total_download += len(data)
shutil.move(f.name, pkg_path)
msi = OUTPUT_CACHE_MSVC_DIR / pkg / first(dia["payloads"], lambda p: p["fileName"].endswith(".msi"))["fileName"]
with tempfile.TemporaryDirectory() as d2:
subprocess.check_call(["msiexec.exe", "/a", str(msi), "/quiet", "/qn", f"TARGETDIR={d2}"])
if HOST == "x86": msdia = "msdia140.dll"
elif HOST == "x64": msdia = "amd64/msdia140.dll"
else: exit("unknown")
src = Path(d2) / "Program Files" / "Microsoft Visual Studio 14.0" / "DIA SDK" / "bin" / msdia
src.replace(dst / "msdia140.dll")
# place the folders under the Redist folder in the SDK under a versioned folder to allow other versions to be installed
if install_sdk:
redist_dir = OUTPUT / "Windows Kits/10/Redist"
redist_versioned_dir = redist_dir / f'{sdkv}'
shutil.rmtree(redist_versioned_dir, ignore_errors=True)
redist_versioned_dir.mkdir(exist_ok=True)
for file_name in os.listdir(redist_dir):
if not file_name.startswith('10.0.'): # Simple heuristic
shutil.move((redist_dir / file_name), redist_versioned_dir)
### cleanup
shutil.rmtree(OUTPUT / "Common7", ignore_errors=True)
for f in ["Auxiliary", f"lib/{TARGET}/store", f"lib/{TARGET}/uwp"]:
shutil.rmtree(OUTPUT / "VC/Tools/MSVC" / msvcv / f, ignore_errors=True)
for f in OUTPUT.glob("*.msi"):
f.unlink()
for f in ["Catalogs", "DesignTime", f"bin/{sdkv}/chpe", f"Lib/{sdkv}/ucrt_enclave"]:
shutil.rmtree(OUTPUT / "Windows Kits/10" / f, ignore_errors=True)
for arch in ["x86", "x64", "arm", "arm64"]:
if arch != TARGET:
shutil.rmtree(OUTPUT / "Windows Kits/10/Lib" / sdkv / "ucrt" / arch, ignore_errors=True)
shutil.rmtree(OUTPUT / "Windows Kits/10/Lib" / sdkv / "um" / arch, ignore_errors=True)
if arch != HOST:
shutil.rmtree(OUTPUT / "Windows Kits/10/bin" / sdkv / arch, ignore_errors=True)
# executable that is collecting & sending telemetry every time cl/link runs
(OUTPUT / "VC/Tools/MSVC" / msvcv / f"bin/Host{HOST}/{TARGET}/vctip.exe").unlink(missing_ok=True)
### setup.bat
if install_msvc and install_sdk:
SETUP = f"""@echo off
set MSVC_VERSION={msvcv}
set MSVC_HOST=Host{HOST}
set MSVC_ARCH={TARGET}
set MSVC_ROOT=%~dp0VC\\Tools\\MSVC\\%MSVC_VERSION%
set SDK_VERSION={sdkv}
set SDK_ARCH={TARGET}
set SDK_INCLUDE=%~dp0Windows Kits\\10\\Include\\%SDK_VERSION%
set SDK_LIBS=%~dp0Windows Kits\\10\\Lib\\%SDK_VERSION%
set WindowsSDKVersion=%SDK_VERSION%
set WindowsSDKDir=%~dp0Windows Kits\\10
set VCToolsInstallDir=%MSVC_ROOT%
set VSCMD_ARG_TGT_ARCH=%MSVC_ARCH%
set PATH=%MSVC_ROOT%\\bin\\%MSVC_HOST%\\%MSVC_ARCH%;%~dp0Windows Kits\\10\\bin\\%SDK_VERSION%\\%SDK_ARCH%;%~dp0Windows Kits\\10\\bin\\%SDK_VERSION%\\%SDK_ARCH%\\ucrt;%PATH%
set INCLUDE=%MSVC_ROOT%\\include;%SDK_INCLUDE%\\ucrt;%SDK_INCLUDE%\\shared;%SDK_INCLUDE%\\um;%SDK_INCLUDE%\\winrt;%SDK_INCLUDE%\\cppwinrt
set LIB=%MSVC_ROOT%\\lib\\%MSVC_ARCH%;%SDK_LIBS%\\ucrt\\%SDK_ARCH%;%SDK_LIBS%\\um\\%SDK_ARCH%
"""
(OUTPUT / "setup.bat").write_text(SETUP)
if install_msvc:
MSVC_SCRIPT = f"""@echo off
set MSVC_VERSION={msvcv}
set MSVC_HOST=Host{HOST}
set MSVC_ARCH={TARGET}
set MSVC_ROOT=%~dp0VC\\Tools\\MSVC\\%MSVC_VERSION%
set VCToolsInstallDir=%MSVC_ROOT%
set VSCMD_ARG_TGT_ARCH=%MSVC_ARCH%
set PATH=%MSVC_ROOT%\\bin\\%MSVC_HOST%\\%MSVC_ARCH%;%PATH%
set INCLUDE=%MSVC_ROOT%\\include;%INCLUDE%
set LIB=%MSVC_ROOT%\\lib\\%MSVC_ARCH%;%LIB%
"""
(OUTPUT / f"msvc-{msvcv}.bat").write_text(MSVC_SCRIPT)
(OUTPUT / f"msvc-{msvc_ver_key}.bat").write_text(MSVC_SCRIPT)
if install_sdk:
WIN10_SDK_SCRIPT = f"""@echo off
set SDK_VERSION={sdkv}
set SDK_ARCH={TARGET}
set SDK_INCLUDE=%~dp0Windows Kits\\10\\Include\\%SDK_VERSION%
set SDK_LIBS=%~dp0Windows Kits\\10\\Lib\\%SDK_VERSION%
set WindowsSDKVersion=%SDK_VERSION%
set WindowsSDKDir=%~dp0Windows Kits\\10
set PATH=%~dp0Windows Kits\\10\\bin\\%SDK_VERSION%\\%SDK_ARCH%;%~dp0Windows Kits\\10\\bin\\%SDK_VERSION%\\%SDK_ARCH%\\ucrt;%PATH%
set INCLUDE=%SDK_INCLUDE%\\ucrt;%SDK_INCLUDE%\\shared;%SDK_INCLUDE%\\um;%SDK_INCLUDE%\\winrt;%SDK_INCLUDE%\\cppwinrt;%INCLUDE%
set LIB=%SDK_LIBS%\\ucrt\\%SDK_ARCH%;%SDK_LIBS%\\um\\%SDK_ARCH%;%LIB%
"""
(OUTPUT / f"win-sdk-{sdkv}.bat").write_text(WIN10_SDK_SCRIPT)
(OUTPUT / f"win-sdk-{sdk_ver_key}.bat").write_text(WIN10_SDK_SCRIPT)
print(f"Total downloaded: {total_download>>20} MB")
print("Done!")