Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save aont/e65fca49dde0c8553f22b6755fc0e49a to your computer and use it in GitHub Desktop.

Select an option

Save aont/e65fca49dde0c8553f22b6755fc0e49a to your computer and use it in GitHub Desktop.

A Tool to Convert MSVC Environment Settings for MSYS2 Bash

When you use Microsoft Visual C++ (MSVC) from the command line, Visual Studio’s DevShell sets many environment variables (such as INCLUDE, LIB, and Path). These settings are made for Windows shells like cmd.exe or PowerShell, not for MSYS2 bash. As a result, paths and environment formats do not work directly in MSYS2.

The Python tool above automates the conversion:

  1. It runs Visual Studio Build Tools DevShell via PowerShell.

  2. It captures the environment before and after DevShell runs and outputs both as JSON.

  3. It compares these snapshots to find:

    • Added or changed variables.
    • Removed variables.
    • New entries added to Path.
  4. It converts only the new Path entries from Windows style (e.g. C:\Program Files\...) to MSYS style (e.g. /c/Program Files/...) using cygpath.

  5. It generates a bash script that:

    • exports all new or changed variables (except Path).
    • Appends the converted path entries to PATH.
    • unsets variables that were removed (excluding Path for safety).

By running this script and then sourcing the generated bash file in MSYS2, you can recreate the MSVC DevShell environment in a form that works naturally inside MSYS2 bash.

import json
import os
import subprocess
from typing import Dict, List, Tuple
import shlex
import argparse
import ctypes
from ctypes import wintypes
from pathlib import Path
# comtypes is only required for the "shortcut" mode (script B style)
try:
import comtypes.client
except ImportError:
comtypes = None
# ==============================
# Path conversion utilities
# ==============================
def convert_windows_path_list_to_msys(path_value: str) -> str:
"""
Convert a Windows ';'-separated PATH string into ':'-separated
MSYS-style paths (e.g. /c/...).
"""
path_unix = subprocess.check_output(
["C:\\msys64\\usr\\bin\\cygpath.exe", "-u", "-p", path_value],
text=True,
)
path_unix = path_unix.rstrip()
return path_unix
# ==============================
# Shell utilities
# ==============================
def shell_quote(value: str) -> str:
"""
Safely quote a string for use in a POSIX shell.
Example:
foo'bar -> 'foo'"'"'bar'
"""
return shlex.quote(value)
# ==============================
# Env diff → bash script generation
# ==============================
IGNORED_ENV_KEYS = {"__VSCMD_PREINIT_PATH"}
def parse_env_snapshot_json(data_str: str) -> Tuple[Dict[str, str], Dict[str, str]]:
"""
Parse a JSON string passed from PowerShell and return
the 'before' and 'after' environment snapshots as dictionaries.
"""
data = json.loads(data_str)
env_before = data.get("before", {}) or {}
env_after = data.get("after", {}) or {}
return env_before, env_after
def compute_env_changes(
env_before: Dict[str, str],
env_after: Dict[str, str],
) -> Tuple[List[Tuple[str, str]], List[str]]:
"""
Compute the difference between two environment snapshots.
Returns:
added_or_changed: list of (key, value) for added or changed variables
removed: list of keys that were removed
"""
keys_before = set(env_before.keys())
keys_after = set(env_after.keys())
added_or_changed: List[Tuple[str, str]] = []
removed: List[str] = []
# Added or changed variables
for key in sorted(keys_after):
if key in IGNORED_ENV_KEYS:
continue
value_after = env_after[key]
value_before = env_before.get(key)
if value_before != value_after:
added_or_changed.append((key, value_after))
# Removed variables
for key in sorted(keys_before - keys_after):
if key in IGNORED_ENV_KEYS:
continue
removed.append(key)
return added_or_changed, removed
def compute_path_additions(
env_before: Dict[str, str],
env_after: Dict[str, str],
) -> List[str]:
"""
Return only the newly added elements from the Windows 'Path' environment
variable difference.
"""
before_path_raw = env_before.get("Path")
after_path_raw = env_after.get("Path")
if after_path_raw is None:
return []
after_elems = [elem for elem in after_path_raw.split(";") if elem.strip()]
before_elems = [elem for elem in (before_path_raw or "").split(";") if elem.strip()]
before_set = set(before_elems)
return [elem for elem in after_elems if elem not in before_set]
def generate_bash_env_script(data_str: str) -> str:
"""
Generate a bash script that applies export/unset operations
based on the environment diff JSON captured from PowerShell.
"""
env_before, env_after = parse_env_snapshot_json(data_str)
added_or_changed, removed = compute_env_changes(env_before, env_after)
path_additions = compute_path_additions(env_before, env_after)
lines: List[str] = []
# 1. Export all non-Path variables
for key, value in added_or_changed:
if key.lower() == "path":
# Handle Path separately
continue
lines.append(f"export {key}={shell_quote(value)}")
# 2. Convert Path additions to MSYS format and append to PATH
if path_additions:
converted_path = convert_windows_path_list_to_msys(";".join(path_additions))
# On Windows the variable is 'Path' but in bash it's 'PATH'
lines.append(f'PATH="$PATH":{shell_quote(converted_path)}')
# 3. Unset removed variables
for key in removed:
# Even if Path is removed on Windows side, we already have PATH on the MSYS side,
# so unsetting PATH would be dangerous. Skip it.
if key.lower() == "path":
continue
lines.append(f"unset {key}")
return "\n".join(lines) + ("\n" if lines else "")
# ==============================
# VS DevShell invocation (Mode A: vswhere)
# ==============================
def build_vs_dev_shell_pwsh_script_vswhere(instance_id: str, dev_shell_dll_path: str) -> str:
"""
Build a PowerShell script string that calls VS DevShell and
outputs the before/after environment as JSON.
(Script A style)
"""
# DevShell invocation part
dev_shell_call = (
f"&{{ Import-Module {json.dumps(dev_shell_dll_path)}; "
f"Enter-VsDevShell {instance_id} -Arch amd64 -DevCmdArguments -no_logo }};"
)
# Capture before / after environments into Hashtables and convert to JSON
pwsh_script = (
"function Get-EnvAsHashtable { "
"$envTable = @{}; "
"Get-ChildItem Env: | ForEach-Object { $envTable[$_.Name] = $_.Value }; "
"return $envTable "
"}; "
"$before = Get-EnvAsHashtable; "
f"{dev_shell_call} "
"$after = Get-EnvAsHashtable; "
"[pscustomobject]@{ before = $before; after = $after } | ConvertTo-Json"
)
return pwsh_script
def query_latest_vs_buildtools_instance() -> Tuple[str, str]:
"""
Use vswhere to obtain the latest Visual Studio Build Tools instance and
return (instance_id, dev_shell_dll_path).
"""
vswhere_output = subprocess.check_output(
[
os.path.join(os.environ["PROGRAMFILES(X86)"], "Microsoft Visual Studio\\Installer\\vswhere.exe"),
"-latest",
"-products",
"Microsoft.VisualStudio.Product.BuildTools",
"-format",
"json",
],
text=True,
)
vs_instances = json.loads(vswhere_output)
if not vs_instances:
raise RuntimeError("Visual Studio Build Tools not found.")
vs0 = vs_instances[0]
instance_id = vs0["instanceId"]
product_path = vs0["productPath"]
dev_shell_dll_path = os.path.join(
os.path.dirname(product_path),
"Microsoft.VisualStudio.DevShell.dll",
)
return instance_id, dev_shell_dll_path
# ==============================
# VS DevShell invocation (Mode B: Developer PowerShell shortcut)
# ==============================
# Type definitions (used in script B style)
CommandLineToArgvW = ctypes.windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [wintypes.LPCWSTR, ctypes.POINTER(ctypes.c_int)]
CommandLineToArgvW.restype = ctypes.POINTER(wintypes.LPWSTR)
LocalFree = ctypes.windll.kernel32.LocalFree
LocalFree.argtypes = [wintypes.HLOCAL]
LocalFree.restype = wintypes.HLOCAL
def parse_command_line(cmdline: str) -> list[str]:
"""
Parse a Windows command line string using CommandLineToArgvW.
(From script B)
"""
argc = ctypes.c_int()
argv_ptr = CommandLineToArgvW(cmdline, ctypes.byref(argc))
if not argv_ptr:
raise OSError("CommandLineToArgvW failed")
try:
args = [argv_ptr[i] for i in range(argc.value)]
finally:
# Free memory
LocalFree(argv_ptr)
return args
def find_developer_powershell_lnk() -> Path:
"""
Search under
C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs
for a single .lnk that contains both 'Developer' and 'PowerShell' in its file name.
(Based on script B)
"""
base_dir = Path(os.path.join(os.environ["PROGRAMDATA"], "Microsoft\\Windows\\Start Menu\\Programs"))
target_files: List[Path] = []
for path in base_dir.rglob("*.lnk"):
name = path.name
if "Developer" in name and "PowerShell" in name:
target_files.append(path)
if len(target_files) == 0:
raise RuntimeError("Developer PowerShell .lnk not found.")
if len(target_files) > 1:
raise RuntimeError(
"Multiple Developer PowerShell .lnk files were found. "
"You may need to tighten the search criteria."
)
return target_files[0]
def build_vs_dev_shell_pwsh_script_from_shortcut() -> str:
"""
Follow the Developer PowerShell .lnk, extract the Enter-VsDevShell invocation,
modify it to add -DevCmdArguments -no_logo, and wrap it so that it outputs
environment before/after as JSON.
This is essentially script B's approach turned into a function that returns
a PowerShell script string.
"""
if comtypes is None:
raise RuntimeError(
"comtypes is not installed. Please 'pip install comtypes' to use --mode shortcut."
)
lnk_path = str(find_developer_powershell_lnk())
wsh = comtypes.client.CreateObject("WScript.Shell", dynamic=True)
shortcut = wsh.CreateShortcut(lnk_path)
arguments = shortcut.Arguments
arguments_list = parse_command_line(arguments)
# Same assumption as script B: arguments_list[2] is the PowerShell script text
if len(arguments_list) < 3:
raise RuntimeError("Failed to parse Developer PowerShell shortcut arguments.")
pwsh_script = arguments_list[2]
# Insert "-DevCmdArguments -no_logo" right after "Enter-VsDevShell"
enter_vsdevshell_str = "Enter-VsDevShell"
pos = pwsh_script.rfind(enter_vsdevshell_str)
if pos == -1:
raise RuntimeError("'Enter-VsDevShell' not found in shortcut script.")
insert_pos = pos + len(enter_vsdevshell_str)
pwsh_script = (
pwsh_script[:insert_pos]
+ " -DevCmdArguments -no_logo"
+ pwsh_script[insert_pos:]
)
# Wrap with before/after environment capture and JSON output
wrapped_pwsh = (
"function Get-EnvAsHashtable { "
"$envTable = @{}; "
"Get-ChildItem Env: | ForEach-Object { $envTable[$_.Name] = $_.Value }; "
"return $envTable "
"}; "
"$before = Get-EnvAsHashtable; "
f"{pwsh_script}; "
"$after = Get-EnvAsHashtable; "
"[pscustomobject]@{ before = $before; after = $after } | ConvertTo-Json"
)
return wrapped_pwsh
# ==============================
# PowerShell execution
# ==============================
def run_pwsh_and_capture_env_diff_json(pwsh_script: str) -> str:
"""
Run the given PowerShell script via powershell.exe and return the JSON output.
Raise an exception with stderr if execution fails.
"""
# Follow script A: run DevShell under as clean an environment as possible
env = dict()
env["Path"] = ""
env["SYSTEMROOT"] = os.environ["SYSTEMROOT"]
env["DRIVERDATA"] = os.environ.get("DRIVERDATA", "")
env["PROGRAMDATA"] = os.environ.get("PROGRAMDATA", "")
result = subprocess.run(
["powershell.exe", "-NoLogo", "-c", pwsh_script],
text=True,
env=env,
capture_output=True,
)
if result.returncode != 0:
raise RuntimeError(
"Error occurred while running powershell.exe.\n"
f"returncode: {result.returncode}\n"
f"STDOUT:\n{result.stdout}\n"
f"STDERR:\n{result.stderr}\n"
)
if len(result.stderr) > 0:
raise RuntimeError(result.stderr)
return result.stdout
# ==============================
# Main
# ==============================
def main() -> None:
parser = argparse.ArgumentParser(
description="Generate bash exports/unsets for VS DevShell (for MSYS)."
)
parser.add_argument(
"--mode",
choices=["vswhere", "shortcut"],
default="vswhere",
help=(
"Choose how to invoke VS DevShell: "
"'vswhere' = Visual Studio Build Tools (DevShell.dll), "
"'shortcut' = Developer PowerShell shortcut (.lnk)."
),
)
args = parser.parse_args()
if args.mode == "vswhere":
# Mode A: vswhere + DevShell.dll
instance_id, dev_shell_dll_path = query_latest_vs_buildtools_instance()
pwsh_script = build_vs_dev_shell_pwsh_script_vswhere(
instance_id, dev_shell_dll_path
)
else:
# Mode B: Developer PowerShell .lnk
pwsh_script = build_vs_dev_shell_pwsh_script_from_shortcut()
env_diff_json = run_pwsh_and_capture_env_diff_json(pwsh_script)
bash_script = generate_bash_env_script(env_diff_json)
# Avoid printing an extra trailing newline
print(bash_script, end="")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment