Skip to content

Instantly share code, notes, and snippets.

@CrendKing
Forked from mmozeiko/!README.md
Last active July 12, 2024 13:18
Show Gist options
  • Save CrendKing/154abfa33200ef1cda38ddd61f4d414b to your computer and use it in GitHub Desktop.
Save CrendKing/154abfa33200ef1cda38ddd61f4d414b to your computer and use it in GitHub Desktop.
Download MSVC compiler/linker & Windows SDK without installing full Visual Studio

This downloads standalone 64-bit MSVC compiler, linker & other tools, also headers/libraries from Windows SDK into portable folder, without installing Visual Studio. Has bare minimum components - no UWP/Store/WindowsRT stuff, just files & tools for 64-bit native desktop app development.

Run python.exe portable-msvc.py and it will download output into msvc folder. By default it will download latest available MSVC & Windows SDK - currently v14.32.17.2 and v10.0.22621.0.

You can list available versions with python.exe portable-msvc.py --show-versions and then pass versions you want with --msvc-version and --sdk-version arguments.

To use cl.exe/link.exe from output folder, first run setup.bat - after that PATH/INCLUDE/LIB env variables will be setup to use all the tools as usual. You can also use clang-cl.exe with these includes & libraries.

To use clang-cl.exe without running setup.bat, pass extra /winsysroot msvc argument (msvc is folder name where output is stored).


Modifications:

  1. The original script uses system's default temporary directory, which most likely exists in the system drive (C:), and uses pathlib's Path.replace() to move extracted files to destination. Unfortunately, Path.replace() only works if the source and destination are on the same drive (it's more of a rename than move). This modified version creates temporary directory in the same directory of the script, fixing the problem.
  2. Generates a setup.ps1 for PowerShell users.
#!/usr/bin/env python3
import io
import os
import sys
import stat
import json
import shutil
import hashlib
import zipfile
import tempfile
import argparse
import subprocess
import urllib.error
import urllib.request
from pathlib import Path
OUTPUT = Path("msvc") # output folder
DOWNLOADS = Path("downloads") # temporary download files
# NOTE: not all host & target architecture combinations are supported
DEFAULT_HOST = "x64"
ALL_HOSTS = "x64 x86 arm64".split()
DEFAULT_TARGET = "x64"
ALL_TARGETS = "x64 x86 arm arm64".split()
MANIFEST_URL = "https://aka.ms/vs/17/release/channel"
MANIFEST_PREVIEW_URL = "https://aka.ms/vs/17/pre/channel"
ssl_context = None
def download(url):
with urllib.request.urlopen(url, context=ssl_context) as res:
return res.read()
total_download = 0
def download_progress(url, check, name, filename):
fpath = DOWNLOADS / filename
if fpath.exists():
data = fpath.read_bytes()
if hashlib.sha256(data).hexdigest() == check.lower():
print(f"\r{name} ... OK")
return data
global total_download
with fpath.open("wb") as 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}")
total_download += len(data)
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)), None)
### parse command-line arguments
ap = argparse.ArgumentParser()
ap.add_argument("--show-versions", action="store_true", help="Show available MSVC and Windows SDK versions")
ap.add_argument("--accept-license", action="store_true", 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("--preview", action="store_true", help="Use preview channel for Preview versions")
ap.add_argument("--target", default=DEFAULT_TARGET, help=f"Target architectures, comma separated ({','.join(ALL_TARGETS)})")
ap.add_argument("--host", default=DEFAULT_HOST, help=f"Host architecture", choices=ALL_HOSTS)
args = ap.parse_args()
host = args.host
targets = args.target.split(',')
for target in targets:
if target not in ALL_TARGETS:
exit(f"Unknown {target} target architecture!")
### get main manifest
URL = MANIFEST_PREVIEW_URL if args.preview else MANIFEST_URL
try:
manifest = json.loads(download(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(URL))
else:
raise
### download VS manifest
ITEM_NAME = "Microsoft.VisualStudio.Manifests.VisualStudioPreview" if args.preview else "Microsoft.VisualStudio.Manifests.VisualStudio"
vs = first(manifest["channelItems"], lambda x: x["id"] == ITEM_NAME)
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)
msvc_ver = args.msvc_version or max(sorted(msvc.keys()))
sdk_ver = args.sdk_version or max(sorted(sdk.keys()))
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}")
if sdk_ver in sdk:
sdk_pid = sdk[sdk_ver]
else:
exit(f"Unknown Windows SDK version: f{args.sdk_version}")
print(f"Downloading MSVC v{msvc_ver} and Windows SDK v{sdk_ver}")
### 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)
DOWNLOADS.mkdir(exist_ok=True)
### download MSVC
msvc_packages = [
f"microsoft.visualcpp.dia.sdk",
f"microsoft.vc.{msvc_ver}.crt.headers.base",
f"microsoft.vc.{msvc_ver}.crt.source.base",
f"microsoft.vc.{msvc_ver}.asan.headers.base",
f"microsoft.vc.{msvc_ver}.premium.tools.{host}.base.base",
f"microsoft.vc.{msvc_ver}.pgo.headers.base",
]
for target in targets:
msvc_packages += [
f"microsoft.vc.{msvc_ver}.tools.host{host}.target{target}.base",
f"microsoft.vc.{msvc_ver}.tools.host{host}.target{target}.res.base",
f"microsoft.vc.{msvc_ver}.crt.{target}.desktop.base",
f"microsoft.vc.{msvc_ver}.crt.{target}.store.base",
f"microsoft.vc.{msvc_ver}.crt.redist.{target}.base",
f"microsoft.vc.{msvc_ver}.premium.tools.host{host}.target{target}.base",
f"microsoft.vc.{msvc_ver}.pgo.{target}.base",
]
if target in ["x86", "x64"]:
msvc_packages += [
f"microsoft.vc.{msvc_ver}.asan.{target}.base",
]
for pkg in msvc_packages:
if pkg not in packages:
continue
p = first(packages[pkg], lambda p: p.get("language") in (None, "en-US"))
for payload in p["payloads"]:
filename = payload["fileName"]
download_progress(payload["url"], payload["sha256"], pkg, filename)
with zipfile.ZipFile(DOWNLOADS / filename) 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
sdk_packages = [
f"Windows SDK for Windows Store Apps Tools-x86_en-us.msi",
f"Windows SDK for Windows Store Apps Headers-x86_en-us.msi",
f"Windows SDK for Windows Store Apps Headers OnecoreUap-x86_en-us.msi",
f"Windows SDK for Windows Store Apps Libs-x86_en-us.msi",
f"Windows SDK OnecoreUap Headers x86-x86_en-us.msi",
f"Windows SDK Desktop Headers x86-x86_en-us.msi",
f"Universal CRT Headers Libraries and Sources-x86_en-us.msi",
]
for target in targets:
sdk_packages += [
f"Windows SDK Desktop Libs {target}-x86_en-us.msi",
]
with tempfile.TemporaryDirectory(dir=DOWNLOADS) 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}")
if payload is None:
continue
msi.append(DOWNLOADS / pkg)
data = download_progress(payload["url"], payload["sha256"], pkg, pkg)
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}")
download_progress(payload["url"], payload["sha256"], pkg, pkg)
print("Unpacking msi files...")
# run msi installers
for m in msi:
subprocess.check_call(["msiexec.exe", "/a", m, "/quiet", "/qn", f"TARGETDIR={OUTPUT.resolve()}"])
(OUTPUT / m.name).unlink()
### versions
msvcv = list((OUTPUT / "VC/Tools/MSVC").glob("*"))[0].name
sdkv = list((OUTPUT / "Windows Kits/10/bin").glob("*"))[0].name
# place debug CRT runtime files into MSVC bin filder (not what real Visual Studio installer does... but is reasonable)
redist = OUTPUT / "VC/Redist"
if redist.exists():
src = redist / "MSVC" / msvcv / "debug_nonredist"
for target in targets:
for f in (src / target).glob("**/*.dll"):
dst = OUTPUT / "VC/Tools/MSVC" / msvcv / f"bin/Host{host}" / target
f.replace(dst / f.name)
shutil.rmtree(redist)
# move msdia140.dll file into MSVC folder
msdia140dll = {
"x86": "msdia140.dll",
"x64": "amd64/msdia140.dll",
"arm": "arm/msdia140.dll",
"arm64": "arm64/msdia140.dll",
}
dst = OUTPUT / "VC/Tools/MSVC" / msvcv / f"bin/Host{host}"
src = OUTPUT / "DIA%20SDK/bin" / msdia140dll[host]
for target in targets:
shutil.copyfile(src, dst / target / src.name)
shutil.rmtree(OUTPUT / "DIA%20SDK")
### cleanup
shutil.rmtree(OUTPUT / "Common7", ignore_errors=True)
shutil.rmtree(OUTPUT / "VC/Tools/MSVC" / msvcv / "Auxiliary")
for target in targets:
for f in [f"store", "uwp", "enclave", "onecore"]:
shutil.rmtree(OUTPUT / "VC/Tools/MSVC" / msvcv / "lib" / target / f, ignore_errors=True)
shutil.rmtree(OUTPUT / "VC/Tools/MSVC" / msvcv / f"bin/Host{host}" / target / "onecore", ignore_errors=True)
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 not in targets:
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 / "VC/Tools/MSVC" / msvcv / f"bin/Host{arch}", ignore_errors=True)
shutil.rmtree(OUTPUT / "Windows Kits/10/bin" / sdkv / arch, ignore_errors=True)
# executable that is collecting & sending telemetry every time cl/link runs
for target in targets:
(OUTPUT / "VC/Tools/MSVC" / msvcv / f"bin/Host{host}/{target}/vctip.exe").unlink(missing_ok=True)
### setup.bat
for target in targets:
SETUP_BAT = f"""@echo off
set ROOT=%~dp0
set MSVC_VERSION={msvcv}
set MSVC_HOST=Host{host}
set MSVC_ARCH={target}
set SDK_VERSION={sdkv}
set SDK_ARCH={target}
set MSVC_ROOT=%ROOT%VC\\Tools\\MSVC\\%MSVC_VERSION%
set SDK_INCLUDE=%ROOT%Windows Kits\\10\\Include\\%SDK_VERSION%
set SDK_LIBS=%ROOT%Windows Kits\\10\\Lib\\%SDK_VERSION%
set VCToolsInstallDir=%MSVC_ROOT%\\
set PATH=%MSVC_ROOT%\\bin\\%MSVC_HOST%\\%MSVC_ARCH%;%ROOT%Windows Kits\\10\\bin\\%SDK_VERSION%\\%SDK_ARCH%;%ROOT%Windows 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%
"""
SETUP_PS1 = f"""$MSVC_VERSION = '{msvcv}'
$MSVC_HOST = 'Host{host}'
$MSVC_ARCH = '{target}'
$SDK_VERSION = '{sdkv}'
$SDK_ARCH = '{target}'
$MSVC_ROOT = "${{PSScriptRoot}}\\VC\\Tools\\MSVC\\${{MSVC_VERSION}}"
$SDK_INCLUDE = "${{PSScriptRoot}}\\Windows Kits\\10\\Include\\${{SDK_VERSION}}"
$SDK_LIBS = "${{PSScriptRoot}}\\Windows Kits\\10\\Lib\\${{SDK_VERSION}}"
$env:VCToolsInstallDir = "${{MSVC_ROOT}}\\"
$env:PATH = "${{MSVC_ROOT}}\\bin\\${{MSVC_HOST}}\\${{MSVC_ARCH}};${{PSScriptRoot}}\\Windows Kits\\10\\bin\\${{SDK_VERSION}}\\${{SDK_ARCH}};${{PSScriptRoot}}\\Windows Kits\\10\\bin\\${{SDK_VERSION}}\\${{SDK_ARCH}}\\ucrt;${{env:PATH}}"
$env:INCLUDE = "${{MSVC_ROOT}}\\include;${{SDK_INCLUDE}}\\ucrt;${{SDK_INCLUDE}}\\shared;${{SDK_INCLUDE}}\\um;${{SDK_INCLUDE}}\\winrt;${{SDK_INCLUDE}}\\cppwinrt"
$env:LIB = "${{MSVC_ROOT}}\\lib\\${{MSVC_ARCH}};${{SDK_LIBS}}\\ucrt\\${{SDK_ARCH}};${{SDK_LIBS}}\\um\\${{SDK_ARCH}}"
"""
(OUTPUT / f"setup_{target}.bat").write_text(SETUP_BAT)
(OUTPUT / f"setup_{target}.ps1").write_text(SETUP_PS1)
print(f"Total downloaded: {total_download>>20} MB")
print("Done!")
@amymor
Copy link

amymor commented Oct 27, 2023

Thank you
So, I'm going to put my code in setup.bat because I don't want to use the /k switch for batch files. I personally don't use PowerShell due to its slow startup time. It used to take about 5 seconds on previous Installation of Windows, even after a clean install, a custom OS, and using some tweaks like Atlas. It's now down to about 2 seconds, but still cant beat batch files. It seems I'm very sensitive to startup times, LOL. I've created some apps using the built-in Windows VB.NET framework, and all of them have a 1-second startup delay (in my tests, a 64-bit app had 900ms while a 32-bit app had 1008ms). It seems I can't achieve the speed of VB6 in .NET. I've also tried some other apps that written in .NET (like Paint.NET) to see if they have a better startup time, but none of them had a startup delay of less than 1-2 seconds. In my opinion, PowerShell is similar to VB.NET and batch is similar to VB6. So, I'm going to learn more and more languages like Python and C++ and Java. I've also created a test form in C++ in CodeBlocks, and it started immediately like VB6. However, I heard that Java uses JVM to run, so it seems like .NET in terms of startup.
I'm also thinking about expanding setup.bat to offer more options for compiling and support drag and drop. I've already created a portable and simple GUI app to use the VB.NET framework compiler (vbc.exe), so I might create another one for the C++ compiler too.
0067

@CrendKing
Copy link
Author

CrendKing commented Oct 27, 2023

Feel free to make a fork and add whatever features you like to setup script. This fork tries to maintain consistency with VS default.

@MSK61
Copy link

MSK61 commented Jan 13, 2024

Thanks a lot. I had the same problems with the original repo that this fork fixed.

@goyalyashpal
Copy link

goyalyashpal commented Apr 27, 2024

hi! can you rebase with upstream? there are several commits there after the fork here (4 in the last 3 months):

for doing a 3-way merge/diff at home (say using meld or kdiff3 etc, here are raw links:

$ # Short: 
$ # kdiff3 --auto -o portable-msvc_{merged,<base>,<mmozeiko>,<CrendKing>}.py
$ kdiff3 --auto --merge --output portable-msvc_merged.py --base portable-msvc_{<base>,<mmozeiko>,<CrendKing>}.py

@CrendKing
Copy link
Author

Rebased to the latest (version "Mar 22").

@HyperLemons
Copy link

Can you rebase again?

@CrendKing
Copy link
Author

Sure. Updated to revision "Jul 2, 2024, 5:37 AM PDT". The original already changed to download to a sub-directory, so this fork only needs to add the PowerShell script from now on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment