aboutsummaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2023-02-08 16:12:56 +0100
committerStefan Boberg <[email protected]>2023-02-08 16:12:56 +0100
commit1f348612e825ff4c4d8a0af31a27902bdf20ef98 (patch)
tree19485578ffb645e981296191d06f9793ac336a41 /scripts
parent0.2.3-pre1 (diff)
downloadzen-1f348612e825ff4c4d8a0af31a27902bdf20ef98.tar.xz
zen-1f348612e825ff4c4d8a0af31a27902bdf20ef98.zip
removed some deprecated scripts
Diffstat (limited to 'scripts')
-rw-r--r--scripts/deploybuild.py124
-rw-r--r--scripts/generateprojects.bat1
-rw-r--r--scripts/installdeps.bat1
-rw-r--r--scripts/p4utils.py356
-rw-r--r--scripts/peafour.py187
-rw-r--r--scripts/testsetup.bat18
-rw-r--r--scripts/upload_syms.bat3
-rw-r--r--scripts/vswhere.py317
8 files changed, 0 insertions, 1007 deletions
diff --git a/scripts/deploybuild.py b/scripts/deploybuild.py
deleted file mode 100644
index 37518fd0c..000000000
--- a/scripts/deploybuild.py
+++ /dev/null
@@ -1,124 +0,0 @@
-import argparse
-import sys
-import os
-import fileinput
-import colorama
-import shutil
-import vswhere
-import subprocess
-from peafour import P4
-from colorama import Fore, Back, Style
-
-# Jazzy helpers
-
-def jazz_print(tag, detail = ""):
- print(f"{Fore.WHITE}{Style.BRIGHT}||> {tag}{Style.RESET_ALL} {detail}")
-
-def jazz_fail(tag, detail = ""):
- print(f"{Fore.RED}{Style.BRIGHT}||> {tag}{Style.RESET_ALL} {detail}")
-
-def copy_file(src, dst):
- print(f"{Fore.WHITE}{Style.BRIGHT}||> COPY {Style.RESET_ALL} {src} -> {Fore.GREEN}{Style.BRIGHT}{dst}")
- shutil.copy(src, dst)
-
-colorama.init()
-
-origcwd = os.getcwd()
-
-# Parse and validate arguments
-
-parser = argparse.ArgumentParser(description='Deploy a zen build to an UE tree')
-parser.add_argument("root", help="Path to an UE5 root directory")
-parser.add_argument("--sentry", action="store_true", help="Whether to upload symobls to Sentry")
-args = parser.parse_args()
-
-engineroot = args.root
-upload_symbols = args.sentry
-
-if not os.path.isfile(os.path.join(engineroot, "RunUAT.bat")):
- print(f"{Fore.RED}Not a valid UE5 engine root directory: '{engineroot}'")
- print(Style.RESET_ALL)
- exit(1)
-
-# Establish root of zen tree
-
-zenroot = __file__
-
-while not os.path.exists(os.path.join(zenroot, "deploy_build.bat")):
- zenroot = os.path.dirname(zenroot)
-
-jazz_print("Zen root:", zenroot)
-# Build fresh binaries
-
-try:
- subprocess.run(["xmake.exe", "config", "-p", "windows", "-a", "x64", "-m", "release"], check=True)
- build_cmd = ["xmake.exe", "build", "--rebuild", "zenserver"]
- build_output_dir = r'build\windows\x64\release'
-
- subprocess.run(build_cmd, check=True)
-except:
- jazz_fail("Build failed!")
- exit(1)
-
-build_output_binary_path = os.path.join(zenroot, build_output_dir, "zenserver.exe")
-build_output_binary_pdb_path = os.path.join(zenroot, build_output_dir, "zenserver.pdb")
-
-# Upload symbols etc to Sentry
-if upload_symbols:
- jazz_print("Uploading symbols", "to Sentry")
- subprocess.run(["scripts\sentry-cli.exe", "upload-dif", "--org", "to", "--project", "zen-server", build_output_binary_path, build_output_binary_pdb_path])
-
-# Change into root directory to pick up Perforce environment
-
-jazz_print(f"Determining P4 environment", f"for directory '{engineroot}'")
-
-os.chdir(engineroot)
-
-p4info = P4.info().run()
-
-if p4info is None:
- jazz_fail("Unable to query P4 info", "do you have source control connectivity?")
- exit(1)
-
-if not os.path.samefile(p4info.clientRoot, engineroot):
- print(f"{Fore.RED}Could not find P4 client for UE5 engine root directory '{engineroot}'")
- print(Style.RESET_ALL)
- exit(1)
-
-# check out the binaries
-
-jazz_print("Reverting", "any previous unsubmitted deploy")
-
-try:
- P4.revert("Engine/Binaries/Win64/zenserver.*").run()
- P4.revert("Engine/Binaries/Win64/crashpad_handler.exe").run()
-except Exception as e:
- # it's not super important to report failure here, it's likely
- # due to the user not actually having the file checked out (yet)
- pass
-
-jazz_print("Checking out", "(at head) zenserver executables")
-
-try:
- P4.edit("Engine/Binaries/Win64/zenserver.*").run()
- P4.edit("Engine/Binaries/Win64/crashpad_handler.exe").run()
-except Exception as e:
- jazz_fail("edit failed", str(e))
- exit(1)
-
-target_bin_dir = os.path.join(engineroot, "Engine\\Binaries\\Win64")
-
-jazz_print("Placing zenserver", f"executables into tree at '{target_bin_dir}'")
-
-crashpadtarget = os.path.join(target_bin_dir, "crashpad_handler.exe")
-
-try:
- copy_file(build_output_binary_path, os.path.join(target_bin_dir, "zenserver.exe"))
- copy_file(build_output_binary_pdb_path, os.path.join(target_bin_dir, "zenserver.pdb"))
- copy_file(os.path.join(zenroot, r'vcpkg_installed\x64-windows-static\tools\sentry-native\crashpad_handler.exe'), crashpadtarget)
- P4.add(crashpadtarget).run()
-except Exception as e:
- print(f"Caught exception while copying: {e.args}")
- exit(1)
-
-jazz_print("SUCCESS", "binaries ready for check-in")
diff --git a/scripts/generateprojects.bat b/scripts/generateprojects.bat
deleted file mode 100644
index cc6732aaa..000000000
--- a/scripts/generateprojects.bat
+++ /dev/null
@@ -1 +0,0 @@
-xmake project -k vsxmake2022 -y
diff --git a/scripts/installdeps.bat b/scripts/installdeps.bat
deleted file mode 100644
index f8ec42608..000000000
--- a/scripts/installdeps.bat
+++ /dev/null
@@ -1 +0,0 @@
-vcpkg --x-manifest-root=%~dp0.. --overlay-ports=%~dp0..\vcpkg_overlay-ports --triplet=x64-windows-static install \ No newline at end of file
diff --git a/scripts/p4utils.py b/scripts/p4utils.py
deleted file mode 100644
index d2a9933e7..000000000
--- a/scripts/p4utils.py
+++ /dev/null
@@ -1,356 +0,0 @@
-import os
-import re
-import time
-import flow.cmd
-import threading
-from peafour import P4
-import subprocess as sp
-
-#-------------------------------------------------------------------------------
-p4_set_result = None
-def get_p4_set(prop_name):
- if ret := os.getenv(prop_name):
- return ret
-
- global p4_set_result
- if not p4_set_result:
- p4_set_result = {}
- try: proc = sp.Popen(("p4", "set", "-q"), stdout=sp.PIPE)
- except: return
-
- for line in iter(proc.stdout.readline, b""):
- try: key, value = line.split(b"=", 1)
- except ValueError: continue
- p4_set_result[key.decode()] = value.strip().decode()
-
- proc.wait()
- proc.stdout.close()
-
- return p4_set_result.get(prop_name, None)
-
-#-------------------------------------------------------------------------------
-def login():
- try:
- detail = P4.login(s=True).run()
- except P4.Error:
- raise EnvironmentError("No valid Perforce session found. Run 'p4 login' to authenticate.")
- return getattr(detail, "User", None)
-
-#-------------------------------------------------------------------------------
-def get_p4config_name():
- return os.path.basename(get_p4_set("P4CONFIG") or ".p4config.txt")
-
-#-------------------------------------------------------------------------------
-def has_p4config(start_dir):
- from pathlib import Path
- p4config = get_p4config_name()
- for dir in (Path(start_dir) / "x").parents:
- candidate = dir / p4config
- if os.path.isfile(candidate):
- return True, candidate
- return False, p4config
-
-#-------------------------------------------------------------------------------
-def create_p4config(p4config_path, client, username, port=None):
- with open(p4config_path, "wt") as out:
- print_args = { "sep" : "", "file" : out }
- print("P4CLIENT=", client, **print_args)
- print("P4USER=", username, **print_args)
- if port:
- print("P4PORT", port, **print_args)
-
-#-------------------------------------------------------------------------------
-def ensure_p4config(start_dir=None):
- start_dir = start_dir or os.getcwd()
- found, p4config_name = has_p4config(start_dir)
- if found:
- return p4config_name, False
-
- username = login()
-
- # Get the client for 'start_dir'
- client = get_client_from_dir(start_dir, username)
- if not client:
- return
- client, root_dir = client
-
- # Now we know where to locate a p4config file
- p4config_path = f"{root_dir}/{p4config_name}"
- create_p4config(p4config_path, client, username)
- return p4config_path, True
-
-#-------------------------------------------------------------------------------
-def get_client_from_dir(root_dir, username):
- import socket
- host_name = socket.gethostname().lower()
-
- root_dir = os.path.normpath(root_dir).replace("\\", "/").lower()
-
- clients = (x for x in P4.clients(u=username) if x.Host.lower() == host_name)
- for client in clients:
- if client.Root.replace("\\", "/").lower() in root_dir:
- client_host = client.Host.lower()
- if not client_host or client_host == host_name:
- return client.client, client.Root
-
-#-------------------------------------------------------------------------------
-def get_branch_root(depot_path):
- def fstat_paths():
- limit = 5 # ...two of which are always required
- query_path = "//"
- for piece in depot_path[2:].split("/")[:limit]:
- query_path += piece + "/"
- yield query_path + "GenerateProjectFiles.bat"
-
- print("Probing for well-known file:")
- for x in fstat_paths():
- print(" ", x)
-
- fstat = P4.fstat(fstat_paths(), T="depotFile")
- root_path = fstat.run(on_error=False)
- if root_path:
- return "/".join(root_path.depotFile.split("/")[:-1]) + "/"
-
- raise ValueError("Unable to establish branch root")
-
-
-
-#-------------------------------------------------------------------------------
-class TempBranchSpec(object):
- def __init__(self, use, username, from_path, to_path, ignore_streams=False):
- import hashlib
- id = hashlib.md5()
- id.update(from_path.encode())
- id.update(to_path.encode())
- id = id.hexdigest()[:6]
- self._name = f"{username}-ushell.{use}-{id}"
-
- # To map between streams we need to extract the internal branchspec that
- # Perforce builds. If from/to aren't related streams it will fail so we
- # fallback to a conventional trivial branchspec.
- try:
- if ignore_streams:
- raise P4.Error("")
-
- branch = P4.branch(self._name, o=True, S=from_path[:-1], P=to_path[:-1])
- result = branch.run()
- spec = result.as_dict()
- except P4.Error:
- spec = {
- "Branch" : self._name,
- "View0" : f"{from_path}... {to_path}...",
- }
-
- P4.branch(i=True).run(input_data=spec, on_error=False)
-
- def __del__(self):
- P4.branch(self._name, d=True).run()
-
- def __str__(self):
- return self._name
-
-
-
-#-------------------------------------------------------------------------------
-def _kb_string(value):
- return format(value // 1024, ",") + "KB"
-
-#-------------------------------------------------------------------------------
-class _SyncRota(object):
- class _Worker(object):
- def __init__(self, id):
- self.id = id
- self.work_items = []
- self.burden = 0
- self.done_size = 0
- self.done_items = 0
- self.error = False
-
- def __init__(self, changelist, worker_count):
- self._workers = [_SyncRota._Worker(x) for x in range(worker_count)]
- self.changelist = str(changelist)
-
- def add_work(self, item, rev, cost):
- worker = min(self._workers, key=lambda x: x.burden)
- worker.work_items.append((item, rev, cost))
- worker.burden += cost
-
- def sort(self):
- direction = 1
- for worker in self._workers:
- worker.work_items.sort(key=lambda x: x[2] * direction)
- direction *= -1
-
- def read_workers(self):
- yield from (x for x in self._workers if x.work_items)
-
-#-------------------------------------------------------------------------------
-class Syncer(object):
- def __init__(self):
- self._paths = []
- self._excluded_views = set()
-
- def _read_sync_specs(self, include_excluded=True):
- cl_suffix = "@" + self._rota.changelist
- cl_suffix = "#0" if cl_suffix == "@0" else cl_suffix
- for depot_path in self._paths:
- yield depot_path + cl_suffix
-
- if include_excluded:
- # Using "@0" results in slow queries it seems
- yield from (x + "#0" for x in self._excluded_views)
-
- def _is_excluded(self, path):
- return next((True for x in self._re_excludes if x.match(path)), False)
-
- def _build_exclude_re(self):
- re_excludes = []
- for view in self._excluded_views:
- view = view.replace("...", "@")
- view = view.replace(".", "\\.")
- view = view.replace("*", "[^/]*")
- view = view.replace("@", ".*")
- re_excludes.append(view)
-
- if re_excludes:
- # The expression isn't escaped so hopefully it's not complicated...
- try: re_excludes = [re.compile(x, re.IGNORECASE) for x in re_excludes]
- except: pass
-
- self._re_excludes = re_excludes
-
- def add_path(self, dir):
- self._paths.append(dir)
-
- def add_exclude(self, view):
- self._excluded_views.add(view)
-
- def schedule(self, changelist, worker_count=8):
- self._build_exclude_re()
-
- # P4.<cmd> uses p4's Python-marshalled output (the -G option). However the
- # "p4 sync -n" will report open files via a "info" message instead of a
- # structured "stat" one. So we explicitly add open files to the rota.
- def read_items():
- yield from P4.sync(self._read_sync_specs(), n=True).read(on_error=False)
- yield from P4.opened().read(on_error=False)
-
- self._rota = _SyncRota(changelist, worker_count)
-
- # Fill the rota
- total_size = 0
- count = 0
- for item in read_items():
- depot_path = item.depotFile
- rev = int(item.rev)
-
- if self._is_excluded(depot_path):
- if item.action != "deleted":
- continue
- rev = 0
-
- if count % 17 == 0:
- print("\r" + str(count), "files", f"({_kb_string(total_size)})", end="")
-
- size = int(getattr(item, "fileSize", 0)) # deletes have no size attr
- self._rota.add_work(depot_path, rev, size)
-
- total_size += size
- count += 1
- self._rota.sort()
- print("\r" + str(count), "files", f"({_kb_string(total_size)})")
-
- def sync(self, *, dryrun=False, echo=False):
- # Sum up what we have to do
- total_burden = sum(x.burden for x in self._rota.read_workers())
- total_items = sum(len(x.work_items) for x in self._rota.read_workers())
-
- print(f"Fetching {_kb_string(total_burden)} in {total_items} files")
-
- # Launch the worker threads
- def sync_thread(worker):
- def on_error(p4_error):
- if "not enough space" in p4_error.data:
- worker.error = "Out of disk space"
- raise EOFError()
-
- try:
- # Annecdotally path@cl appears to be the quickest. path#rev is
- # appeared 15% slower, and with -L it was 30%.
- def read_sync_items():
- cl_prefix = "@" + self._rota.changelist
- for path, rev, size in worker.work_items:
- yield path + (cl_prefix if rev else "#0")
-
- sync = P4(b=8192).sync(read_sync_items(), n=dryrun)
- for item in sync.read(on_error=on_error):
- if echo:
- print(item.depotFile)
- worker.done_size += int(getattr(item, "fileSize", 0)) + 0.01
- except EOFError:
- pass
-
- def create_thread(worker):
- thread = threading.Thread(target=sync_thread, args=(worker,))
- thread.start()
- return thread
-
- threads = [create_thread(x) for x in self._rota.read_workers()]
- print(f"Using {len(threads)} workers")
-
- # While there are active threads, print detail about their progress
- total_burden += (0.01 * total_items)
- while not echo:
- threads = [x for x in threads if x.is_alive()]
- if not threads:
- break
-
- done_size = sum(x.done_size for x in self._rota.read_workers())
- progress = ((done_size * 1000) // total_burden) / 10
- print("\r%5.1f%%" % progress, _kb_string(int(done_size)), end="");
- time.sleep(0.3)
- else:
- for thread in threads:
- thread.join()
- print("\r...done ")
-
- # Check for errors from the workers
- for worker in (x for x in self._rota.read_workers() if x.error):
- print(flow.cmd.text.red("!!" + str(worker.error)))
- return False
-
- # Nothing more to do if this is a dry run as the remaining tasks need a
- # sync to operate on.
- if dryrun:
- return True
-
- # P4.sync() returns 'stat' type events but "p4 sync" will report files
- # with a complex sync scenario only as unstructured 'info' messages. As the
- # above won't know about these files we'll do a second sync to catch them.
- global sync_errors
- sync_errors = False
- print("Finalising ", end="")
- def read_depot_files():
- def on_error(data):
- msg = data.data.strip()
- if "up-to-date" in msg: return
- if "not in client view" in msg: return
-
- print("\n", flow.cmd.text.red(msg), end="")
- global sync_errors
- sync_errors = True
-
- def on_info(data):
- print("\n", flow.cmd.text.light_yellow(data.data), end="")
-
- sync = P4.sync(self._read_sync_specs(False), n=True)
- for item in sync.read(on_error=on_error, on_info=on_info):
- if not self._is_excluded(item.depotFile):
- yield item.depotFile
-
- sync = P4.sync(read_depot_files(), q=True)
- for i in sync.read(on_error=False):
- pass
- print()
-
- return not sync_errors
diff --git a/scripts/peafour.py b/scripts/peafour.py
deleted file mode 100644
index c9f559a21..000000000
--- a/scripts/peafour.py
+++ /dev/null
@@ -1,187 +0,0 @@
-import types
-import marshal
-import threading
-import subprocess as sp
-
-#-------------------------------------------------------------------------------
-class _P4Result(object):
- def __init__(self, result):
- super().__setattr__("_result", result)
-
- def __str__(self): return str(self._result)
- def __contains__(self, key): return (key in self._result) or (key + "0" in self._result)
- def as_dict(self): return {k.decode():v.decode() for k,v in self._result.items()}
- def __setattr__(self, key, value): self._result[key.encode()] = str(value).encode()
-
- def __getattr__(self, key):
- if (key + "0").encode() in self._result:
- def as_list():
- index = 0;
- while (key + str(index)).encode() in self._result:
- indexed_key = (key + str(index)).encode()
- yield self._result[indexed_key].decode()
- index += 1
- return as_list()
-
- ret = self._result.get(str.encode(key))
- if ret == None: raise AttributeError(key)
- return ret.decode(errors="replace")
-
-#-------------------------------------------------------------------------------
-class _P4Command(object):
- @staticmethod
- def _read_args(*args, **kwargs):
- for k,v in kwargs.items():
- if isinstance(v, bool):
- if v:
- yield "-" + k
- elif v != None:
- yield f"-{k}={v}" if len(k) > 1 else f"-{k}{v}"
-
- for arg in (x for x in args if isinstance(x, str)):
- yield arg
-
- def __init__(self, **options):
- opt_iter = _P4Command._read_args(**options)
- self._command = ["p4", "-Qutf8", "-G"]
- self._command += (x for x in opt_iter)
-
- def start(self, command, *args, **kwargs):
- self._stdin_args = []
- for arg in (x for x in args if not isinstance(x, str)):
- if not hasattr(arg, "__iter__"):
- raise TypeError("P4 arguments can be only strings or sequences")
- self._stdin_args.append(arg)
-
- self._proc = None
-
- arg_iter = _P4Command._read_args(*args, **kwargs)
-
- if self._stdin_args:
- self._command.append("-x-")
- self._command.append(command)
- self._command += (x for x in arg_iter)
-
- def __del__(self): self._close_proc()
- def __str__(self): return " ".join(self._command)
- def __iter__(self): yield from self._iter()
- def __getattr__(self, name): return getattr(self.run(), name)
-
- def run(self, **kwargs):
- return next(self._iter(**kwargs), None)
-
- def read(self, **kwargs):
- yield from self._iter(**kwargs)
-
- def _close_proc(self):
- if self._proc:
- if self._proc.stdin:
- self._proc.stdin.close()
- self._proc.stdout.close()
- self._proc = None
-
- def _iter(self, input_data=None, on_error=True, on_info=None, on_text=None):
- stdin = None
- if input_data != None:
- if self._stdin_args:
- raise _P4.Error("It is unsupported to have both generator-type arguments and input data")
-
- if isinstance(input_data, dict):
- input_data = {str(k).encode():str(v).encode() for k,v in input_data.items()}
- else:
- raise _P4.Error("Unsupported input data type; " + type(input_data).__name__)
- stdin = sp.PIPE
-
- if self._stdin_args:
- stdin = sp.PIPE
-
- proc = sp.Popen(self._command, stdout=sp.PIPE, stdin=stdin)
- self._proc = proc
-
- if stdin:
- def stdin_thread_entry():
- try:
- if input_data:
- marshal.dump(input_data, proc.stdin, 0)
- for args in self._stdin_args:
- for arg in args:
- arg = str(arg).encode() + b"\n"
- proc.stdin.write(arg)
- except (BrokenPipeError, OSError):
- pass
- finally:
- try: proc.stdin.close()
- except: pass
-
- stdin_thread = threading.Thread(target=stdin_thread_entry)
- stdin_thread.start()
-
- while True:
- try: result = marshal.load(proc.stdout)
- except EOFError: break
-
- code = result[b"code"]
- del result[b"code"]
-
- if code == b"error":
- if isinstance(on_error, bool):
- if on_error:
- raise _P4.Error(result[b"data"].decode()[:-1])
- continue
-
- try:
- on_error(_P4Result(result))
- except:
- proc.terminate()
- raise
- continue
-
- if code == b"stat":
- yield _P4Result(result)
- continue
-
- if code == b"text" and on_text:
- try:
- data = result.get(b"data", b"")
- on_text(data)
- continue
- except:
- proc.terminate()
- raise
-
- if code == b"info" and on_info:
- try:
- on_info(_P4Result(result))
- continue
- except:
- proc.terminate()
- raise
-
- if stdin:
- stdin_thread.join()
- self._close_proc()
-
-#-------------------------------------------------------------------------------
-class _P4(object):
- class Error(Exception):
- def __init__(self, msg):
- super().__init__("Perforce: " + msg)
-
- def __init__(self, **options):
- self._options = options
-
- def __getattr__(self, command):
- if command == "Error":
- return _P4.Error
-
- def inner(*args, **kwargs):
- instance = _P4Command(**self._options)
- instance.start(command, *args, **kwargs)
- return instance
-
- return inner
-
- def __call__(self, **options):
- return _P4(**options)
-
-P4 = _P4()
diff --git a/scripts/testsetup.bat b/scripts/testsetup.bat
deleted file mode 100644
index 3adabca29..000000000
--- a/scripts/testsetup.bat
+++ /dev/null
@@ -1,18 +0,0 @@
-@echo off
-
-rem This script can be used to add the build output folder (Release) to the path for easier
-rem ad hoc testing from the command line
-
-CALL :NORMALIZEPATH "%~dp0..\x64\Release"
-
-SET _BINPATH=%RETVAL%
-
-path|find /i "%_BINPATH%" >nul || set PATH=%PATH%;%_BINPATH%
-
-SET _BINPATH=
-
-EXIT /B
-
-:NORMALIZEPATH
- SET RETVAL=%~f1
- EXIT /B
diff --git a/scripts/upload_syms.bat b/scripts/upload_syms.bat
deleted file mode 100644
index 663d64e06..000000000
--- a/scripts/upload_syms.bat
+++ /dev/null
@@ -1,3 +0,0 @@
-rem This is a temporary hack until everyone has access to Sentry
-
-scripts\sentry-cli upload-dif --org to --project zen-server \ue5-main\Engine\Binaries\Win64\zenserver.exe \ue5-main\engine\Binaries\Win64\zenserver.pdb
diff --git a/scripts/vswhere.py b/scripts/vswhere.py
deleted file mode 100644
index 82b0723f9..000000000
--- a/scripts/vswhere.py
+++ /dev/null
@@ -1,317 +0,0 @@
-r"""
-Interface to Microsoft's Visual Studio locator tool, vswhere.
-
-If Visual Studio 15.2 or later has been installed, this will use the vswhere
-binary installed with Visual Studio. Otherwise, it will download the latest
-release of vswhere from https://github.com/Microsoft/vswhere the first time a
-function is called.
-"""
-
-import json
-import os
-import shutil
-import subprocess
-
-__version__ = '1.3.0'
-__author__ = 'Joel Spadin'
-__license__ = 'MIT'
-
-LATEST_RELEASE_ENDPOINT = 'https://api.github.com/repos/Microsoft/vswhere/releases/latest'
-DOWNLOAD_PATH = os.path.join(os.path.dirname(__file__), 'vswhere.exe')
-
-if 'ProgramFiles(x86)' in os.environ:
- DEFAULT_PATH = os.path.join(os.environ['ProgramFiles(x86)'], 'Microsoft Visual Studio', 'Installer', 'vswhere.exe')
-else:
- DEFAULT_PATH = None
-
-alternate_path = None
-download_mirror_url = None
-
-
-def execute(args):
- """
- Call vswhere with the given arguments and return an array of results.
-
- `args` is a list of command line arguments to pass to vswhere.
-
- If the argument list contains '-property', this returns an array with the
- property value for each result. Otherwise, this returns an array of
- dictionaries containing the results.
- """
- is_property = '-property' in args
-
- args = [get_vswhere_path(), '-utf8'] + args
-
- if not is_property:
- args.extend(['-format', 'json'])
-
- output = subprocess.check_output(args).decode('utf-8')
-
- if is_property:
- return output.splitlines()
- else:
- return json.loads(output)
-
-
-def find(
- find=None,
- find_all=False,
- latest=False,
- legacy=False,
- path=None,
- prerelease=False,
- products=None,
- prop=None,
- requires=None,
- requires_any=False,
- sort=False,
- version=None,
-):
- """
- Call vswhere and return an array of the results.
-
- Selection Options:
- find_all: If True, finds all instances even if they are incomplete and
- may not launch.
- prerelease: If True, also searches prereleases. By default, only
- releases are searched.
- products: a product ID or list of one or more product IDs to find.
- Defaults to Community, Professional, and Enterprise if not specified.
- Specify '*' by itself to search all product instances installed.
- See https://aka.ms/vs/workloads for a list of product IDs.
- requires: a workload component ID or list of one or more IDs required
- when finding instances. All specified IDs must be installed unless
- `requires_any` is True. See https://aka.ms/vs/workloads for a list
- of workload and component IDs.
- requires_any: If True, find instances with any one or more workload or
- component IDs passed to `requires`.
- version: A version range for instances to find. Example: '[15.0,16.0)'
- will find versions 15.*.
- latest: If True, returns only the newest version and last installed.
- legacy: If True, also searches Visual Studio 2015 and older products.
- Information is limited. This option cannot be used with either
- `products` or `requires`.
- path: Gets the instance for the given file path. Not compatible with any
- other selection option.
-
- Output Options:
- sort: If True, sorts the instances from newest version and last installed
- to oldest. When used with `find`, first instances are sorted, then
- files are sorted lexigraphically.
- prop: The name of a property to return instead of the full installation
- details. Use delimiters '.', '/', or '_' to separate object and
- property names. Example: 'properties.nickname' will return the
- 'nickname' property under 'properties'.
- find: Returns the file paths matching this glob pattern under the
- installation path. The following patterns are supported:
- ? Matches any one character except "\\"
- * Matches zero or more characters except "\\"
- ** Searches the current directory and subdirectories for the
- remaining search pattern.
- """
- args = []
-
- if find:
- args.append('-find')
- args.append(find)
-
- if find_all:
- args.append('-all')
-
- if latest:
- args.append('-latest')
-
- if legacy:
- args.append('-legacy')
-
- if path:
- args.append('-path')
- args.append(path)
-
- if prerelease:
- args.append('-prerelease')
-
- if products:
- args.append('-products')
- _extend_or_append(args, products)
-
- if prop:
- args.append('-property')
- args.append(prop)
-
- if requires:
- args.append('-requires')
- _extend_or_append(args, requires)
-
- if requires_any:
- args.append('-requiresAny')
-
- if sort:
- args.append('-sort')
-
- if version:
- args.append('-version')
- args.append(version)
-
- return execute(args)
-
-
-def find_first(**kwargs):
- """
- Call vswhere and returns only the first result, or None if there are no results.
-
- See find() for keyword arguments.
- """
- return next(iter(find(**kwargs)), None)
-
-
-def get_latest(legacy=None, **kwargs):
- """
- Get the information for the latest installed version of Visual Studio.
-
- Also supports the same selection options as find(), for example to select
- different products. If the `legacy` argument is not set, it defaults to
- `True` unless either `products` or `requires` arguments are set.
- """
- legacy = _get_legacy_arg(legacy, **kwargs)
- return find_first(latest=True, legacy=legacy, **kwargs)
-
-
-def get_latest_path(legacy=None, **kwargs):
- """
- Get the file path to the latest installed version of Visual Studio.
-
- Returns None if no installations could be found.
-
- Also supports the same selection options as find(), for example to select
- different products. If the `legacy` argument is not set, it defaults to
- `True` unless either `products` or `requires` arguments are set.
- """
- legacy = _get_legacy_arg(legacy, **kwargs)
- return find_first(latest=True, legacy=legacy, prop='installationPath', **kwargs)
-
-
-def get_latest_version(legacy=None, **kwargs):
- """
- Get the version string of the latest installed version of Visual Studio.
-
- For Visual Studio 2017 and newer, this is the full version number, for
- example: '15.8.28010.2003'.
-
- For Visual Studio 2015 and older, this only contains the major version, with
- the minor version set to 0, for example: '14.0'.
-
- Returns None if no installations could be found.
-
- Also supports the same selection options as find(), for example to select
- different products. If the `legacy` argument is not set, it defaults to
- `True` unless either `products` or `requires` arguments are set.
- """
- legacy = _get_legacy_arg(legacy, **kwargs)
- return find_first(latest=True, legacy=legacy, prop='installationVersion', **kwargs)
-
-def get_latest_major_version(**kwargs):
- """
- Get the major version of the latest installed version of Visual Studio as an int.
-
- Returns 0 if no installations could be found.
-
- Also supports the same selection options as find(), for example to select
- different products. If the `legacy` argument is not set, it defaults to
- `True` unless either `products` or `requires` arguments are set.
- """
- return int(next(iter(get_latest_version(**kwargs).split('.')), '0'))
-
-
-def get_vswhere_path():
- """
- Get the path to vshwere.exe.
-
- If vswhere is not already installed as part of Visual Studio, and no
- alternate path is given using `set_vswhere_path()`, the latest release will
- be downloaded and stored alongside this script.
- """
- if alternate_path and os.path.exists(alternate_path):
- return alternate_path
-
- if DEFAULT_PATH and os.path.exists(DEFAULT_PATH):
- return DEFAULT_PATH
-
- if os.path.exists(DOWNLOAD_PATH):
- return DOWNLOAD_PATH
-
- _download_vswhere()
- return DOWNLOAD_PATH
-
-
-def set_vswhere_path(path):
- """
- Set the path to vswhere.exe.
-
- If this is set, it overrides any version installed as part of Visual Studio.
- """
- global alternate_path
- alternate_path = path
-
-
-def set_download_mirror(url):
- """
- Set a URL from which vswhere.exe should be downloaded if it is not already
- installed as part of Visual Studio and no alternate path is given using
- `set_vswhere_path()`.
- """
- global download_mirror_url
- download_mirror_url = url
-
-
-def _extend_or_append(lst, value):
- if isinstance(value, str):
- lst.append(value)
- else:
- lst.extend(value)
-
-
-def _get_legacy_arg(legacy, **kwargs):
- if legacy is None:
- return 'products' not in kwargs and 'requires' not in kwargs
- else:
- return legacy
-
-
-def _download_vswhere():
- """
- Download vswhere to DOWNLOAD_PATH.
- """
- print('downloading from', _get_latest_release_url())
- try:
- from urllib.request import urlopen
- with urlopen(_get_latest_release_url()) as response, open(DOWNLOAD_PATH, 'wb') as outfile:
- shutil.copyfileobj(response, outfile)
- except ImportError:
- # Python 2
- import urllib
- urllib.urlretrieve(_get_latest_release_url(), DOWNLOAD_PATH)
-
-
-def _get_latest_release_url():
- """
- The the URL of the latest release of vswhere.
- """
- if download_mirror_url:
- return download_mirror_url
-
- try:
- from urllib.request import urlopen
- with urlopen(LATEST_RELEASE_ENDPOINT) as response:
- release = json.loads(response.read(), encoding=response.headers.get_content_charset() or 'utf-8')
- except ImportError:
- # Python 2
- import urllib2
- response = urllib2.urlopen(LATEST_RELEASE_ENDPOINT)
- release = json.loads(response.read(), encoding=response.headers.getparam('charset') or 'utf-8')
-
- for asset in release['assets']:
- if asset['name'] == 'vswhere.exe':
- return asset['browser_download_url']
-
- raise Exception('Could not locate the latest release of vswhere.') \ No newline at end of file