#!/usr/bin/env python import argparse import functools import logging import os from pathlib import Path import re import shutil import subprocess import tempfile import textwrap import urllib.request import zipfile # Update both variables when updating the GDK GIT_REF = "June_2024_Update_1" GDK_EDITION = "240601" # YYMMUU logger = logging.getLogger(__name__) class GdDesktopConfigurator: def __init__(self, gdk_path, arch, vs_folder, vs_version=None, vs_toolset=None, temp_folder=None, git_ref=None, gdk_edition=None): self.git_ref = git_ref or GIT_REF self.gdk_edition = gdk_edition or GDK_EDITION self.gdk_path = gdk_path self.temp_folder = temp_folder or Path(tempfile.gettempdir()) self.dl_archive_path = Path(self.temp_folder) / f"{ self.git_ref }.zip" self.gdk_extract_path = Path(self.temp_folder) / f"GDK-{ self.git_ref }" self.arch = arch self.vs_folder = vs_folder self._vs_version = vs_version self._vs_toolset = vs_toolset def download_archive(self) -> None: gdk_url = f"https://github.com/microsoft/GDK/archive/refs/tags/{ GIT_REF }.zip" logger.info("Downloading %s to %s", gdk_url, self.dl_archive_path) urllib.request.urlretrieve(gdk_url, self.dl_archive_path) assert self.dl_archive_path.is_file() def extract_zip_archive(self) -> None: extract_path = self.gdk_extract_path.parent assert self.dl_archive_path.is_file() logger.info("Extracting %s to %s", self.dl_archive_path, extract_path) with zipfile.ZipFile(self.dl_archive_path) as zf: zf.extractall(extract_path) assert self.gdk_extract_path.is_dir(), f"{self.gdk_extract_path} must exist" def extract_development_kit(self) -> None: extract_dks_cmd = self.gdk_extract_path / "SetupScripts/ExtractXboxOneDKs.cmd" assert extract_dks_cmd.is_file() logger.info("Extracting GDK Development Kit: running %s", extract_dks_cmd) cmd = ["cmd.exe", "/C", str(extract_dks_cmd), str(self.gdk_extract_path), str(self.gdk_path)] logger.debug("Running %r", cmd) subprocess.check_call(cmd) def detect_vs_version(self) -> str: vs_regex = re.compile("VS([0-9]{4})") supported_vs_versions = [] for p in self.gaming_grdk_build_path.iterdir(): if not p.is_dir(): continue if m := vs_regex.match(p.name): supported_vs_versions.append(m.group(1)) logger.info(f"Supported Visual Studio versions: {supported_vs_versions}") vs_versions = set(self.vs_folder.parts).intersection(set(supported_vs_versions)) if not vs_versions: raise RuntimeError("Visual Studio version is incompatible") if len(vs_versions) > 1: raise RuntimeError(f"Too many compatible VS versions found ({vs_versions})") vs_version = vs_versions.pop() logger.info(f"Used Visual Studio version: {vs_version}") return vs_version def detect_vs_toolset(self) -> str: toolset_paths = [] for ts_path in self.gdk_toolset_parent_path.iterdir(): if not ts_path.is_dir(): continue ms_props = ts_path / "Microsoft.Cpp.props" if not ms_props.is_file(): continue toolset_paths.append(ts_path.name) logger.info("Detected Visual Studio toolsets: %s", toolset_paths) assert toolset_paths, "Have we detected at least one toolset?" def toolset_number(toolset: str) -> int: if m:= re.match("[^0-9]*([0-9]+).*", toolset): return int(m.group(1)) return -9 return max(toolset_paths, key=toolset_number) @property def vs_version(self) -> str: if self._vs_version is None: self._vs_version = self.detect_vs_version() return self._vs_version @property def vs_toolset(self) -> str: if self._vs_toolset is None: self._vs_toolset = self.detect_vs_toolset() return self._vs_toolset @staticmethod def copy_files_and_merge_into(srcdir: Path, dstdir: Path) -> None: logger.info(f"Copy {srcdir} to {dstdir}") for root, _, files in os.walk(srcdir): dest_root = dstdir / Path(root).relative_to(srcdir) if not dest_root.is_dir(): dest_root.mkdir() for file in files: srcfile = Path(root) / file dstfile = dest_root / file shutil.copy(srcfile, dstfile) def copy_msbuild(self) -> None: vc_toolset_parent_path = self.vs_folder / "MSBuild/Microsoft/VC" if 1: logger.info(f"Detected compatible Visual Studio version: {self.vs_version}") srcdir = vc_toolset_parent_path dstdir = self.gdk_toolset_parent_path assert srcdir.is_dir(), "Source directory must exist" assert dstdir.is_dir(), "Destination directory must exist" self.copy_files_and_merge_into(srcdir=srcdir, dstdir=dstdir) @property def game_dk_path(self) -> Path: return self.gdk_path / "Microsoft GDK" @property def game_dk_latest_path(self) -> Path: return self.game_dk_path / self.gdk_edition @property def windows_sdk_path(self) -> Path: return self.gdk_path / "Windows Kits/10" @property def gaming_grdk_build_path(self) -> Path: return self.game_dk_latest_path / "GRDK" @property def gdk_toolset_parent_path(self) -> Path: return self.gaming_grdk_build_path / f"VS{self.vs_version}/flatDeployment/MSBuild/Microsoft/VC" @property def env(self) -> dict[str, str]: game_dk = self.game_dk_path game_dk_latest = self.game_dk_latest_path windows_sdk_dir = self.windows_sdk_path gaming_grdk_build = self.gaming_grdk_build_path return { "GRDKEDITION": f"{self.gdk_edition}", "GameDK": f"{game_dk}\\", "GameDKLatest": f"{ game_dk_latest }\\", "WindowsSdkDir": f"{ windows_sdk_dir }\\", "GamingGRDKBuild": f"{ gaming_grdk_build }\\", "VSInstallDir": f"{ self.vs_folder }\\", } def create_user_props(self, path: Path) -> None: vc_targets_path = self.gaming_grdk_build_path / f"VS{ self.vs_version }/flatDeployment/MSBuild/Microsoft/VC/{ self.vs_toolset }" vc_targets_path16 = self.gaming_grdk_build_path / f"VS2019/flatDeployment/MSBuild/Microsoft/VC/{ self.vs_toolset }" vc_targets_path17 = self.gaming_grdk_build_path / f"VS2022/flatDeployment/MSBuild/Microsoft/VC/{ self.vs_toolset }" additional_include_directories = ";".join(str(p) for p in self.gdk_include_paths) additional_library_directories = ";".join(str(p) for p in self.gdk_library_paths) durango_xdk_install_path = self.gdk_path / "Microsoft GDK" with path.open("w") as f: f.write(textwrap.dedent(f"""\ { vc_targets_path }\\ { vc_targets_path16 }\\ { vc_targets_path17 }\\ { self.gaming_grdk_build_path }\\ Gaming.Desktop.x64 Debug { self.gdk_edition } { durango_xdk_install_path } $(DurangoXdkInstallPath)\\{self.gdk_edition}\\GRDK\\VS2019\\flatDeployment\\MSBuild\\Microsoft\\VC\\{self.vs_toolset}\\Platforms\\$(Platform)\\ $(DurangoXdkInstallPath)\\{self.gdk_edition}\\GRDK\\VS2019\\flatDeployment\\MSBuild\\Microsoft\\VC\\{self.vs_toolset}\\Platforms\\$(Platform)\\ $(DurangoXdkInstallPath)\\{self.gdk_edition}\\GRDK\\VS2022\\flatDeployment\\MSBuild\\Microsoft\\VC\\{self.vs_toolset}\\Platforms\\$(Platform)\\ $(DurangoXdkInstallPath)\\{self.gdk_edition}\\GRDK\\VS2022\\flatDeployment\\MSBuild\\Microsoft\\VC\\{self.vs_toolset}\\Platforms\\$(Platform)\\ true true true { additional_include_directories };%(AdditionalIncludeDirectories) { additional_library_directories };%(AdditionalLibraryDirectories) """)) @property def gdk_include_paths(self) -> list[Path]: return [ self.gaming_grdk_build_path / "gamekit/include", ] @property def gdk_library_paths(self) -> list[Path]: return [ self.gaming_grdk_build_path / f"gamekit/lib/{self.arch}", ] @property def gdk_binary_path(self) -> list[Path]: return [ self.gaming_grdk_build_path / "bin", self.game_dk_path / "bin", ] @property def build_env(self) -> dict[str, str]: gdk_include = ";".join(str(p) for p in self.gdk_include_paths) gdk_lib = ";".join(str(p) for p in self.gdk_library_paths) gdk_path = ";".join(str(p) for p in self.gdk_binary_path) return { "GDK_INCLUDE": gdk_include, "GDK_LIB": gdk_lib, "GDK_PATH": gdk_path, } def print_env(self) -> None: for k, v in self.env.items(): print(f"set \"{k}={v}\"") print() for k, v in self.build_env.items(): print(f"set \"{k}={v}\"") print() print(f"set \"PATH=%GDK_PATH%;%PATH%\"") print(f"set \"LIB=%GDK_LIB%;%LIB%\"") print(f"set \"INCLUDE=%GDK_INCLUDE%;%INCLUDE%\"") def main(): logging.basicConfig(level=logging.INFO) parser = argparse.ArgumentParser(allow_abbrev=False) parser.add_argument("--arch", choices=["amd64"], default="amd64", help="Architecture") parser.add_argument("--download", action="store_true", help="Download GDK") parser.add_argument("--extract", action="store_true", help="Extract downloaded GDK") parser.add_argument("--copy-msbuild", action="store_true", help="Copy MSBuild files") parser.add_argument("--temp-folder", help="Temporary folder where to download and extract GDK") parser.add_argument("--gdk-path", required=True, type=Path, help="Folder where to store the GDK") parser.add_argument("--ref-edition", type=str, help="Git ref and GDK edition separated by comma") parser.add_argument("--vs-folder", required=True, type=Path, help="Installation folder of Visual Studio") parser.add_argument("--vs-version", required=False, type=int, help="Visual Studio version") parser.add_argument("--vs-toolset", required=False, help="Visual Studio toolset (e.g. v150)") parser.add_argument("--props-folder", required=False, type=Path, default=Path(), help="Visual Studio toolset (e.g. v150)") parser.add_argument("--no-user-props", required=False, dest="user_props", action="store_false", help="Don't ") args = parser.parse_args() logging.basicConfig(level=logging.INFO) git_ref = None gdk_edition = None if args.ref_edition is not None: git_ref, gdk_edition = args.ref_edition.split(",", 1) try: int(gdk_edition) except ValueError: parser.error("Edition should be an integer (YYMMUU) (Y=year M=month U=update)") configurator = GdDesktopConfigurator( arch=args.arch, git_ref=git_ref, gdk_edition=gdk_edition, vs_folder=args.vs_folder, vs_version=args.vs_version, vs_toolset=args.vs_toolset, gdk_path=args.gdk_path, temp_folder=args.temp_folder, ) if args.download: configurator.download_archive() if args.extract: configurator.extract_zip_archive() configurator.extract_development_kit() if args.copy_msbuild: configurator.copy_msbuild() if args.user_props: configurator.print_env() configurator.create_user_props(args.props_folder / "Directory.Build.props") if __name__ == "__main__": raise SystemExit(main())