setup-gdk-desktop.py 13 KB

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