build_assemblies.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. #!/usr/bin/python3
  2. import os
  3. import os.path
  4. import shlex
  5. import subprocess
  6. from dataclasses import dataclass
  7. from typing import Optional, List
  8. def find_dotnet_cli():
  9. if os.name == "nt":
  10. for hint_dir in os.environ["PATH"].split(os.pathsep):
  11. hint_dir = hint_dir.strip('"')
  12. hint_path = os.path.join(hint_dir, "dotnet")
  13. if os.path.isfile(hint_path) and os.access(hint_path, os.X_OK):
  14. return hint_path
  15. if os.path.isfile(hint_path + ".exe") and os.access(hint_path + ".exe", os.X_OK):
  16. return hint_path + ".exe"
  17. else:
  18. for hint_dir in os.environ["PATH"].split(os.pathsep):
  19. hint_dir = hint_dir.strip('"')
  20. hint_path = os.path.join(hint_dir, "dotnet")
  21. if os.path.isfile(hint_path) and os.access(hint_path, os.X_OK):
  22. return hint_path
  23. def find_msbuild_standalone_windows():
  24. msbuild_tools_path = find_msbuild_tools_path_reg()
  25. if msbuild_tools_path:
  26. return os.path.join(msbuild_tools_path, "MSBuild.exe")
  27. return None
  28. def find_msbuild_mono_windows(mono_prefix):
  29. assert mono_prefix is not None
  30. mono_bin_dir = os.path.join(mono_prefix, "bin")
  31. msbuild_mono = os.path.join(mono_bin_dir, "msbuild.bat")
  32. if os.path.isfile(msbuild_mono):
  33. return msbuild_mono
  34. return None
  35. def find_msbuild_mono_unix():
  36. import sys
  37. hint_dirs = []
  38. if sys.platform == "darwin":
  39. hint_dirs[:0] = [
  40. "/Library/Frameworks/Mono.framework/Versions/Current/bin",
  41. "/usr/local/var/homebrew/linked/mono/bin",
  42. ]
  43. for hint_dir in hint_dirs:
  44. hint_path = os.path.join(hint_dir, "msbuild")
  45. if os.path.isfile(hint_path):
  46. return hint_path
  47. elif os.path.isfile(hint_path + ".exe"):
  48. return hint_path + ".exe"
  49. for hint_dir in os.environ["PATH"].split(os.pathsep):
  50. hint_dir = hint_dir.strip('"')
  51. hint_path = os.path.join(hint_dir, "msbuild")
  52. if os.path.isfile(hint_path) and os.access(hint_path, os.X_OK):
  53. return hint_path
  54. if os.path.isfile(hint_path + ".exe") and os.access(hint_path + ".exe", os.X_OK):
  55. return hint_path + ".exe"
  56. return None
  57. def find_msbuild_tools_path_reg():
  58. import subprocess
  59. program_files = os.getenv("PROGRAMFILES(X86)")
  60. if not program_files:
  61. program_files = os.getenv("PROGRAMFILES")
  62. vswhere = os.path.join(program_files, "Microsoft Visual Studio", "Installer", "vswhere.exe")
  63. vswhere_args = ["-latest", "-products", "*", "-requires", "Microsoft.Component.MSBuild"]
  64. try:
  65. lines = subprocess.check_output([vswhere] + vswhere_args).splitlines()
  66. for line in lines:
  67. parts = line.decode("utf-8").split(":", 1)
  68. if len(parts) < 2 or parts[0] != "installationPath":
  69. continue
  70. val = parts[1].strip()
  71. if not val:
  72. raise ValueError("Value of `installationPath` entry is empty")
  73. # Since VS2019, the directory is simply named "Current"
  74. msbuild_dir = os.path.join(val, "MSBuild", "Current", "Bin")
  75. if os.path.isdir(msbuild_dir):
  76. return msbuild_dir
  77. # Directory name "15.0" is used in VS 2017
  78. return os.path.join(val, "MSBuild", "15.0", "Bin")
  79. raise ValueError("Cannot find `installationPath` entry")
  80. except ValueError as e:
  81. print("Error reading output from vswhere: " + str(e))
  82. except OSError:
  83. pass # Fine, vswhere not found
  84. except (subprocess.CalledProcessError, OSError):
  85. pass
  86. @dataclass
  87. class ToolsLocation:
  88. dotnet_cli: str = ""
  89. msbuild_standalone: str = ""
  90. msbuild_mono: str = ""
  91. mono_bin_dir: str = ""
  92. def find_any_msbuild_tool(mono_prefix):
  93. # Preference order: dotnet CLI > Standalone MSBuild > Mono's MSBuild
  94. # Find dotnet CLI
  95. dotnet_cli = find_dotnet_cli()
  96. if dotnet_cli:
  97. return ToolsLocation(dotnet_cli=dotnet_cli)
  98. # Find standalone MSBuild
  99. if os.name == "nt":
  100. msbuild_standalone = find_msbuild_standalone_windows()
  101. if msbuild_standalone:
  102. return ToolsLocation(msbuild_standalone=msbuild_standalone)
  103. if mono_prefix:
  104. # Find Mono's MSBuild
  105. if os.name == "nt":
  106. msbuild_mono = find_msbuild_mono_windows(mono_prefix)
  107. if msbuild_mono:
  108. return ToolsLocation(msbuild_mono=msbuild_mono)
  109. else:
  110. msbuild_mono = find_msbuild_mono_unix()
  111. if msbuild_mono:
  112. return ToolsLocation(msbuild_mono=msbuild_mono)
  113. return None
  114. def run_msbuild(tools: ToolsLocation, sln: str, msbuild_args: Optional[List[str]] = None):
  115. using_msbuild_mono = False
  116. # Preference order: dotnet CLI > Standalone MSBuild > Mono's MSBuild
  117. if tools.dotnet_cli:
  118. args = [tools.dotnet_cli, "msbuild"]
  119. elif tools.msbuild_standalone:
  120. args = [tools.msbuild_standalone]
  121. elif tools.msbuild_mono:
  122. args = [tools.msbuild_mono]
  123. using_msbuild_mono = True
  124. else:
  125. raise RuntimeError("Path to MSBuild or dotnet CLI not provided.")
  126. args += [sln]
  127. if msbuild_args:
  128. args += msbuild_args
  129. print("Running MSBuild: ", " ".join(shlex.quote(arg) for arg in args), flush=True)
  130. msbuild_env = os.environ.copy()
  131. # Needed when running from Developer Command Prompt for VS
  132. if "PLATFORM" in msbuild_env:
  133. del msbuild_env["PLATFORM"]
  134. if using_msbuild_mono:
  135. # The (Csc/Vbc/Fsc)ToolExe environment variables are required when
  136. # building with Mono's MSBuild. They must point to the batch files
  137. # in Mono's bin directory to make sure they are executed with Mono.
  138. msbuild_env.update(
  139. {
  140. "CscToolExe": os.path.join(tools.mono_bin_dir, "csc.bat"),
  141. "VbcToolExe": os.path.join(tools.mono_bin_dir, "vbc.bat"),
  142. "FscToolExe": os.path.join(tools.mono_bin_dir, "fsharpc.bat"),
  143. }
  144. )
  145. return subprocess.call(args, env=msbuild_env)
  146. def build_godot_api(msbuild_tool, module_dir, output_dir, push_nupkgs_local, precision):
  147. target_filenames = [
  148. "GodotSharp.dll",
  149. "GodotSharp.pdb",
  150. "GodotSharp.xml",
  151. "GodotSharpEditor.dll",
  152. "GodotSharpEditor.pdb",
  153. "GodotSharpEditor.xml",
  154. "GodotPlugins.dll",
  155. "GodotPlugins.pdb",
  156. "GodotPlugins.runtimeconfig.json",
  157. ]
  158. for build_config in ["Debug", "Release"]:
  159. editor_api_dir = os.path.join(output_dir, "GodotSharp", "Api", build_config)
  160. targets = [os.path.join(editor_api_dir, filename) for filename in target_filenames]
  161. args = ["/restore", "/t:Build", "/p:Configuration=" + build_config, "/p:NoWarn=1591"]
  162. if push_nupkgs_local:
  163. args += ["/p:ClearNuGetLocalCache=true", "/p:PushNuGetToLocalSource=" + push_nupkgs_local]
  164. if precision == "double":
  165. args += ["/p:GodotFloat64=true"]
  166. sln = os.path.join(module_dir, "glue/GodotSharp/GodotSharp.sln")
  167. exit_code = run_msbuild(
  168. msbuild_tool,
  169. sln=sln,
  170. msbuild_args=args,
  171. )
  172. if exit_code != 0:
  173. return exit_code
  174. # Copy targets
  175. core_src_dir = os.path.abspath(os.path.join(sln, os.pardir, "GodotSharp", "bin", build_config))
  176. editor_src_dir = os.path.abspath(os.path.join(sln, os.pardir, "GodotSharpEditor", "bin", build_config))
  177. plugins_src_dir = os.path.abspath(os.path.join(sln, os.pardir, "GodotPlugins", "bin", build_config, "net6.0"))
  178. if not os.path.isdir(editor_api_dir):
  179. assert not os.path.isfile(editor_api_dir)
  180. os.makedirs(editor_api_dir)
  181. def copy_target(target_path):
  182. from shutil import copy
  183. filename = os.path.basename(target_path)
  184. src_path = os.path.join(core_src_dir, filename)
  185. if not os.path.isfile(src_path):
  186. src_path = os.path.join(editor_src_dir, filename)
  187. if not os.path.isfile(src_path):
  188. src_path = os.path.join(plugins_src_dir, filename)
  189. print(f"Copying assembly to {target_path}...")
  190. copy(src_path, target_path)
  191. for scons_target in targets:
  192. copy_target(scons_target)
  193. return 0
  194. def generate_sdk_package_versions():
  195. # I can't believe importing files in Python is so convoluted when not
  196. # following the golden standard for packages/modules.
  197. import os
  198. import sys
  199. from os.path import dirname
  200. # We want ../../../methods.py.
  201. script_path = dirname(os.path.abspath(__file__))
  202. root_path = dirname(dirname(dirname(script_path)))
  203. sys.path.insert(0, root_path)
  204. from methods import get_version_info
  205. version_info = get_version_info("")
  206. sys.path.remove(root_path)
  207. version_str = "{major}.{minor}.{patch}".format(**version_info)
  208. version_status = version_info["status"]
  209. if version_status != "stable": # Pre-release
  210. # If version was overridden to be e.g. "beta3", we insert a dot between
  211. # "beta" and "3" to follow SemVer 2.0.
  212. import re
  213. match = re.search(r"[\d]+$", version_status)
  214. if match:
  215. pos = match.start()
  216. version_status = version_status[:pos] + "." + version_status[pos:]
  217. version_str += "-" + version_status
  218. import version
  219. version_defines = (
  220. [
  221. f"GODOT{version.major}",
  222. f"GODOT{version.major}_{version.minor}",
  223. f"GODOT{version.major}_{version.minor}_{version.patch}",
  224. ]
  225. + [f"GODOT{v}_OR_GREATER" for v in range(4, version.major + 1)]
  226. + [f"GODOT{version.major}_{v}_OR_GREATER" for v in range(0, version.minor + 1)]
  227. + [f"GODOT{version.major}_{version.minor}_{v}_OR_GREATER" for v in range(0, version.patch + 1)]
  228. )
  229. props = """<Project>
  230. <PropertyGroup>
  231. <PackageVersion_GodotSharp>{0}</PackageVersion_GodotSharp>
  232. <PackageVersion_Godot_NET_Sdk>{0}</PackageVersion_Godot_NET_Sdk>
  233. <PackageVersion_Godot_SourceGenerators>{0}</PackageVersion_Godot_SourceGenerators>
  234. <GodotVersionConstants>{1}</GodotVersionConstants>
  235. </PropertyGroup>
  236. </Project>
  237. """.format(
  238. version_str, ";".join(version_defines)
  239. )
  240. # We write in ../SdkPackageVersions.props.
  241. with open(os.path.join(dirname(script_path), "SdkPackageVersions.props"), "w") as f:
  242. f.write(props)
  243. f.close()
  244. # Also write the versioned docs URL to a constant for the Source Generators.
  245. constants = """namespace Godot.SourceGenerators
  246. {{
  247. partial class Common
  248. {{
  249. public const string VersionDocsUrl = "https://docs.godotengine.org/en/{docs_branch}";
  250. }}
  251. }}
  252. """.format(
  253. **version_info
  254. )
  255. generators_dir = os.path.join(
  256. dirname(script_path),
  257. "editor",
  258. "Godot.NET.Sdk",
  259. "Godot.SourceGenerators",
  260. "Generated",
  261. )
  262. os.makedirs(generators_dir, exist_ok=True)
  263. with open(os.path.join(generators_dir, "Common.Constants.cs"), "w") as f:
  264. f.write(constants)
  265. f.close()
  266. def build_all(msbuild_tool, module_dir, output_dir, godot_platform, dev_debug, push_nupkgs_local, precision):
  267. # Generate SdkPackageVersions.props and VersionDocsUrl constant
  268. generate_sdk_package_versions()
  269. # Godot API
  270. exit_code = build_godot_api(msbuild_tool, module_dir, output_dir, push_nupkgs_local, precision)
  271. if exit_code != 0:
  272. return exit_code
  273. # GodotTools
  274. sln = os.path.join(module_dir, "editor/GodotTools/GodotTools.sln")
  275. args = ["/restore", "/t:Build", "/p:Configuration=" + ("Debug" if dev_debug else "Release")] + (
  276. ["/p:GodotPlatform=" + godot_platform] if godot_platform else []
  277. )
  278. if push_nupkgs_local:
  279. args += ["/p:ClearNuGetLocalCache=true", "/p:PushNuGetToLocalSource=" + push_nupkgs_local]
  280. if precision == "double":
  281. args += ["/p:GodotFloat64=true"]
  282. exit_code = run_msbuild(msbuild_tool, sln=sln, msbuild_args=args)
  283. if exit_code != 0:
  284. return exit_code
  285. # Godot.NET.Sdk
  286. args = ["/restore", "/t:Build", "/p:Configuration=Release"]
  287. if push_nupkgs_local:
  288. args += ["/p:ClearNuGetLocalCache=true", "/p:PushNuGetToLocalSource=" + push_nupkgs_local]
  289. if precision == "double":
  290. args += ["/p:GodotFloat64=true"]
  291. sln = os.path.join(module_dir, "editor/Godot.NET.Sdk/Godot.NET.Sdk.sln")
  292. exit_code = run_msbuild(msbuild_tool, sln=sln, msbuild_args=args)
  293. if exit_code != 0:
  294. return exit_code
  295. return 0
  296. def main():
  297. import argparse
  298. import sys
  299. parser = argparse.ArgumentParser(description="Builds all Godot .NET solutions")
  300. parser.add_argument("--godot-output-dir", type=str, required=True)
  301. parser.add_argument(
  302. "--dev-debug",
  303. action="store_true",
  304. default=False,
  305. help="Build GodotTools and Godot.NET.Sdk with 'Configuration=Debug'",
  306. )
  307. parser.add_argument("--godot-platform", type=str, default="")
  308. parser.add_argument("--mono-prefix", type=str, default="")
  309. parser.add_argument("--push-nupkgs-local", type=str, default="")
  310. parser.add_argument(
  311. "--precision", type=str, default="single", choices=["single", "double"], help="Floating-point precision level"
  312. )
  313. args = parser.parse_args()
  314. this_script_dir = os.path.dirname(os.path.realpath(__file__))
  315. module_dir = os.path.abspath(os.path.join(this_script_dir, os.pardir))
  316. output_dir = os.path.abspath(args.godot_output_dir)
  317. push_nupkgs_local = os.path.abspath(args.push_nupkgs_local) if args.push_nupkgs_local else None
  318. msbuild_tool = find_any_msbuild_tool(args.mono_prefix)
  319. if msbuild_tool is None:
  320. print("Unable to find MSBuild")
  321. sys.exit(1)
  322. exit_code = build_all(
  323. msbuild_tool,
  324. module_dir,
  325. output_dir,
  326. args.godot_platform,
  327. args.dev_debug,
  328. push_nupkgs_local,
  329. args.precision,
  330. )
  331. sys.exit(exit_code)
  332. if __name__ == "__main__":
  333. main()