build_assemblies.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  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, chdir_to: 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. # We want to control cwd when running msbuild, because that's where the search for global.json begins.
  146. return subprocess.call(args, env=msbuild_env, cwd=chdir_to)
  147. def build_godot_api(msbuild_tool, module_dir, output_dir, push_nupkgs_local, precision):
  148. target_filenames = [
  149. "GodotSharp.dll",
  150. "GodotSharp.pdb",
  151. "GodotSharp.xml",
  152. "GodotSharpEditor.dll",
  153. "GodotSharpEditor.pdb",
  154. "GodotSharpEditor.xml",
  155. "GodotPlugins.dll",
  156. "GodotPlugins.pdb",
  157. "GodotPlugins.runtimeconfig.json",
  158. ]
  159. for build_config in ["Debug", "Release"]:
  160. editor_api_dir = os.path.join(output_dir, "GodotSharp", "Api", build_config)
  161. targets = [os.path.join(editor_api_dir, filename) for filename in target_filenames]
  162. args = ["/restore", "/t:Build", "/p:Configuration=" + build_config, "/p:NoWarn=1591"]
  163. if push_nupkgs_local:
  164. args += ["/p:ClearNuGetLocalCache=true", "/p:PushNuGetToLocalSource=" + push_nupkgs_local]
  165. if precision == "double":
  166. args += ["/p:GodotFloat64=true"]
  167. sln = os.path.join(module_dir, "glue/GodotSharp/GodotSharp.sln")
  168. exit_code = run_msbuild(msbuild_tool, sln=sln, chdir_to=module_dir, msbuild_args=args)
  169. if exit_code != 0:
  170. return exit_code
  171. # Copy targets
  172. core_src_dir = os.path.abspath(os.path.join(sln, os.pardir, "GodotSharp", "bin", build_config))
  173. editor_src_dir = os.path.abspath(os.path.join(sln, os.pardir, "GodotSharpEditor", "bin", build_config))
  174. plugins_src_dir = os.path.abspath(os.path.join(sln, os.pardir, "GodotPlugins", "bin", build_config, "net6.0"))
  175. if not os.path.isdir(editor_api_dir):
  176. assert not os.path.isfile(editor_api_dir)
  177. os.makedirs(editor_api_dir)
  178. def copy_target(target_path):
  179. from shutil import copy
  180. filename = os.path.basename(target_path)
  181. src_path = os.path.join(core_src_dir, filename)
  182. if not os.path.isfile(src_path):
  183. src_path = os.path.join(editor_src_dir, filename)
  184. if not os.path.isfile(src_path):
  185. src_path = os.path.join(plugins_src_dir, filename)
  186. print(f"Copying assembly to {target_path}...")
  187. copy(src_path, target_path)
  188. for scons_target in targets:
  189. copy_target(scons_target)
  190. return 0
  191. def generate_sdk_package_versions():
  192. # I can't believe importing files in Python is so convoluted when not
  193. # following the golden standard for packages/modules.
  194. import os
  195. import sys
  196. from os.path import dirname
  197. # We want ../../../methods.py.
  198. script_path = dirname(os.path.abspath(__file__))
  199. root_path = dirname(dirname(dirname(script_path)))
  200. sys.path.insert(0, root_path)
  201. from methods import get_version_info
  202. version_info = get_version_info("")
  203. sys.path.remove(root_path)
  204. version_str = "{major}.{minor}.{patch}".format(**version_info)
  205. version_status = version_info["status"]
  206. if version_status != "stable": # Pre-release
  207. # If version was overridden to be e.g. "beta3", we insert a dot between
  208. # "beta" and "3" to follow SemVer 2.0.
  209. import re
  210. match = re.search(r"[\d]+$", version_status)
  211. if match:
  212. pos = match.start()
  213. version_status = version_status[:pos] + "." + version_status[pos:]
  214. version_str += "-" + version_status
  215. import version
  216. version_defines = (
  217. [
  218. f"GODOT{version.major}",
  219. f"GODOT{version.major}_{version.minor}",
  220. f"GODOT{version.major}_{version.minor}_{version.patch}",
  221. ]
  222. + [f"GODOT{v}_OR_GREATER" for v in range(4, version.major + 1)]
  223. + [f"GODOT{version.major}_{v}_OR_GREATER" for v in range(0, version.minor + 1)]
  224. + [f"GODOT{version.major}_{version.minor}_{v}_OR_GREATER" for v in range(0, version.patch + 1)]
  225. )
  226. props = """<Project>
  227. <PropertyGroup>
  228. <PackageVersion_GodotSharp>{0}</PackageVersion_GodotSharp>
  229. <PackageVersion_Godot_NET_Sdk>{0}</PackageVersion_Godot_NET_Sdk>
  230. <PackageVersion_Godot_SourceGenerators>{0}</PackageVersion_Godot_SourceGenerators>
  231. <GodotVersionConstants>{1}</GodotVersionConstants>
  232. </PropertyGroup>
  233. </Project>
  234. """.format(
  235. version_str, ";".join(version_defines)
  236. )
  237. # We write in ../SdkPackageVersions.props.
  238. with open(os.path.join(dirname(script_path), "SdkPackageVersions.props"), "w", encoding="utf-8", newline="\n") as f:
  239. f.write(props)
  240. # Also write the versioned docs URL to a constant for the Source Generators.
  241. constants = """namespace Godot.SourceGenerators
  242. {{
  243. // TODO: This is currently disabled because of https://github.com/dotnet/roslyn/issues/52904
  244. #pragma warning disable IDE0040 // Add accessibility modifiers.
  245. partial class Common
  246. {{
  247. public const string VersionDocsUrl = "https://docs.godotengine.org/en/{docs_branch}";
  248. }}
  249. }}
  250. """.format(
  251. **version_info
  252. )
  253. generators_dir = os.path.join(
  254. dirname(script_path),
  255. "editor",
  256. "Godot.NET.Sdk",
  257. "Godot.SourceGenerators",
  258. "Generated",
  259. )
  260. os.makedirs(generators_dir, exist_ok=True)
  261. with open(os.path.join(generators_dir, "Common.Constants.cs"), "w", encoding="utf-8", newline="\n") as f:
  262. f.write(constants)
  263. def build_all(msbuild_tool, module_dir, output_dir, godot_platform, dev_debug, push_nupkgs_local, precision):
  264. # Generate SdkPackageVersions.props and VersionDocsUrl constant
  265. generate_sdk_package_versions()
  266. # Godot API
  267. exit_code = build_godot_api(msbuild_tool, module_dir, output_dir, push_nupkgs_local, precision)
  268. if exit_code != 0:
  269. return exit_code
  270. # GodotTools
  271. sln = os.path.join(module_dir, "editor/GodotTools/GodotTools.sln")
  272. args = ["/restore", "/t:Build", "/p:Configuration=" + ("Debug" if dev_debug else "Release")] + (
  273. ["/p:GodotPlatform=" + godot_platform] if godot_platform else []
  274. )
  275. if push_nupkgs_local:
  276. args += ["/p:ClearNuGetLocalCache=true", "/p:PushNuGetToLocalSource=" + push_nupkgs_local]
  277. if precision == "double":
  278. args += ["/p:GodotFloat64=true"]
  279. exit_code = run_msbuild(msbuild_tool, sln=sln, chdir_to=module_dir, msbuild_args=args)
  280. if exit_code != 0:
  281. return exit_code
  282. # Godot.NET.Sdk
  283. args = ["/restore", "/t:Build", "/p:Configuration=Release"]
  284. if push_nupkgs_local:
  285. args += ["/p:ClearNuGetLocalCache=true", "/p:PushNuGetToLocalSource=" + push_nupkgs_local]
  286. if precision == "double":
  287. args += ["/p:GodotFloat64=true"]
  288. sln = os.path.join(module_dir, "editor/Godot.NET.Sdk/Godot.NET.Sdk.sln")
  289. exit_code = run_msbuild(msbuild_tool, sln=sln, chdir_to=module_dir, msbuild_args=args)
  290. if exit_code != 0:
  291. return exit_code
  292. return 0
  293. def main():
  294. import argparse
  295. import sys
  296. parser = argparse.ArgumentParser(description="Builds all Godot .NET solutions")
  297. parser.add_argument("--godot-output-dir", type=str, required=True)
  298. parser.add_argument(
  299. "--dev-debug",
  300. action="store_true",
  301. default=False,
  302. help="Build GodotTools and Godot.NET.Sdk with 'Configuration=Debug'",
  303. )
  304. parser.add_argument("--godot-platform", type=str, default="")
  305. parser.add_argument("--mono-prefix", type=str, default="")
  306. parser.add_argument("--push-nupkgs-local", type=str, default="")
  307. parser.add_argument(
  308. "--precision", type=str, default="single", choices=["single", "double"], help="Floating-point precision level"
  309. )
  310. args = parser.parse_args()
  311. this_script_dir = os.path.dirname(os.path.realpath(__file__))
  312. module_dir = os.path.abspath(os.path.join(this_script_dir, os.pardir))
  313. output_dir = os.path.abspath(args.godot_output_dir)
  314. push_nupkgs_local = os.path.abspath(args.push_nupkgs_local) if args.push_nupkgs_local else None
  315. msbuild_tool = find_any_msbuild_tool(args.mono_prefix)
  316. if msbuild_tool is None:
  317. print("Unable to find MSBuild")
  318. sys.exit(1)
  319. exit_code = build_all(
  320. msbuild_tool,
  321. module_dir,
  322. output_dir,
  323. args.godot_platform,
  324. args.dev_debug,
  325. push_nupkgs_local,
  326. args.precision,
  327. )
  328. sys.exit(exit_code)
  329. if __name__ == "__main__":
  330. main()