| #!/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"""\ |
| <?xml version="1.0" encoding="utf-8"?> |
| <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
| <PropertyGroup> |
| <VCTargetsPath>{ vc_targets_path }\\</VCTargetsPath> |
| <VCTargetsPath16>{ vc_targets_path16 }\\</VCTargetsPath16> |
| <VCTargetsPath17>{ vc_targets_path17 }\\</VCTargetsPath17> |
| <BWOI_GDK_Path>{ self.gaming_grdk_build_path }\\</BWOI_GDK_Path> |
| <Platform Condition="'$(Platform)' == ''">Gaming.Desktop.x64</Platform> |
| <Configuration Condition="'$(Configuration)' == ''">Debug</Configuration> |
| <XdkEditionTarget>{ self.gdk_edition }</XdkEditionTarget> |
| <DurangoXdkInstallPath>{ durango_xdk_install_path }</DurangoXdkInstallPath> |
| |
| <DefaultXdkEditionRootVS2019>$(DurangoXdkInstallPath)\\{self.gdk_edition}\\GRDK\\VS2019\\flatDeployment\\MSBuild\\Microsoft\\VC\\{self.vs_toolset}\\Platforms\\$(Platform)\\</DefaultXdkEditionRootVS2019> |
| <XdkEditionRootVS2019>$(DurangoXdkInstallPath)\\{self.gdk_edition}\\GRDK\\VS2019\\flatDeployment\\MSBuild\\Microsoft\\VC\\{self.vs_toolset}\\Platforms\\$(Platform)\\</XdkEditionRootVS2019> |
| <DefaultXdkEditionRootVS2022>$(DurangoXdkInstallPath)\\{self.gdk_edition}\\GRDK\\VS2022\\flatDeployment\\MSBuild\\Microsoft\\VC\\{self.vs_toolset}\\Platforms\\$(Platform)\\</DefaultXdkEditionRootVS2022> |
| <XdkEditionRootVS2022>$(DurangoXdkInstallPath)\\{self.gdk_edition}\\GRDK\\VS2022\\flatDeployment\\MSBuild\\Microsoft\\VC\\{self.vs_toolset}\\Platforms\\$(Platform)\\</XdkEditionRootVS2022> |
| |
| <Deterministic>true</Deterministic> |
| <DisableInstalledVCTargetsUse>true</DisableInstalledVCTargetsUse> |
| <ClearDevCommandPromptEnvVars>true</ClearDevCommandPromptEnvVars> |
| </PropertyGroup> |
| <ItemDefinitionGroup Condition="'$(Platform)' == 'Gaming.Desktop.x64'"> |
| <ClCompile> |
| <AdditionalIncludeDirectories>{ additional_include_directories };%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> |
| </ClCompile> |
| <Link> |
| <AdditionalLibraryDirectories>{ additional_library_directories };%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories> |
| </Link> |
| </ItemDefinitionGroup> |
| </Project> |
| """)) |
| |
| @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()) |