tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 2f641eb641efe5100c9ee2e1bfa4f6b54ce76e46
parent 857251224ef496662756b7e69de72647fe946b4a
Author: Ryan VanderMeulen <rvandermeulen@mozilla.com>
Date:   Thu, 13 Nov 2025 13:14:14 +0000

Bug 1999680 - Update vendored vsdownload.py. r=firefox-build-system-reviewers,sergesanspaille,glandium

Differential Revision: https://phabricator.services.mozilla.com/D272247

Diffstat:
Mbuild/vs/generate_yaml.py | 2+-
Mthird_party/python/vsdownload/moz.yaml | 4++--
Mthird_party/python/vsdownload/vsdownload.py | 497+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
3 files changed, 400 insertions(+), 103 deletions(-)

diff --git a/build/vs/generate_yaml.py b/build/vs/generate_yaml.py @@ -25,7 +25,7 @@ if __name__ == "__main__": args = parser.parse_args() lowercaseIgnores(args) - packages = getPackages(getManifest(args)) + packages = getPackages(getManifest(args), "x64") setPackageSelection(args, packages) selected = getSelectedPackages(packages, args) reduced = [] diff --git a/third_party/python/vsdownload/moz.yaml b/third_party/python/vsdownload/moz.yaml @@ -20,11 +20,11 @@ origin: # Human-readable identifier for this version/release # Generally "version NNN", "tag SSS", "bookmark SSS" - release: 18102a0b3e701b43169294521d1b4dbf5b1845d4 (2023-04-13T20:31:12Z). + release: 32b504c63b869681cda6824a20e30b74cb718432 (2025-11-11T17:54:21Z). # Revision to pull in # Must be a long or short commit SHA (long preferred) - revision: 18102a0b3e701b43169294521d1b4dbf5b1845d4 + revision: 32b504c63b869681cda6824a20e30b74cb718432 # The package's license, where possible using the mnemonic from # https://spdx.org/licenses/ diff --git a/third_party/python/vsdownload/vsdownload.py b/third_party/python/vsdownload/vsdownload.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3 +#!/usr/bin/env python3 # # Copyright (c) 2019 Martin Storsjo # @@ -16,27 +16,54 @@ import argparse import functools +import glob import hashlib import os import multiprocessing.pool -try: - import simplejson -except ModuleNotFoundError: - import json as simplejson -import six +import json +import platform +import re import shutil import socket import subprocess import sys import tempfile +import urllib.request +import xml.etree.ElementTree as ET import zipfile def getArgsParser(): + class OptionalBoolean(argparse.Action): + def __init__(self, + option_strings, + dest, + default=None, + help=None): + + if default is not None: + default_string = "yes" if default else "no" + if help is None: + help = "default: " + default_string + else: + help += " (default: %s)" % default_string + + super().__init__( + option_strings=option_strings, + dest=dest, + nargs='?', + default=default, + choices=["yes", "no"], + help=help, + metavar="yes|no") + + def __call__(self, parser, namespace, values, option_string=None): + setattr(namespace, self.dest, values != "no") + parser = argparse.ArgumentParser(description = "Download and install Visual Studio") parser.add_argument("--manifest", metavar="manifest", help="A predownloaded manifest file") parser.add_argument("--save-manifest", const=True, action="store_const", help="Store the downloaded manifest to a file") - parser.add_argument("--major", default=17, metavar="version", help="The major version to download (defaults to 17)") - parser.add_argument("--preview", dest="type", default="release", const="pre", action="store_const", help="Download the preview version instead of the release version") + parser.add_argument("--major", default=17, type=int, metavar="version", help="The major version to download (defaults to 17)") + parser.add_argument("--preview", const=True, action="store_const", help="Download the preview version instead of the release version") parser.add_argument("--cache", metavar="dir", help="Directory to use as a persistent cache for downloaded files") parser.add_argument("--dest", metavar="dir", help="Directory to install into") parser.add_argument("package", metavar="package", help="Package to install. If omitted, installs the default command line tools.", nargs="*") @@ -56,11 +83,28 @@ def getArgsParser(): parser.add_argument("--keep-unpack", const=True, action="store_const", help="Keep the unpacked files that aren't otherwise selected as needed output") parser.add_argument("--msvc-version", metavar="version", help="Install a specific MSVC toolchain version") parser.add_argument("--sdk-version", metavar="version", help="Install a specific Windows SDK version") + parser.add_argument("--architecture", metavar="arch", choices=["host", "x86", "x64", "arm", "arm64"], help="Target architectures to include (defaults to all)", nargs="+") + parser.add_argument("--with-wdk-installers", metavar="dir", help="Install Windows Driver Kit using the provided MSI installers") + parser.add_argument("--host-arch", metavar="arch", choices=["x86", "x64", "arm64"], help="Specify the host architecture of packages to install") + parser.add_argument("--only-host", default=True, action=OptionalBoolean, help="Only download packages that match host arch") + parser.add_argument("--skip-patch", action="store_true", help="Don't patch downloaded packages") return parser def setPackageSelectionMSVC16(args, packages, userversion, sdk, toolversion, defaultPackages): - if findPackage(packages, "Microsoft.VisualStudio.Component.VC." + toolversion + ".x86.x64", None, warn=False): - args.package.extend(["Win10SDK_" + sdk, "Microsoft.VisualStudio.Component.VC." + toolversion + ".x86.x64", "Microsoft.VisualStudio.Component.VC." + toolversion + ".ARM", "Microsoft.VisualStudio.Component.VC." + toolversion + ".ARM64"]) + if findPackage(packages, "Microsoft.VisualStudio.Component.VC." + toolversion + ".x86.x64", warn=False): + if "x86" in args.architecture or "x64" in args.architecture: + args.package.append("Microsoft.VisualStudio.Component.VC." + toolversion + ".x86.x64") + args.package.append("Microsoft.VC." + toolversion + ".ASAN.X86") + args.package.append("Microsoft.VisualStudio.Component.VC." + toolversion + ".ATL") + if "arm" in args.architecture: + args.package.append("Microsoft.VisualStudio.Component.VC." + toolversion + ".ARM") + args.package.append("Microsoft.VisualStudio.Component.VC." + toolversion + ".ATL.ARM") + if "arm64" in args.architecture: + args.package.append("Microsoft.VisualStudio.Component.VC." + toolversion + ".ARM64") + args.package.append("Microsoft.VisualStudio.Component.VC." + toolversion + ".ATL.ARM64") + + if args.sdk_version == None: + args.sdk_version = sdk else: # Options for toolchains for specific versions. The latest version in # each manifest isn't available as a pinned version though, so if that @@ -69,7 +113,7 @@ def setPackageSelectionMSVC16(args, packages, userversion, sdk, toolversion, def args.package.extend(defaultPackages) def setPackageSelectionMSVC15(args, packages, userversion, sdk, toolversion, defaultPackages): - if findPackage(packages, "Microsoft.VisualStudio.Component.VC.Tools." + toolversion, None, warn=False): + if findPackage(packages, "Microsoft.VisualStudio.Component.VC.Tools." + toolversion, warn=False): args.package.extend(["Win10SDK_" + sdk, "Microsoft.VisualStudio.Component.VC.Tools." + toolversion]) else: # Options for toolchains for specific versions. The latest version in @@ -79,9 +123,22 @@ def setPackageSelectionMSVC15(args, packages, userversion, sdk, toolversion, def args.package.extend(defaultPackages) def setPackageSelection(args, packages): + if not args.architecture: + args.architecture = ["host", "x86", "x64", "arm", "arm64"] + if args.host_arch is not None and "host" in args.architecture: + args.architecture.append(args.host_arch) + # If no packages are selected, install these versionless packages, which # gives the latest/recommended version for the current manifest. - defaultPackages = ["Microsoft.VisualStudio.Workload.VCTools", "Microsoft.VisualStudio.Component.VC.Tools.ARM", "Microsoft.VisualStudio.Component.VC.Tools.ARM64"] + defaultPackages = ["Microsoft.VisualStudio.Workload.VCTools"] + if "x86" in args.architecture or "x64" in args.architecture: + defaultPackages.append("Microsoft.VisualStudio.Component.VC.ATL") + if "arm" in args.architecture: + defaultPackages.append("Microsoft.VisualStudio.Component.VC.Tools.ARM") + defaultPackages.append("Microsoft.VisualStudio.Component.VC.ATL.ARM") + if "arm64" in args.architecture: + defaultPackages.append("Microsoft.VisualStudio.Component.VC.Tools.ARM64") + defaultPackages.append("Microsoft.VisualStudio.Component.VC.ATL.ARM64") # Note, that in the manifest for MSVC version X.Y, only version X.Y-1 # exists with a package name like "Microsoft.VisualStudio.Component.VC." @@ -112,6 +169,36 @@ def setPackageSelection(args, packages): setPackageSelectionMSVC16(args, packages, args.msvc_version, "10.0.19041", "14.29.16.11", defaultPackages) elif args.msvc_version == "17.0": setPackageSelectionMSVC16(args, packages, args.msvc_version, "10.0.19041", "14.30.17.0", defaultPackages) + elif args.msvc_version == "17.1": + setPackageSelectionMSVC16(args, packages, args.msvc_version, "10.0.19041", "14.31.17.1", defaultPackages) + elif args.msvc_version == "17.2": + setPackageSelectionMSVC16(args, packages, args.msvc_version, "10.0.19041", "14.32.17.2", defaultPackages) + elif args.msvc_version == "17.3": + setPackageSelectionMSVC16(args, packages, args.msvc_version, "10.0.19041", "14.33.17.3", defaultPackages) + elif args.msvc_version == "17.4": + setPackageSelectionMSVC16(args, packages, args.msvc_version, "10.0.22621", "14.34.17.4", defaultPackages) + elif args.msvc_version == "17.5": + setPackageSelectionMSVC16(args, packages, args.msvc_version, "10.0.22621", "14.35.17.5", defaultPackages) + elif args.msvc_version == "17.6": + setPackageSelectionMSVC16(args, packages, args.msvc_version, "10.0.22621", "14.36.17.6", defaultPackages) + elif args.msvc_version == "17.7": + setPackageSelectionMSVC16(args, packages, args.msvc_version, "10.0.22621", "14.37.17.7", defaultPackages) + elif args.msvc_version == "17.8": + setPackageSelectionMSVC16(args, packages, args.msvc_version, "10.0.22621", "14.38.17.8", defaultPackages) + elif args.msvc_version == "17.9": + setPackageSelectionMSVC16(args, packages, args.msvc_version, "10.0.22621", "14.39.17.9", defaultPackages) + elif args.msvc_version == "17.10": + setPackageSelectionMSVC16(args, packages, args.msvc_version, "10.0.22621", "14.40.17.10", defaultPackages) + elif args.msvc_version == "17.11": + setPackageSelectionMSVC16(args, packages, args.msvc_version, "10.0.22621", "14.41.17.11", defaultPackages) + elif args.msvc_version == "17.12": + setPackageSelectionMSVC16(args, packages, args.msvc_version, "10.0.22621", "14.42.17.12", defaultPackages) + elif args.msvc_version == "17.13": + setPackageSelectionMSVC16(args, packages, args.msvc_version, "10.0.22621", "14.43.17.13", defaultPackages) + elif args.msvc_version == "17.14": + setPackageSelectionMSVC16(args, packages, args.msvc_version, "10.0.26100", "14.44.17.14", defaultPackages) + elif args.msvc_version == "18.0": + setPackageSelectionMSVC16(args, packages, args.msvc_version, "10.0.26100", "14.50.18.0", defaultPackages) elif args.msvc_version == "15.4": setPackageSelectionMSVC15(args, packages, args.msvc_version, "10.0.16299", "14.11", defaultPackages) @@ -133,15 +220,29 @@ def setPackageSelection(args, packages): args.package = defaultPackages if args.sdk_version != None: + found = False + versions = [] for key in packages: if key.startswith("win10sdk") or key.startswith("win11sdk"): base = key[0:8] + version = key[9:] + if re.match(r'\d+\.\d+\.\d+', version): + versions += [version] sdkname = base + "_" + args.sdk_version if key == sdkname: + found = True args.package.append(key) else: args.ignore.append(key) - p = packages[key][0] + if not found: + print("WinSDK version " + args.sdk_version + " not found") + print("Available versions:") + for v in sorted(versions): + print(" " + v) + sys.exit(1) + + if args.with_wdk_installers is not None: + args.package.append("Component.Microsoft.Windows.DriverKit.BuildTools") def lowercaseIgnores(args): ignore = [] @@ -152,9 +253,18 @@ def lowercaseIgnores(args): def getManifest(args): if args.manifest == None: - url = "https://aka.ms/vs/%s/%s/channel" % (args.major, args.type) + if args.major < 18: + type = "release" + else: + type = "stable" + if args.preview: + if args.major < 18: + type = "pre" + else: + type = "insiders" + url = "https://aka.ms/vs/%s/%s/channel" % (args.major, type) print("Fetching %s" % (url)) - manifest = simplejson.loads(six.moves.urllib.request.urlopen(url).read()) + manifest = json.loads(urllib.request.urlopen(url).read()) print("Got toplevel manifest for %s" % (manifest["info"]["productDisplayVersion"])) for item in manifest["channelItems"]: if "type" in item and item["type"] == "Manifest": @@ -166,8 +276,8 @@ def getManifest(args): if not args.manifest.startswith("http"): args.manifest = "file:" + args.manifest - manifestdata = six.moves.urllib.request.urlopen(args.manifest).read() - manifest = simplejson.loads(manifestdata) + manifestdata = urllib.request.urlopen(args.manifest).read() + manifest = json.loads(manifestdata) print("Loaded installer manifest for %s" % (manifest["info"]["productDisplayVersion"])) if args.save_manifest: @@ -186,14 +296,22 @@ def getManifest(args): return manifest -def prioritizePackage(a, b): - if "chip" in a and "chip" in b: - ax64 = a["chip"].lower() == "x64" - bx64 = b["chip"].lower() == "x64" - if ax64 and not bx64: +def prioritizePackage(arch, a, b): + def archOrd(k, x): + if arch is None: + return 0 + c = x.get(k, "neutral").lower() + if c == "neutral": + return 0 + if c == arch: return -1 - elif bx64 and not ax64: - return 1 + return 1 + + for k in ["chip", "machineArch", "productArch"]: + r = archOrd(k, a) - archOrd(k, b) + if r != 0: + return r + if "language" in a and "language" in b: aeng = a["language"].lower().startswith("en-") beng = b["language"].lower().startswith("en-") @@ -203,7 +321,7 @@ def prioritizePackage(a, b): return 1 return 0 -def getPackages(manifest): +def getPackages(manifest, arch): packages = {} for p in manifest["packages"]: id = p["id"].lower() @@ -211,7 +329,7 @@ def getPackages(manifest): packages[id] = [] packages[id].append(p) for key in packages: - packages[key] = sorted(packages[key], key=functools.cmp_to_key(prioritizePackage)) + packages[key] = sorted(packages[key], key=functools.cmp_to_key(functools.partial(prioritizePackage, arch))) return packages def listPackageType(packages, type): @@ -227,7 +345,7 @@ def listPackageType(packages, type): for id in sorted(ids): print(id) -def findPackage(packages, id, chip, warn=True): +def findPackage(packages, id, constraints={}, warn=True): origid = id id = id.lower() candidates = None @@ -236,18 +354,64 @@ def findPackage(packages, id, chip, warn=True): print("WARNING: %s not found" % (origid)) return None candidates = packages[id] - if chip != None: - chip = chip.lower() - for a in candidates: - if "chip" in a and a["chip"].lower() == chip: - return a + for a in candidates: + matched = True + for k, v in constraints.items(): + if k in ["chip", "machineArch"]: + matched = a.get(k, "").lower() == v.lower() + if not matched: + break + if matched: + return a return candidates[0] -def printDepends(packages, target, deptype, chip, indent, args): +def matchPackageHostArch(p, host): + if host is None: + return True + + known_archs = ["x86", "x64", "arm64"] + + # Some packages have host arch in their ids, e.g. + # - Microsoft.VisualCpp.Tools.HostARM64.TargetX64 + # - Microsoft.VisualCpp.Tools.HostX64.TargetX64 + id = p["id"].lower() + for a in known_archs: + if "host" + a in id: + return a == host + + for k in ["chip", "machineArch", "productArch"]: + a = p.get(k, "neutral").lower() + if a == "neutral": + continue + if a != host: + return False + + return True + +def matchPackageTargetArch(p, archs): + if archs is None: + return True + + known_archs = ["x86", "x64", "arm", "arm64"] + + # Some packages have target arch in their ids, e.g. + # - Microsoft.VisualCpp.Tools.HostARM64.TargetX64 + # - Microsoft.VisualCpp.Tools.HostX64.TargetX64 + id = p["id"].lower() + for a in known_archs: + if re.search(fr"\.target{a}(\W|$)", id): + return a in archs + + return True + +def printDepends(packages, target, constraints, indent, args): chipstr = "" - if chip != None: - chipstr = " (" + chip + ")" + for k in ["chip", "machineArch"]: + v = constraints.get(k) + if v is not None: + chipstr = chipstr + " (" + k + "." + v + ")" deptypestr = "" + deptype = constraints.get("type", "") if deptype != "": deptypestr = " (" + deptype + ")" ignorestr = "" @@ -255,27 +419,29 @@ def printDepends(packages, target, deptype, chip, indent, args): if target.lower() in args.ignore: ignorestr = " (Ignored)" ignore = True - print(indent + target + chipstr + deptypestr + ignorestr) if deptype == "Optional" and not args.include_optional: - return + ignore = True if deptype == "Recommended" and args.skip_recommended: - return + ignore = True + if not ignore: + p = findPackage(packages, target, constraints, warn=False) + if p == None: + ignorestr = " (NotFound)" + ignore = True + elif args.only_host and not matchPackageHostArch(p, args.host_arch): + ignorestr = " (HostArchMismatch)" + ignore = True + elif not matchPackageTargetArch(p, args.architecture): + ignorestr = " (TargetArchMismatch)" + ignore = True + print(indent + target + chipstr + deptypestr + ignorestr) if ignore: return - p = findPackage(packages, target, chip) - if p == None: - return - if "dependencies" in p: - deps = p["dependencies"] - for key in deps: - dep = deps[key] - type = "" - if "type" in dep: - type = dep["type"] - chip = None - if "chip" in dep: - chip = dep["chip"] - printDepends(packages, key, type, chip, indent + " ", args) + for target, constraints in p.get("dependencies", {}).items(): + if not isinstance(constraints, dict): + constraints = { "version": constraints } + target = constraints.get("id", target) + printDepends(packages, target, constraints, indent + " ", args) def printReverseDepends(packages, target, deptype, indent, args): deptypestr = "" @@ -304,42 +470,44 @@ def getPackageKey(p): packagekey = p["id"] if "version" in p: packagekey = packagekey + "-" + p["version"] - if "chip" in p: - packagekey = packagekey + "-" + p["chip"] + for k in ["chip", "machineArch", "productArch"]: + v = p.get(k) + if v is not None: + packagekey = packagekey + "-" + k + "." + v return packagekey -def aggregateDepends(packages, included, target, chip, args): +def aggregateDepends(packages, included, target, constraints, args): if target.lower() in args.ignore: return [] - p = findPackage(packages, target, chip) + p = findPackage(packages, target, constraints) if p == None: return [] + if args.only_host and not matchPackageHostArch(p, args.host_arch): + return [] + if not matchPackageTargetArch(p, args.architecture): + return [] packagekey = getPackageKey(p) if packagekey in included: return [] ret = [p] included[packagekey] = True - if "dependencies" in p: - deps = p["dependencies"] - for key in deps: - dep = deps[key] - if "type" in dep: - deptype = dep["type"] - if deptype == "Optional" and not args.include_optional: - continue - if deptype == "Recommended" and args.skip_recommended: - continue - chip = None - if "chip" in dep: - chip = dep["chip"] - ret.extend(aggregateDepends(packages, included, key, chip, args)) + for target, constraints in p.get("dependencies", {}).items(): + if not isinstance(constraints, dict): + constraints = { "version": constraints } + target = constraints.get("id", target) + deptype = constraints.get("type") + if deptype == "Optional" and not args.include_optional: + continue + if deptype == "Recommended" and args.skip_recommended: + continue + ret.extend(aggregateDepends(packages, included, target, constraints, args)) return ret def getSelectedPackages(packages, args): ret = [] included = {} for i in args.package: - ret.extend(aggregateDepends(packages, included, i, None, args)) + ret.extend(aggregateDepends(packages, included, i, {}, args)) return ret def sumInstalledSize(l): @@ -374,8 +542,10 @@ def printPackageList(l): s = p["id"] if "type" in p: s = s + " (" + p["type"] + ")" - if "chip" in p: - s = s + " (" + p["chip"] + ")" + for k in ["chip", "machineArch", "productArch"]: + v = p.get(k) + if v is not None: + s = s + " (" + k + "." + v + ")" if "language" in p: s = s + " (" + p["language"] + ")" s = s + " " + formatSize(sumInstalledSize([p])) @@ -429,29 +599,29 @@ def _downloadPayload(payload, destname, fileid, allowHashMismatch): if os.access(destname, os.F_OK): if "sha256" in payload: if sha256File(destname).lower() != payload["sha256"].lower(): - six.print_("Incorrect existing file %s, removing" % (fileid), flush=True) + print("Incorrect existing file %s, removing" % (fileid), flush=True) os.remove(destname) else: - six.print_("Using existing file %s" % (fileid), flush=True) + print("Using existing file %s" % (fileid), flush=True) return 0 else: return 0 size = 0 if "size" in payload: size = payload["size"] - six.print_("Downloading %s (%s)" % (fileid, formatSize(size)), flush=True) - six.moves.urllib.request.urlretrieve(payload["url"], destname) + print("Downloading %s (%s)" % (fileid, formatSize(size)), flush=True) + urllib.request.urlretrieve(payload["url"], destname) if "sha256" in payload: if sha256File(destname).lower() != payload["sha256"].lower(): if allowHashMismatch: - six.print_("WARNING: Incorrect hash for downloaded file %s" % (fileid), flush=True) + print("WARNING: Incorrect hash for downloaded file %s" % (fileid), flush=True) else: raise Exception("Incorrect hash for downloaded file %s, aborting" % fileid) return size except Exception as e: if attempt == attempts - 1: raise - six.print_("%s: %s" % (type(e).__name__, e), flush=True) + print("%s: %s" % (type(e).__name__, e), flush=True) def mergeTrees(src, dest): if not os.path.isdir(src): @@ -479,7 +649,7 @@ def mergeTrees(src, dest): def unzipFiltered(zip, dest): tmp = os.path.join(dest, "extract") for f in zip.infolist(): - name = six.moves.urllib.parse.unquote(f.filename) + name = urllib.parse.unquote(f.filename) if "/" in name: sep = name.rfind("/") dir = os.path.join(dest, name[0:sep]) @@ -491,7 +661,7 @@ def unzipFiltered(zip, dest): def unpackVsix(file, dest, listing): temp = os.path.join(dest, "vsix") makedirs(temp) - with zipfile.ZipFile(file, 'r') as zip: + with zipfile.ZipFile(file, "r") as zip: unzipFiltered(zip, temp) with open(listing, "w") as f: for n in zip.namelist(): @@ -499,6 +669,10 @@ def unpackVsix(file, dest, listing): contents = os.path.join(temp, "Contents") if os.access(contents, os.F_OK): mergeTrees(contents, dest) + # This archive directory structure is used in WDK.vsix. + msbuild = os.path.join(temp, "$MSBuild") + if os.access(msbuild, os.F_OK): + mergeTrees(msbuild, os.path.join(dest, "MSBuild")) shutil.rmtree(temp) def unpackWin10SDK(src, payloads, dest): @@ -506,20 +680,74 @@ def unpackWin10SDK(src, payloads, dest): # Note, this extracts some files into Program Files/..., and some # files directly in the root unpack directory. The files we need # are under Program Files/... though. + # On Windows, msiexec extracts files to the root unpack directory. + # To be consistent, symlink Program Files to root. + if sys.platform != "win32" and not os.access(os.path.join(dest, "Program Files"), os.F_OK): + os.symlink(".", os.path.join(dest, "Program Files"), target_is_directory=True) + for payload in payloads: name = getPayloadName(payload) if name.endswith(".msi"): print("Extracting " + name) srcfile = os.path.join(src, name) if sys.platform == "win32": - cmd = ["msiexec", "/a", srcfile, "/qn", "TARGETDIR=" + os.path.abspath(dest)] + # The path to TARGETDIR need to be quoted in the case of spaces. + cmd = "msiexec /a \"%s\" /qn TARGETDIR=\"%s\"" % (srcfile, os.path.abspath(dest)) else: cmd = ["msiextract", "-C", dest, srcfile] with open(os.path.join(dest, "WinSDK-" + getPayloadName(payload) + "-listing.txt"), "w") as log: subprocess.check_call(cmd, stdout=log) +def unpackWin10WDK(src, dest): + print("Unpacking WDK installers from", src) + + # WDK installers downloaded by wdksetup.exe include a huge pile of + # non-WDK installers, just skip these. + for srcfile in glob.glob(src + "/Windows Driver*.msi"): + name = os.path.basename(srcfile) + print("Extracting", name) + + # Do not try to run msiexec here because TARGETDIR + # does not work with WDK installers. + cmd = ["msiextract", "-C", dest, srcfile] + + payloadName, _ = os.path.splitext(name) + with open(os.path.join(dest, "WDK-" + payloadName + "-listing.txt"), "w") as log: + subprocess.check_call(cmd, stdout=log) + + # WDK includes a VS extension, unpack it before copying the extracted files. + for vsix in glob.glob(dest + "/**/WDK.vsix", recursive=True): + name = os.path.basename(vsix) + print("Unpacking WDK VS extension", name) + + payloadName, _ = os.path.splitext(name) + listing = os.path.join(dest, "WDK-VS-" + payloadName + "-listing.txt") + unpackVsix(vsix, dest, listing) + + # Merge incorrectly extracted 'Build' and 'build' directory trees. + # The WDK 'build' tree must be versioned. + kitsPath = os.path.join(dest, "Program Files", "Windows Kits", "10") + brokenBuildDir = os.path.join(kitsPath, "Build") + for buildDir in glob.glob(kitsPath + "/build/10.*/"): + wdkVersion = buildDir.split("/")[-2]; + print("Merging WDK 'Build' and 'build' directories into version", wdkVersion); + mergeTrees(brokenBuildDir, buildDir) + shutil.rmtree(brokenBuildDir) + + # Move the WDK .props files into a versioned directory. + propsPath = os.path.join(kitsPath, "DesignTime", "CommonConfiguration", "Neutral", "WDK"); + versionedPath = os.path.join(propsPath, wdkVersion) + makedirs(versionedPath) + for props in glob.glob(propsPath + "/*.props"): + filename = os.path.basename(props) + print("Moving", filename, "into version", wdkVersion); + shutil.move(props, os.path.join(versionedPath, filename)) + def extractPackages(selected, cache, dest): makedirs(dest) + # The path name casing is not consistent across packages, or even within a single package. + # Manually create top-level folders before extracting packages to ensure the desired casing. + makedirs(os.path.join(dest, "MSBuild")) for p in selected: type = p["type"] dir = os.path.join(cache, getPackageKey(p)) @@ -535,21 +763,60 @@ def extractPackages(selected, cache, dest): else: print("Skipping unpacking of " + p["id"] + " of type " + type) +def patchPackages(dest): + patches = os.path.join(os.path.dirname(os.path.abspath(__file__)), "patches") + if not os.path.isdir(patches): + return + for patch in glob.iglob(os.path.join(patches, "**"), recursive=True): + if os.path.isdir(patch): + continue + p = os.path.relpath(patch, patches) + f, op = os.path.splitext(p) + if op == ".patch": + if os.access(os.path.join(dest, f), os.F_OK): + # Check if the patch has already been applied by attempting a reverse application; skip if already applied. + if subprocess.call(["git", "--work-tree=.", "apply", "--quiet", "--reverse", "--check", patch], cwd=dest) != 0: + print("Patching " + f) + subprocess.check_call(["git", "--work-tree=.", "apply", patch], cwd=dest) + elif op == ".remove": + if os.access(os.path.join(dest, f), os.F_OK): + print("Removing " + f) + os.remove(os.path.join(dest, f)) + else: + print("Copying " + p) + os.makedirs(os.path.dirname(os.path.join(dest, p)), exist_ok=True) + shutil.copyfile(patch, os.path.join(dest, p)) + +def copyDependentAssemblies(app): + if not os.path.isfile(app + ".config"): + return + dest = os.path.dirname(app) + ns = "{urn:schemas-microsoft-com:asm.v1}" + configuration = ET.parse(app + ".config") + for codeBase in configuration.findall(f"./runtime/{ns}assemblyBinding/{ns}dependentAssembly/{ns}codeBase/[@href]"): + href = codeBase.attrib["href"].replace("\\", "/") + src = os.path.join(dest, href) + if os.path.isfile(src): + shutil.copy(src, dest) + def moveVCSDK(unpack, dest): - # Move the VC and Program Files\Windows Kits\10 directories - # out from the unpack directory, allowing the rest of unpacked - # files to be removed. - makedirs(os.path.join(dest, "kits")) - mergeTrees(os.path.join(unpack, "VC"), os.path.join(dest, "VC")) - kitsPath = unpack - # msiexec extracts to Windows Kits rather than Program Files\Windows Kits - if sys.platform != "win32": - kitsPath = os.path.join(kitsPath, "Program Files") - kitsPath = os.path.join(kitsPath, "Windows Kits", "10") - mergeTrees(kitsPath, os.path.join(dest, "kits", "10")) - # The DIA SDK isn't necessary for normal use, but can be used when e.g. - # compiling LLVM. - mergeTrees(os.path.join(unpack, "DIA SDK"), os.path.join(dest, "DIA SDK")) + # Move some components out from the unpack directory, + # allowing the rest of unpacked files to be removed. + components = [ + "VC", + "Windows Kits", + # The DIA SDK isn't necessary for normal use, but can be used when e.g. + # compiling LLVM. + "DIA SDK", + # MSBuild is the standard VC build tool. + "MSBuild", + # This directory contains batch scripts to setup Developer Command Prompt. + # Environment variable VS170COMNTOOLS points to this directory, and some + # tools use it to locate VS installation root and MSVC toolchains. + os.path.join("Common7", "Tools"), + ] + for dir in filter(None, components): + mergeTrees(os.path.join(unpack, dir), os.path.join(dest, dir)) if __name__ == "__main__": parser = getArgsParser() @@ -558,15 +825,34 @@ if __name__ == "__main__": socket.setdefaulttimeout(15) - packages = getPackages(getManifest(args)) + if args.host_arch is None: + args.host_arch = platform.machine().lower() + if platform.system() == "Darwin": + # There is no prebuilt arm64 Wine on macOS. + args.host_arch = "x64" + elif args.host_arch in ["x86", "i386", "i686"]: + args.host_arch = "x86" + elif args.host_arch in ["x64", "x86_64", "amd64"]: + args.host_arch = "x64" + elif args.host_arch in ["arm64", "aarch64"]: + args.host_arch = "arm64" + else: + args.host_arch = None + + if args.host_arch is None: + print("WARNING: Unable to detect host architecture") + else: + print("Install packages for %s host architecture" % args.host_arch) + + packages = getPackages(getManifest(args), args.host_arch) if args.print_version: sys.exit(0) if not args.accept_license: - response = six.moves.input("Do you accept the license at " + findPackage(packages, "Microsoft.VisualStudio.Product.BuildTools", None)["localizedResources"][0]["license"] + " (yes/no)? ") + response = input("Do you accept the license at " + findPackage(packages, "Microsoft.VisualStudio.Product.BuildTools")["localizedResources"][0]["license"] + " (yes/no)? ") while response != "yes" and response != "no": - response = six.moves.input("Do you accept the license? Answer \"yes\" or \"no\": ") + response = input("Do you accept the license? Answer \"yes\" or \"no\": ") if response == "no": sys.exit(0) @@ -583,7 +869,7 @@ if __name__ == "__main__": if args.print_deps_tree: for i in args.package: - printDepends(packages, i, "", None, "", args) + printDepends(packages, i, {}, "", args) sys.exit(0) if args.print_reverse_deps: @@ -626,10 +912,21 @@ if __name__ == "__main__": extractPackages(selected, cache, unpack) + if args.with_wdk_installers is not None: + unpackWin10WDK(args.with_wdk_installers, unpack) + + if sys.platform != "win32": + # Wine doesn't support dependentAssembly yet. + # Manually copy dependencies to app directory. + copyDependentAssemblies(os.path.join(unpack, "MSBuild", "Current", "Bin", "amd64", "MSBuild.exe")) + copyDependentAssemblies(os.path.join(unpack, "MSBuild", "Current", "Bin", "arm64", "MSBuild.exe")) + if not args.only_unpack: moveVCSDK(unpack, dest) if not args.keep_unpack: shutil.rmtree(unpack) + if not args.skip_patch and args.major == 17: # Only apply patches to latest VS + patchPackages(dest) finally: if tempcache != None: shutil.rmtree(tempcache)