build_assemblies.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. #!/usr/bin/python3
  2. import os
  3. import os.path
  4. import shlex
  5. import subprocess
  6. from dataclasses import dataclass
  7. def find_dotnet_cli():
  8. if os.name == "nt":
  9. for hint_dir in os.environ["PATH"].split(os.pathsep):
  10. hint_dir = hint_dir.strip('"')
  11. hint_path = os.path.join(hint_dir, "dotnet")
  12. if os.path.isfile(hint_path) and os.access(hint_path, os.X_OK):
  13. return hint_path
  14. if os.path.isfile(hint_path + ".exe") and os.access(hint_path + ".exe", os.X_OK):
  15. return hint_path + ".exe"
  16. else:
  17. for hint_dir in os.environ["PATH"].split(os.pathsep):
  18. hint_dir = hint_dir.strip('"')
  19. hint_path = os.path.join(hint_dir, "dotnet")
  20. if os.path.isfile(hint_path) and os.access(hint_path, os.X_OK):
  21. return hint_path
  22. def find_msbuild_standalone_windows():
  23. msbuild_tools_path = find_msbuild_tools_path_reg()
  24. if msbuild_tools_path:
  25. return os.path.join(msbuild_tools_path, "MSBuild.exe")
  26. return None
  27. def find_msbuild_mono_windows(mono_prefix):
  28. assert mono_prefix is not None
  29. mono_bin_dir = os.path.join(mono_prefix, "bin")
  30. msbuild_mono = os.path.join(mono_bin_dir, "msbuild.bat")
  31. if os.path.isfile(msbuild_mono):
  32. return msbuild_mono
  33. return None
  34. def find_msbuild_mono_unix():
  35. import sys
  36. hint_dirs = []
  37. if sys.platform == "darwin":
  38. hint_dirs[:0] = [
  39. "/Library/Frameworks/Mono.framework/Versions/Current/bin",
  40. "/usr/local/var/homebrew/linked/mono/bin",
  41. ]
  42. for hint_dir in hint_dirs:
  43. hint_path = os.path.join(hint_dir, "msbuild")
  44. if os.path.isfile(hint_path):
  45. return hint_path
  46. elif os.path.isfile(hint_path + ".exe"):
  47. return hint_path + ".exe"
  48. for hint_dir in os.environ["PATH"].split(os.pathsep):
  49. hint_dir = hint_dir.strip('"')
  50. hint_path = os.path.join(hint_dir, "msbuild")
  51. if os.path.isfile(hint_path) and os.access(hint_path, os.X_OK):
  52. return hint_path
  53. if os.path.isfile(hint_path + ".exe") and os.access(hint_path + ".exe", os.X_OK):
  54. return hint_path + ".exe"
  55. return None
  56. def find_msbuild_tools_path_reg():
  57. import subprocess
  58. program_files = os.getenv("PROGRAMFILES(X86)")
  59. if not program_files:
  60. program_files = os.getenv("PROGRAMFILES")
  61. vswhere = os.path.join(program_files, "Microsoft Visual Studio", "Installer", "vswhere.exe")
  62. vswhere_args = ["-latest", "-products", "*", "-requires", "Microsoft.Component.MSBuild"]
  63. try:
  64. lines = subprocess.check_output([vswhere] + vswhere_args).splitlines()
  65. for line in lines:
  66. parts = line.decode("utf-8").split(":", 1)
  67. if len(parts) < 2 or parts[0] != "installationPath":
  68. continue
  69. val = parts[1].strip()
  70. if not val:
  71. raise ValueError("Value of `installationPath` entry is empty")
  72. # Since VS2019, the directory is simply named "Current"
  73. msbuild_dir = os.path.join(val, "MSBuild", "Current", "Bin")
  74. if os.path.isdir(msbuild_dir):
  75. return msbuild_dir
  76. # Directory name "15.0" is used in VS 2017
  77. return os.path.join(val, "MSBuild", "15.0", "Bin")
  78. raise ValueError("Cannot find `installationPath` entry")
  79. except ValueError as e:
  80. print("Error reading output from vswhere: " + str(e))
  81. except OSError:
  82. pass # Fine, vswhere not found
  83. except (subprocess.CalledProcessError, OSError):
  84. pass
  85. @dataclass
  86. class ToolsLocation:
  87. dotnet_cli: str = ""
  88. msbuild_standalone: str = ""
  89. msbuild_mono: str = ""
  90. mono_bin_dir: str = ""
  91. def find_any_msbuild_tool(mono_prefix):
  92. # Preference order: dotnet CLI > Standalone MSBuild > Mono's MSBuild
  93. # Find dotnet CLI
  94. dotnet_cli = find_dotnet_cli()
  95. if dotnet_cli:
  96. return ToolsLocation(dotnet_cli=dotnet_cli)
  97. # Find standalone MSBuild
  98. if os.name == "nt":
  99. msbuild_standalone = find_msbuild_standalone_windows()
  100. if msbuild_standalone:
  101. return ToolsLocation(msbuild_standalone=msbuild_standalone)
  102. if mono_prefix:
  103. # Find Mono's MSBuild
  104. if os.name == "nt":
  105. msbuild_mono = find_msbuild_mono_windows(mono_prefix)
  106. if msbuild_mono:
  107. return ToolsLocation(msbuild_mono=msbuild_mono)
  108. else:
  109. msbuild_mono = find_msbuild_mono_unix()
  110. if msbuild_mono:
  111. return ToolsLocation(msbuild_mono=msbuild_mono)
  112. return None
  113. def run_msbuild(tools: ToolsLocation, sln: str, msbuild_args: [str] = None):
  114. if msbuild_args is None:
  115. msbuild_args = []
  116. using_msbuild_mono = False
  117. # Preference order: dotnet CLI > Standalone MSBuild > Mono's MSBuild
  118. if tools.dotnet_cli:
  119. args = [tools.dotnet_cli, "msbuild"]
  120. elif tools.msbuild_standalone:
  121. args = [tools.msbuild_standalone]
  122. elif tools.msbuild_mono:
  123. args = [tools.msbuild_mono]
  124. using_msbuild_mono = True
  125. else:
  126. raise RuntimeError("Path to MSBuild or dotnet CLI not provided.")
  127. args += [sln]
  128. if len(msbuild_args) > 0:
  129. args += msbuild_args
  130. print("Running MSBuild: ", " ".join(shlex.quote(arg) for arg in args), flush=True)
  131. msbuild_env = os.environ.copy()
  132. # Needed when running from Developer Command Prompt for VS
  133. if "PLATFORM" in msbuild_env:
  134. del msbuild_env["PLATFORM"]
  135. if using_msbuild_mono:
  136. # The (Csc/Vbc/Fsc)ToolExe environment variables are required when
  137. # building with Mono's MSBuild. They must point to the batch files
  138. # in Mono's bin directory to make sure they are executed with Mono.
  139. msbuild_env.update(
  140. {
  141. "CscToolExe": os.path.join(tools.mono_bin_dir, "csc.bat"),
  142. "VbcToolExe": os.path.join(tools.mono_bin_dir, "vbc.bat"),
  143. "FscToolExe": os.path.join(tools.mono_bin_dir, "fsharpc.bat"),
  144. }
  145. )
  146. return subprocess.call(args, env=msbuild_env)
  147. def build_godot_api(msbuild_tool, module_dir, output_dir, push_nupkgs_local, float_size):
  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 float_size == "64":
  166. args += ["/p:GodotFloat64=true"]
  167. sln = os.path.join(module_dir, "glue/GodotSharp/GodotSharp.sln")
  168. exit_code = run_msbuild(
  169. msbuild_tool,
  170. sln=sln,
  171. msbuild_args=args,
  172. )
  173. if exit_code != 0:
  174. return exit_code
  175. # Copy targets
  176. core_src_dir = os.path.abspath(os.path.join(sln, os.pardir, "GodotSharp", "bin", build_config))
  177. editor_src_dir = os.path.abspath(os.path.join(sln, os.pardir, "GodotSharpEditor", "bin", build_config))
  178. plugins_src_dir = os.path.abspath(os.path.join(sln, os.pardir, "GodotPlugins", "bin", build_config, "net6.0"))
  179. if not os.path.isdir(editor_api_dir):
  180. assert not os.path.isfile(editor_api_dir)
  181. os.makedirs(editor_api_dir)
  182. def copy_target(target_path):
  183. from shutil import copy
  184. filename = os.path.basename(target_path)
  185. src_path = os.path.join(core_src_dir, filename)
  186. if not os.path.isfile(src_path):
  187. src_path = os.path.join(editor_src_dir, filename)
  188. if not os.path.isfile(src_path):
  189. src_path = os.path.join(plugins_src_dir, filename)
  190. print(f"Copying assembly to {target_path}...")
  191. copy(src_path, target_path)
  192. for scons_target in targets:
  193. copy_target(scons_target)
  194. return 0
  195. def build_all(msbuild_tool, module_dir, output_dir, godot_platform, dev_debug, push_nupkgs_local, float_size):
  196. # Godot API
  197. exit_code = build_godot_api(msbuild_tool, module_dir, output_dir, push_nupkgs_local, float_size)
  198. if exit_code != 0:
  199. return exit_code
  200. # GodotTools
  201. sln = os.path.join(module_dir, "editor/GodotTools/GodotTools.sln")
  202. args = ["/restore", "/t:Build", "/p:Configuration=" + ("Debug" if dev_debug else "Release")] + (
  203. ["/p:GodotPlatform=" + godot_platform] if godot_platform else []
  204. )
  205. if push_nupkgs_local:
  206. args += ["/p:ClearNuGetLocalCache=true", "/p:PushNuGetToLocalSource=" + push_nupkgs_local]
  207. if float_size == "64":
  208. args += ["/p:GodotFloat64=true"]
  209. exit_code = run_msbuild(msbuild_tool, sln=sln, msbuild_args=args)
  210. if exit_code != 0:
  211. return exit_code
  212. # Godot.NET.Sdk
  213. args = ["/restore", "/t:Build", "/p:Configuration=Release"]
  214. if push_nupkgs_local:
  215. args += ["/p:ClearNuGetLocalCache=true", "/p:PushNuGetToLocalSource=" + push_nupkgs_local]
  216. if float_size == "64":
  217. args += ["/p:GodotFloat64=true"]
  218. sln = os.path.join(module_dir, "editor/Godot.NET.Sdk/Godot.NET.Sdk.sln")
  219. exit_code = run_msbuild(msbuild_tool, sln=sln, msbuild_args=args)
  220. if exit_code != 0:
  221. return exit_code
  222. return 0
  223. def main():
  224. import argparse
  225. import sys
  226. parser = argparse.ArgumentParser(description="Builds all Godot .NET solutions")
  227. parser.add_argument("--godot-output-dir", type=str, required=True)
  228. parser.add_argument(
  229. "--dev-debug",
  230. action="store_true",
  231. default=False,
  232. help="Build GodotTools and Godot.NET.Sdk with 'Configuration=Debug'",
  233. )
  234. parser.add_argument("--godot-platform", type=str, default="")
  235. parser.add_argument("--mono-prefix", type=str, default="")
  236. parser.add_argument("--push-nupkgs-local", type=str, default="")
  237. parser.add_argument("--float", type=str, default="32", choices=["32", "64"], help="Floating-point precision")
  238. args = parser.parse_args()
  239. this_script_dir = os.path.dirname(os.path.realpath(__file__))
  240. module_dir = os.path.abspath(os.path.join(this_script_dir, os.pardir))
  241. output_dir = os.path.abspath(args.godot_output_dir)
  242. push_nupkgs_local = os.path.abspath(args.push_nupkgs_local) if args.push_nupkgs_local else None
  243. msbuild_tool = find_any_msbuild_tool(args.mono_prefix)
  244. if msbuild_tool is None:
  245. print("Unable to find MSBuild")
  246. sys.exit(1)
  247. exit_code = build_all(
  248. msbuild_tool,
  249. module_dir,
  250. output_dir,
  251. args.godot_platform,
  252. args.dev_debug,
  253. push_nupkgs_local,
  254. args.float,
  255. )
  256. sys.exit(exit_code)
  257. if __name__ == "__main__":
  258. main()