Skip to content

Instantly share code, notes, and snippets.

@mmozeiko
Last active March 13, 2025 03:42
Download MSVC compiler/linker & Windows SDK without installing full Visual Studio

This downloads standalone 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 native desktop app development.

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

You can list available versions with py.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 first run setup_TARGET.bat - after that PATH/INCLUDE/LIB env variables will be updated 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).

#!/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, filename):
fpath = DOWNLOADS / filename
if fpath.exists():
data = fpath.read_bytes()
if hashlib.sha256(data).hexdigest() == check.lower():
print(f"\r{filename} ... 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{filename} ... {perc}%", end="")
print()
data = data.getvalue()
digest = hashlib.sha256(data).hexdigest()
if check.lower() != digest:
sys.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 = lambda x: True):
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:
sys.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")
sys.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.VC.".lower()) and pid.endswith(".Tools.HostX64.TargetX64.base".lower()):
pver = ".".join(pid.split(".")[2:4])
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())))
sys.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(".")[2:6])
else:
sys.exit(f"Unknown MSVC version: f{args.msvc_version}")
if sdk_ver in sdk:
sdk_pid = sdk[sdk_ver]
else:
sys.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":
sys.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}.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}.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"]
redist_suffix = ".onecore.desktop" if target == "arm" else ""
redist_pkg = f"microsoft.vc.{msvc_ver}.crt.redist.{target}{redist_suffix}.base"
if redist_pkg not in packages:
redist_name = f"microsoft.visualcpp.crt.redist.{target}{redist_suffix}"
redist = first(packages[redist_name])
redist_pkg = first(redist["dependencies"], lambda dep: dep.endswith(".base")).lower()
msvc_packages += [redist_pkg]
for pkg in sorted(msvc_packages):
if pkg not in packages:
print(f"\r{pkg} ... !!! MISSING !!!")
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"], 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"Universal CRT Headers Libraries and Sources-x86_en-us.msi",
]
for target in ALL_TARGETS:
sdk_packages += [
f"Windows SDK Desktop Headers {target}-x86_en-us.msi",
f"Windows SDK OnecoreUap Headers {target}-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"]).lower()][0]
msi = []
cabs = []
# download msi files
for pkg in sorted(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)
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)
print("Unpacking msi files...")
# run msi installers
for m in msi:
subprocess.check_call(f'msiexec.exe /a "{m}" /quiet /qn TARGETDIR="{OUTPUT.resolve()}"')
(OUTPUT / m.name).unlink()
### versions
msvcv = first((OUTPUT / "VC/Tools/MSVC").glob("*")).name
sdkv = first((OUTPUT / "Windows Kits/10/bin").glob("*")).name
# place debug CRT runtime files into MSVC bin folder (not what real Visual Studio installer does... but is reasonable)
# NOTE: these are Target architecture, not Host architecture binaries
redist = OUTPUT / "VC/Redist"
if redist.exists():
redistv = first((redist / "MSVC").glob("*")).name
src = redist / "MSVC" / redistv / "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)
# copy msdia140.dll file into MSVC bin folder
# NOTE: this is meant only for development - always Host architecture, even when placed into all Target architecture folders
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)
# extra files for nvcc
build = OUTPUT / "VC/Auxiliary/Build"
build.mkdir(parents=True, exist_ok=True)
(build / "vcvarsall.bat").write_text("rem both bat files are here only for nvcc, do not call them manually")
(build / "vcvars64.bat").touch()
### setup.bat
for target in targets:
SETUP = fr"""@echo off
set VSCMD_ARG_HOST_ARCH={host}
set VSCMD_ARG_TGT_ARCH={target}
set VCToolsVersion={msvcv}
set WindowsSDKVersion={sdkv}\
set VCToolsInstallDir=%~dp0VC\Tools\MSVC\{msvcv}\
set WindowsSdkBinPath=%~dp0Windows Kits\10\bin\
set PATH=%~dp0VC\Tools\MSVC\{msvcv}\bin\Host{host}\{target};%~dp0Windows Kits\10\bin\{sdkv}\{host};%~dp0Windows Kits\10\bin\{sdkv}\{host}\ucrt;%PATH%
set INCLUDE=%~dp0VC\Tools\MSVC\{msvcv}\include;%~dp0Windows Kits\10\Include\{sdkv}\ucrt;%~dp0Windows Kits\10\Include\{sdkv}\shared;%~dp0Windows Kits\10\Include\{sdkv}\um;%~dp0Windows Kits\10\Include\{sdkv}\winrt;%~dp0Windows Kits\10\Include\{sdkv}\cppwinrt
set LIB=%~dp0VC\Tools\MSVC\{msvcv}\lib\{target};%~dp0Windows Kits\10\Lib\{sdkv}\ucrt\{target};%~dp0Windows Kits\10\Lib\{sdkv}\um\{target}
"""
(OUTPUT / f"setup_{target}.bat").write_text(SETUP)
print(f"Total downloaded: {total_download>>20} MB")
print("Done!")
@madebr
Copy link

madebr commented Jan 20, 2025

Running portable-msvc.py --target x86 --accept-license fails with the following error:

Traceback (most recent call last):
  File "D:\portable-msvc.py", line 261, in <module>
    sdk_pkg = packages[first(sdk_pkg["packages"]).lower()][0]

@mmozeiko
Copy link
Author

What is the full error when that happens? It works for me.
You may want to put something like print(sdk_pid, json.dumps(sdk_pkg, indent=2)) before line 261 and see what it prints out. If it prints out empty json then dump the whole packages json to see look inside to figure out why sdk_pid is not there.

@madebr
Copy link

madebr commented Jan 20, 2025

What is the full error when that happens? It works for me. You may want to put something like print(sdk_pid, json.dumps(sdk_pkg, indent=2)) before line 261 and see what it prints out. If it prints out empty json then dump the whole packages json to see look inside to figure out why sdk_pid is not there.

After downloading the latest portable-msvc script, the error went away.
Not sure what went wrong since wget'ed it yesterday.
The diff shows "packages" was replaced with "dependencies".

Sorry about the false alarm.

@mmozeiko
Copy link
Author

Updated code so it can find latest 14.43 preview version.

@CortexReaver
Copy link

CortexReaver commented Feb 23, 2025

there is no "setup_TARGET.bat" and "setup_x64.bat" doesn't do ANYTHING at all. Windows 10 Pro 22H2 19045.5247 running it from admin.
The setx command is working for me, not set

@mmozeiko
Copy link
Author

mmozeiko commented Feb 23, 2025

TARGET there means which target architecture you want to target. You run for whichever target you'll want to compile.
You don't need to run it from admin. That's irrelevant. You run it when you need environment set up. What exactly do you mean that bat file doesn't do anything?

@CortexReaver
Copy link

CortexReaver commented Feb 23, 2025

Because it doesn't do anything.
This works tho:

@echo off

setx VSCMD_ARG_HOST_ARCH x64
setx VSCMD_ARG_TGT_ARCH x64

setx VCToolsVersion 14.43.34808
setx WindowsSDKVersion 10.0.26100.0\

setx VCToolsInstallDir "%~dp0VC\Tools\MSVC\14.43.34808\"
setx WindowsSdkBinPath "%~dp0Windows Kits\10\bin\"

setx PATH "%~dp0VC\Tools\MSVC\14.43.34808\bin\Hostx64\x64;%~dp0Windows Kits\10\bin\10.0.26100.0\x64;%~dp0Windows Kits\10\bin\10.0.26100.0\x64\ucrt;%PATH%"
setx INCLUDE "%~dp0VC\Tools\MSVC\14.43.34808\include;%~dp0Windows Kits\10\Include\10.0.26100.0\ucrt;%~dp0Windows Kits\10\Include\10.0.26100.0\shared;%~dp0Windows Kits\10\Include\10.0.26100.0\um;%~dp0Windows Kits\10\Include\10.0.26100.0\winrt;%~dp0Windows Kits\10\Include\10.0.26100.0\cppwinrt"
setx LIB "%~dp0VC\Tools\MSVC\14.43.34808\lib\x64;%~dp0Windows Kits\10\Lib\10.0.26100.0\ucrt\x64;%~dp0Windows Kits\10\Lib\10.0.26100.0\um\x64"

After that you still need to cleanup some quote symbols in the variables but it is still better than adding all of these by hand.

@mmozeiko
Copy link
Author

mmozeiko commented Feb 23, 2025

You are wrong about "anything". It does something. And I think you are misunderstanding what it is supposed to do. It only sets up environment for current command prompt session. That's is its only purpose.

It's works similarly like vcvarsall.bat file in full VS installation. Read more information about it here: https://learn.microsoft.com/en-us/cpp/build/building-on-the-command-line?view=msvc-170

It won't modify global system environment. You usually don't want that - because what if you need different target architecture? Or different version compiler? Manipulating global environment will be super messy. This is not an installer script. It just places files in local folder and its up to you to use them correctly.

@CortexReaver
Copy link

You still don't get it. It won't add ANY variables at all, nor global or user specific. After running the bat file nothing has changed there at all. As I described earlier "set" has not worked for me, maybe this is an issue with my specific version of windows, idk. But I've provided an alternative bat file in case there some newbie like me who doesn't want to deal with all the M$ crap.

@mmozeiko
Copy link
Author

mmozeiko commented Feb 24, 2025

It won't add ANY variables at all, nor global or user specific.

Yes, that's expected. That is how it is supposed to work. I just sets variables in current command prompt without affecting global user or system env variables. As I said above, it works the same way how vcvarsall.bat file works. Please read the link I posted above.

@ysiivan
Copy link

ysiivan commented Feb 25, 2025

Installed ok thanks., Environment set. Rust installer still says "Rust requires a linker and Windows API libraries but they don't seem to be
available."
Any idea what else needs to be done?

@mmozeiko
Copy link
Author

I have never used rust, so I do not know how rust finds & uses C/C++ compiler or linker. You'll need to figure out it yourself. For example, for python it is documented here: https://docs.python.org/3.9/distutils/apiref.html#module-distutils.msvccompiler - it requires to set DISTUTILS_USE_SDK env variable. Maybe similar thing exists for rust.

@ysiivan
Copy link

ysiivan commented Feb 26, 2025

Adding this here in case someone ends up in the same boat like me. The Rust installer (and rustup I'm told) check for these two env var: VCINSTALLDIR and VCINSTALLDIR.
I think the installer just uses them as flags (set or not set), but in any case I set mine like so (PowerShell):

# $PSScriptRoot is the msvc folder
$env:VCINSTALLDIR = "$PSScriptRoot\VC\"
$env:VSINSTALLDIR = "$PSScriptRoot\"

Big thanks to you for the portable msvc and to the great folks from the Rust forum that helped with this!

@benjcarson
Copy link

Error on line 287? Ensure your execution path has no spaces for subprocess: subprocess.check_call(["msiexec.exe", "/a", m, "/quiet", "/qn", f"TARGETDIR={OUTPUT.resolve()}"]) to work.

Another reason this error may occur is if you run this script from a drive other than c:\. Apparently msiexec won't install .msi files located on secondary drives. Is anyone is aware of a command line option to make that work?

@mk14steve
Copy link

Brilliant. I set it up for Arm64 and I can compile c++ and arm64 asm from command line. Don't need bloaty visual studio!

@marakew
Copy link

marakew commented Mar 11, 2025

why your remove DIA SDK, can you keep it and normalize path instead %20 ?
also threre are need todo clean up in
Windows Kits\10\Redist\ D3D\{arm64 x64 x86}
and
Windows Kits\10\Redist\NBN\{arm64 x64 x86}

also may be need to add ifc module support as option for downloads

@marakew
Copy link

marakew commented Mar 11, 2025

interesting is it possible to add WDK to this script to allow driver develop bin/include/libs

@marakew
Copy link

marakew commented Mar 11, 2025

can you add formatted output json response files with options like --dump-json ?

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