Simple Directmedia Layer
at main 13 kB view raw
1#!/usr/bin/env python 2 3import argparse 4import functools 5import logging 6import os 7from pathlib import Path 8import re 9import shutil 10import subprocess 11import tempfile 12import textwrap 13import urllib.request 14import zipfile 15 16# Update both variables when updating the GDK 17GIT_REF = "June_2024_Update_1" 18GDK_EDITION = "240601" # YYMMUU 19 20logger = logging.getLogger(__name__) 21 22class GdDesktopConfigurator: 23 def __init__(self, gdk_path, arch, vs_folder, vs_version=None, vs_toolset=None, temp_folder=None, git_ref=None, gdk_edition=None): 24 self.git_ref = git_ref or GIT_REF 25 self.gdk_edition = gdk_edition or GDK_EDITION 26 self.gdk_path = gdk_path 27 self.temp_folder = temp_folder or Path(tempfile.gettempdir()) 28 self.dl_archive_path = Path(self.temp_folder) / f"{ self.git_ref }.zip" 29 self.gdk_extract_path = Path(self.temp_folder) / f"GDK-{ self.git_ref }" 30 self.arch = arch 31 self.vs_folder = vs_folder 32 self._vs_version = vs_version 33 self._vs_toolset = vs_toolset 34 35 def download_archive(self) -> None: 36 gdk_url = f"https://github.com/microsoft/GDK/archive/refs/tags/{ GIT_REF }.zip" 37 logger.info("Downloading %s to %s", gdk_url, self.dl_archive_path) 38 urllib.request.urlretrieve(gdk_url, self.dl_archive_path) 39 assert self.dl_archive_path.is_file() 40 41 def extract_zip_archive(self) -> None: 42 extract_path = self.gdk_extract_path.parent 43 assert self.dl_archive_path.is_file() 44 logger.info("Extracting %s to %s", self.dl_archive_path, extract_path) 45 with zipfile.ZipFile(self.dl_archive_path) as zf: 46 zf.extractall(extract_path) 47 assert self.gdk_extract_path.is_dir(), f"{self.gdk_extract_path} must exist" 48 49 def extract_development_kit(self) -> None: 50 extract_dks_cmd = self.gdk_extract_path / "SetupScripts/ExtractXboxOneDKs.cmd" 51 assert extract_dks_cmd.is_file() 52 logger.info("Extracting GDK Development Kit: running %s", extract_dks_cmd) 53 cmd = ["cmd.exe", "/C", str(extract_dks_cmd), str(self.gdk_extract_path), str(self.gdk_path)] 54 logger.debug("Running %r", cmd) 55 subprocess.check_call(cmd) 56 57 def detect_vs_version(self) -> str: 58 vs_regex = re.compile("VS([0-9]{4})") 59 supported_vs_versions = [] 60 for p in self.gaming_grdk_build_path.iterdir(): 61 if not p.is_dir(): 62 continue 63 if m := vs_regex.match(p.name): 64 supported_vs_versions.append(m.group(1)) 65 logger.info(f"Supported Visual Studio versions: {supported_vs_versions}") 66 vs_versions = set(self.vs_folder.parts).intersection(set(supported_vs_versions)) 67 if not vs_versions: 68 raise RuntimeError("Visual Studio version is incompatible") 69 if len(vs_versions) > 1: 70 raise RuntimeError(f"Too many compatible VS versions found ({vs_versions})") 71 vs_version = vs_versions.pop() 72 logger.info(f"Used Visual Studio version: {vs_version}") 73 return vs_version 74 75 def detect_vs_toolset(self) -> str: 76 toolset_paths = [] 77 for ts_path in self.gdk_toolset_parent_path.iterdir(): 78 if not ts_path.is_dir(): 79 continue 80 ms_props = ts_path / "Microsoft.Cpp.props" 81 if not ms_props.is_file(): 82 continue 83 toolset_paths.append(ts_path.name) 84 logger.info("Detected Visual Studio toolsets: %s", toolset_paths) 85 assert toolset_paths, "Have we detected at least one toolset?" 86 87 def toolset_number(toolset: str) -> int: 88 if m:= re.match("[^0-9]*([0-9]+).*", toolset): 89 return int(m.group(1)) 90 return -9 91 92 return max(toolset_paths, key=toolset_number) 93 94 @property 95 def vs_version(self) -> str: 96 if self._vs_version is None: 97 self._vs_version = self.detect_vs_version() 98 return self._vs_version 99 100 @property 101 def vs_toolset(self) -> str: 102 if self._vs_toolset is None: 103 self._vs_toolset = self.detect_vs_toolset() 104 return self._vs_toolset 105 106 @staticmethod 107 def copy_files_and_merge_into(srcdir: Path, dstdir: Path) -> None: 108 logger.info(f"Copy {srcdir} to {dstdir}") 109 for root, _, files in os.walk(srcdir): 110 dest_root = dstdir / Path(root).relative_to(srcdir) 111 if not dest_root.is_dir(): 112 dest_root.mkdir() 113 for file in files: 114 srcfile = Path(root) / file 115 dstfile = dest_root / file 116 shutil.copy(srcfile, dstfile) 117 118 def copy_msbuild(self) -> None: 119 vc_toolset_parent_path = self.vs_folder / "MSBuild/Microsoft/VC" 120 if 1: 121 logger.info(f"Detected compatible Visual Studio version: {self.vs_version}") 122 srcdir = vc_toolset_parent_path 123 dstdir = self.gdk_toolset_parent_path 124 assert srcdir.is_dir(), "Source directory must exist" 125 assert dstdir.is_dir(), "Destination directory must exist" 126 127 self.copy_files_and_merge_into(srcdir=srcdir, dstdir=dstdir) 128 129 @property 130 def game_dk_path(self) -> Path: 131 return self.gdk_path / "Microsoft GDK" 132 133 @property 134 def game_dk_latest_path(self) -> Path: 135 return self.game_dk_path / self.gdk_edition 136 137 @property 138 def windows_sdk_path(self) -> Path: 139 return self.gdk_path / "Windows Kits/10" 140 141 @property 142 def gaming_grdk_build_path(self) -> Path: 143 return self.game_dk_latest_path / "GRDK" 144 145 @property 146 def gdk_toolset_parent_path(self) -> Path: 147 return self.gaming_grdk_build_path / f"VS{self.vs_version}/flatDeployment/MSBuild/Microsoft/VC" 148 149 @property 150 def env(self) -> dict[str, str]: 151 game_dk = self.game_dk_path 152 game_dk_latest = self.game_dk_latest_path 153 windows_sdk_dir = self.windows_sdk_path 154 gaming_grdk_build = self.gaming_grdk_build_path 155 156 return { 157 "GRDKEDITION": f"{self.gdk_edition}", 158 "GameDK": f"{game_dk}\\", 159 "GameDKLatest": f"{ game_dk_latest }\\", 160 "WindowsSdkDir": f"{ windows_sdk_dir }\\", 161 "GamingGRDKBuild": f"{ gaming_grdk_build }\\", 162 "VSInstallDir": f"{ self.vs_folder }\\", 163 } 164 165 def create_user_props(self, path: Path) -> None: 166 vc_targets_path = self.gaming_grdk_build_path / f"VS{ self.vs_version }/flatDeployment/MSBuild/Microsoft/VC/{ self.vs_toolset }" 167 vc_targets_path16 = self.gaming_grdk_build_path / f"VS2019/flatDeployment/MSBuild/Microsoft/VC/{ self.vs_toolset }" 168 vc_targets_path17 = self.gaming_grdk_build_path / f"VS2022/flatDeployment/MSBuild/Microsoft/VC/{ self.vs_toolset }" 169 additional_include_directories = ";".join(str(p) for p in self.gdk_include_paths) 170 additional_library_directories = ";".join(str(p) for p in self.gdk_library_paths) 171 durango_xdk_install_path = self.gdk_path / "Microsoft GDK" 172 with path.open("w") as f: 173 f.write(textwrap.dedent(f"""\ 174 <?xml version="1.0" encoding="utf-8"?> 175 <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> 176 <PropertyGroup> 177 <VCTargetsPath>{ vc_targets_path }\\</VCTargetsPath> 178 <VCTargetsPath16>{ vc_targets_path16 }\\</VCTargetsPath16> 179 <VCTargetsPath17>{ vc_targets_path17 }\\</VCTargetsPath17> 180 <BWOI_GDK_Path>{ self.gaming_grdk_build_path }\\</BWOI_GDK_Path> 181 <Platform Condition="'$(Platform)' == ''">Gaming.Desktop.x64</Platform> 182 <Configuration Condition="'$(Configuration)' == ''">Debug</Configuration> 183 <XdkEditionTarget>{ self.gdk_edition }</XdkEditionTarget> 184 <DurangoXdkInstallPath>{ durango_xdk_install_path }</DurangoXdkInstallPath> 185 186 <DefaultXdkEditionRootVS2019>$(DurangoXdkInstallPath)\\{self.gdk_edition}\\GRDK\\VS2019\\flatDeployment\\MSBuild\\Microsoft\\VC\\{self.vs_toolset}\\Platforms\\$(Platform)\\</DefaultXdkEditionRootVS2019> 187 <XdkEditionRootVS2019>$(DurangoXdkInstallPath)\\{self.gdk_edition}\\GRDK\\VS2019\\flatDeployment\\MSBuild\\Microsoft\\VC\\{self.vs_toolset}\\Platforms\\$(Platform)\\</XdkEditionRootVS2019> 188 <DefaultXdkEditionRootVS2022>$(DurangoXdkInstallPath)\\{self.gdk_edition}\\GRDK\\VS2022\\flatDeployment\\MSBuild\\Microsoft\\VC\\{self.vs_toolset}\\Platforms\\$(Platform)\\</DefaultXdkEditionRootVS2022> 189 <XdkEditionRootVS2022>$(DurangoXdkInstallPath)\\{self.gdk_edition}\\GRDK\\VS2022\\flatDeployment\\MSBuild\\Microsoft\\VC\\{self.vs_toolset}\\Platforms\\$(Platform)\\</XdkEditionRootVS2022> 190 191 <Deterministic>true</Deterministic> 192 <DisableInstalledVCTargetsUse>true</DisableInstalledVCTargetsUse> 193 <ClearDevCommandPromptEnvVars>true</ClearDevCommandPromptEnvVars> 194 </PropertyGroup> 195 <ItemDefinitionGroup Condition="'$(Platform)' == 'Gaming.Desktop.x64'"> 196 <ClCompile> 197 <AdditionalIncludeDirectories>{ additional_include_directories };%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> 198 </ClCompile> 199 <Link> 200 <AdditionalLibraryDirectories>{ additional_library_directories };%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories> 201 </Link> 202 </ItemDefinitionGroup> 203 </Project> 204 """)) 205 206 @property 207 def gdk_include_paths(self) -> list[Path]: 208 return [ 209 self.gaming_grdk_build_path / "gamekit/include", 210 ] 211 212 @property 213 def gdk_library_paths(self) -> list[Path]: 214 return [ 215 self.gaming_grdk_build_path / f"gamekit/lib/{self.arch}", 216 ] 217 218 @property 219 def gdk_binary_path(self) -> list[Path]: 220 return [ 221 self.gaming_grdk_build_path / "bin", 222 self.game_dk_path / "bin", 223 ] 224 225 @property 226 def build_env(self) -> dict[str, str]: 227 gdk_include = ";".join(str(p) for p in self.gdk_include_paths) 228 gdk_lib = ";".join(str(p) for p in self.gdk_library_paths) 229 gdk_path = ";".join(str(p) for p in self.gdk_binary_path) 230 return { 231 "GDK_INCLUDE": gdk_include, 232 "GDK_LIB": gdk_lib, 233 "GDK_PATH": gdk_path, 234 } 235 236 def print_env(self) -> None: 237 for k, v in self.env.items(): 238 print(f"set \"{k}={v}\"") 239 print() 240 for k, v in self.build_env.items(): 241 print(f"set \"{k}={v}\"") 242 print() 243 print(f"set \"PATH=%GDK_PATH%;%PATH%\"") 244 print(f"set \"LIB=%GDK_LIB%;%LIB%\"") 245 print(f"set \"INCLUDE=%GDK_INCLUDE%;%INCLUDE%\"") 246 247 248def main(): 249 logging.basicConfig(level=logging.INFO) 250 parser = argparse.ArgumentParser(allow_abbrev=False) 251 parser.add_argument("--arch", choices=["amd64"], default="amd64", help="Architecture") 252 parser.add_argument("--download", action="store_true", help="Download GDK") 253 parser.add_argument("--extract", action="store_true", help="Extract downloaded GDK") 254 parser.add_argument("--copy-msbuild", action="store_true", help="Copy MSBuild files") 255 parser.add_argument("--temp-folder", help="Temporary folder where to download and extract GDK") 256 parser.add_argument("--gdk-path", required=True, type=Path, help="Folder where to store the GDK") 257 parser.add_argument("--ref-edition", type=str, help="Git ref and GDK edition separated by comma") 258 parser.add_argument("--vs-folder", required=True, type=Path, help="Installation folder of Visual Studio") 259 parser.add_argument("--vs-version", required=False, type=int, help="Visual Studio version") 260 parser.add_argument("--vs-toolset", required=False, help="Visual Studio toolset (e.g. v150)") 261 parser.add_argument("--props-folder", required=False, type=Path, default=Path(), help="Visual Studio toolset (e.g. v150)") 262 parser.add_argument("--no-user-props", required=False, dest="user_props", action="store_false", help="Don't ") 263 args = parser.parse_args() 264 265 logging.basicConfig(level=logging.INFO) 266 267 git_ref = None 268 gdk_edition = None 269 if args.ref_edition is not None: 270 git_ref, gdk_edition = args.ref_edition.split(",", 1) 271 try: 272 int(gdk_edition) 273 except ValueError: 274 parser.error("Edition should be an integer (YYMMUU) (Y=year M=month U=update)") 275 276 configurator = GdDesktopConfigurator( 277 arch=args.arch, 278 git_ref=git_ref, 279 gdk_edition=gdk_edition, 280 vs_folder=args.vs_folder, 281 vs_version=args.vs_version, 282 vs_toolset=args.vs_toolset, 283 gdk_path=args.gdk_path, 284 temp_folder=args.temp_folder, 285 ) 286 287 if args.download: 288 configurator.download_archive() 289 290 if args.extract: 291 configurator.extract_zip_archive() 292 293 configurator.extract_development_kit() 294 295 if args.copy_msbuild: 296 configurator.copy_msbuild() 297 298 if args.user_props: 299 configurator.print_env() 300 configurator.create_user_props(args.props_folder / "Directory.Build.props") 301 302if __name__ == "__main__": 303 raise SystemExit(main())