DEVenv/win_portable_msvc.py

386 lines
13 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).
#
# Changelog
# 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
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"
def download(url):
with urllib.request.urlopen(url) as res:
return res.read()
def download_progress(url, check, name, f):
data = io.BytesIO()
with urllib.request.urlopen(url) 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
manifest = json.loads(download(MANIFEST_URL))
### 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 = args.msvc_version or max(sorted(msvc.keys()))
sdk_ver = args.sdk_version or max(sorted(sdk.keys()))
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: f{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: f{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
### 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",
]
for pkg in msvc_packages:
p = first(packages[pkg], lambda p: p.get("language") in (None, "en-US"))
for payload in p["payloads"]:
with tempfile.TemporaryFile() as f:
data = download_progress(payload["url"], payload["sha256"], pkg, f)
total_download += len(data)
with zipfile.ZipFile(f) 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",
]
with tempfile.TemporaryDirectory() as d:
dst = Path(d)
sdk_pkg = packages[sdk_pid][0]
sdk_pkg = packages[first(sdk_pkg["dependencies"], lambda x: True).lower()][0]
msi = []
cabs = []
# download msi files
for pkg in sdk_packages:
payload = first(sdk_pkg["payloads"], lambda p: p["fileName"] == f"Installers\\{pkg}")
msi.append(dst / pkg)
with open(dst / pkg, "wb") as f:
data = download_progress(payload["url"], payload["sha256"], pkg, f)
total_download += len(data)
cabs += list(get_msi_cabs(data))
# download .cab files
for pkg in cabs:
payload = first(sdk_pkg["payloads"], lambda p: p["fileName"] == f"Installers\\{pkg}")
with open(dst / pkg, "wb") as f:
download_progress(payload["url"], payload["sha256"], pkg, f)
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("*"))[0].name
if install_sdk:
sdkv = list((OUTPUT / "Windows Kits/10/bin").glob("*"))[0].name
# place debug CRT runtime into MSVC folder (not what real Visual Studio installer does... but is reasonable)
if install_msvc:
dst = str(OUTPUT / "VC/Tools/MSVC" / msvcv / f"bin/Host{HOST}/{TARGET}")
pkg = "microsoft.visualcpp.runtimedebug.14"
dbg = packages[pkg][0]
payload = first(dbg["payloads"], lambda p: p["fileName"] == "cab1.cab")
try:
with tempfile.TemporaryFile(suffix=".cab", delete=False) as f:
data = download_progress(payload["url"], payload["sha256"], pkg, f)
total_download += len(data)
subprocess.check_call(["expand.exe", f.name, "-F:*", dst], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
finally:
os.unlink(f.name)
# 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}'
if not os.path.exists(redist_versioned_dir):
os.makedirs(redist_versioned_dir)
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)
if install_msvc:
for f in ["Auxiliary", f"lib/{TARGET}/store", f"lib/{TARGET}/uwp"]:
shutil.rmtree(OUTPUT / "VC/Tools/MSVC" / msvcv / f)
for f in OUTPUT.glob("*.msi"):
f.unlink()
if install_sdk:
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:
if install_msvc:
shutil.rmtree(OUTPUT / "VC/Tools/MSVC" / msvcv / f"bin/Host{arch}", ignore_errors=True)
if install_sdk:
shutil.rmtree(OUTPUT / "Windows Kits/10/bin" / sdkv / arch)
shutil.rmtree(OUTPUT / "Windows Kits/10/Lib" / sdkv / "ucrt" / arch)
shutil.rmtree(OUTPUT / "Windows Kits/10/Lib" / sdkv / "um" / arch)
### 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 SDK_VERSION={sdkv}
set SDK_ARCH={TARGET}
set MSVC_ROOT=%~dp0VC\\Tools\\MSVC\\%MSVC_VERSION%
set SDK_INCLUDE=%~dp0Windows Kits\\10\\Include\\%SDK_VERSION%
set SDK_LIBS=%~dp0Windows Kits\\10\\Lib\\%SDK_VERSION%
set VCToolsInstallDir=%MSVC_ROOT%\\
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 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-{args.msvc_version}.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 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-{args.sdk_version}.bat").write_text(WIN10_SDK_SCRIPT)
print(f"Total downloaded: {total_download>>20} MB")
print("Done!")