123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427 |
- #!/usr/bin/python3
- import os
- import os.path
- import shlex
- import subprocess
- from dataclasses import dataclass
- from typing import Optional, List
- def find_dotnet_cli():
- if os.name == "nt":
- for hint_dir in os.environ["PATH"].split(os.pathsep):
- hint_dir = hint_dir.strip('"')
- hint_path = os.path.join(hint_dir, "dotnet")
- if os.path.isfile(hint_path) and os.access(hint_path, os.X_OK):
- return hint_path
- if os.path.isfile(hint_path + ".exe") and os.access(hint_path + ".exe", os.X_OK):
- return hint_path + ".exe"
- else:
- for hint_dir in os.environ["PATH"].split(os.pathsep):
- hint_dir = hint_dir.strip('"')
- hint_path = os.path.join(hint_dir, "dotnet")
- if os.path.isfile(hint_path) and os.access(hint_path, os.X_OK):
- return hint_path
- def find_msbuild_standalone_windows():
- msbuild_tools_path = find_msbuild_tools_path_reg()
- if msbuild_tools_path:
- return os.path.join(msbuild_tools_path, "MSBuild.exe")
- return None
- def find_msbuild_mono_windows(mono_prefix):
- assert mono_prefix is not None
- mono_bin_dir = os.path.join(mono_prefix, "bin")
- msbuild_mono = os.path.join(mono_bin_dir, "msbuild.bat")
- if os.path.isfile(msbuild_mono):
- return msbuild_mono
- return None
- def find_msbuild_mono_unix():
- import sys
- hint_dirs = []
- if sys.platform == "darwin":
- hint_dirs[:0] = [
- "/Library/Frameworks/Mono.framework/Versions/Current/bin",
- "/usr/local/var/homebrew/linked/mono/bin",
- ]
- for hint_dir in hint_dirs:
- hint_path = os.path.join(hint_dir, "msbuild")
- if os.path.isfile(hint_path):
- return hint_path
- elif os.path.isfile(hint_path + ".exe"):
- return hint_path + ".exe"
- for hint_dir in os.environ["PATH"].split(os.pathsep):
- hint_dir = hint_dir.strip('"')
- hint_path = os.path.join(hint_dir, "msbuild")
- if os.path.isfile(hint_path) and os.access(hint_path, os.X_OK):
- return hint_path
- if os.path.isfile(hint_path + ".exe") and os.access(hint_path + ".exe", os.X_OK):
- return hint_path + ".exe"
- return None
- def find_msbuild_tools_path_reg():
- import subprocess
- program_files = os.getenv("PROGRAMFILES(X86)")
- if not program_files:
- program_files = os.getenv("PROGRAMFILES")
- vswhere = os.path.join(program_files, "Microsoft Visual Studio", "Installer", "vswhere.exe")
- vswhere_args = ["-latest", "-products", "*", "-requires", "Microsoft.Component.MSBuild"]
- try:
- lines = subprocess.check_output([vswhere] + vswhere_args).splitlines()
- for line in lines:
- parts = line.decode("utf-8").split(":", 1)
- if len(parts) < 2 or parts[0] != "installationPath":
- continue
- val = parts[1].strip()
- if not val:
- raise ValueError("Value of `installationPath` entry is empty")
- # Since VS2019, the directory is simply named "Current"
- msbuild_dir = os.path.join(val, "MSBuild", "Current", "Bin")
- if os.path.isdir(msbuild_dir):
- return msbuild_dir
- # Directory name "15.0" is used in VS 2017
- return os.path.join(val, "MSBuild", "15.0", "Bin")
- raise ValueError("Cannot find `installationPath` entry")
- except ValueError as e:
- print("Error reading output from vswhere: " + str(e))
- except OSError:
- pass # Fine, vswhere not found
- except (subprocess.CalledProcessError, OSError):
- pass
- @dataclass
- class ToolsLocation:
- dotnet_cli: str = ""
- msbuild_standalone: str = ""
- msbuild_mono: str = ""
- mono_bin_dir: str = ""
- def find_any_msbuild_tool(mono_prefix):
- # Preference order: dotnet CLI > Standalone MSBuild > Mono's MSBuild
- # Find dotnet CLI
- dotnet_cli = find_dotnet_cli()
- if dotnet_cli:
- return ToolsLocation(dotnet_cli=dotnet_cli)
- # Find standalone MSBuild
- if os.name == "nt":
- msbuild_standalone = find_msbuild_standalone_windows()
- if msbuild_standalone:
- return ToolsLocation(msbuild_standalone=msbuild_standalone)
- if mono_prefix:
- # Find Mono's MSBuild
- if os.name == "nt":
- msbuild_mono = find_msbuild_mono_windows(mono_prefix)
- if msbuild_mono:
- return ToolsLocation(msbuild_mono=msbuild_mono)
- else:
- msbuild_mono = find_msbuild_mono_unix()
- if msbuild_mono:
- return ToolsLocation(msbuild_mono=msbuild_mono)
- return None
- def run_msbuild(tools: ToolsLocation, sln: str, chdir_to: str, msbuild_args: Optional[List[str]] = None):
- using_msbuild_mono = False
- # Preference order: dotnet CLI > Standalone MSBuild > Mono's MSBuild
- if tools.dotnet_cli:
- args = [tools.dotnet_cli, "msbuild"]
- elif tools.msbuild_standalone:
- args = [tools.msbuild_standalone]
- elif tools.msbuild_mono:
- args = [tools.msbuild_mono]
- using_msbuild_mono = True
- else:
- raise RuntimeError("Path to MSBuild or dotnet CLI not provided.")
- args += [sln]
- if msbuild_args:
- args += msbuild_args
- print("Running MSBuild: ", " ".join(shlex.quote(arg) for arg in args), flush=True)
- msbuild_env = os.environ.copy()
- # Needed when running from Developer Command Prompt for VS
- if "PLATFORM" in msbuild_env:
- del msbuild_env["PLATFORM"]
- if using_msbuild_mono:
- # The (Csc/Vbc/Fsc)ToolExe environment variables are required when
- # building with Mono's MSBuild. They must point to the batch files
- # in Mono's bin directory to make sure they are executed with Mono.
- msbuild_env.update(
- {
- "CscToolExe": os.path.join(tools.mono_bin_dir, "csc.bat"),
- "VbcToolExe": os.path.join(tools.mono_bin_dir, "vbc.bat"),
- "FscToolExe": os.path.join(tools.mono_bin_dir, "fsharpc.bat"),
- }
- )
- # We want to control cwd when running msbuild, because that's where the search for global.json begins.
- return subprocess.call(args, env=msbuild_env, cwd=chdir_to)
- def build_godot_api(msbuild_tool, module_dir, output_dir, push_nupkgs_local, precision):
- target_filenames = [
- "GodotSharp.dll",
- "GodotSharp.pdb",
- "GodotSharp.xml",
- "GodotSharpEditor.dll",
- "GodotSharpEditor.pdb",
- "GodotSharpEditor.xml",
- "GodotPlugins.dll",
- "GodotPlugins.pdb",
- "GodotPlugins.runtimeconfig.json",
- ]
- for build_config in ["Debug", "Release"]:
- editor_api_dir = os.path.join(output_dir, "GodotSharp", "Api", build_config)
- targets = [os.path.join(editor_api_dir, filename) for filename in target_filenames]
- args = ["/restore", "/t:Build", "/p:Configuration=" + build_config, "/p:NoWarn=1591"]
- if push_nupkgs_local:
- args += ["/p:ClearNuGetLocalCache=true", "/p:PushNuGetToLocalSource=" + push_nupkgs_local]
- if precision == "double":
- args += ["/p:GodotFloat64=true"]
- sln = os.path.join(module_dir, "glue/GodotSharp/GodotSharp.sln")
- exit_code = run_msbuild(msbuild_tool, sln=sln, chdir_to=module_dir, msbuild_args=args)
- if exit_code != 0:
- return exit_code
- # Copy targets
- core_src_dir = os.path.abspath(os.path.join(sln, os.pardir, "GodotSharp", "bin", build_config))
- editor_src_dir = os.path.abspath(os.path.join(sln, os.pardir, "GodotSharpEditor", "bin", build_config))
- plugins_src_dir = os.path.abspath(os.path.join(sln, os.pardir, "GodotPlugins", "bin", build_config, "net6.0"))
- if not os.path.isdir(editor_api_dir):
- assert not os.path.isfile(editor_api_dir)
- os.makedirs(editor_api_dir)
- def copy_target(target_path):
- from shutil import copy
- filename = os.path.basename(target_path)
- src_path = os.path.join(core_src_dir, filename)
- if not os.path.isfile(src_path):
- src_path = os.path.join(editor_src_dir, filename)
- if not os.path.isfile(src_path):
- src_path = os.path.join(plugins_src_dir, filename)
- print(f"Copying assembly to {target_path}...")
- copy(src_path, target_path)
- for scons_target in targets:
- copy_target(scons_target)
- return 0
- def generate_sdk_package_versions():
- # I can't believe importing files in Python is so convoluted when not
- # following the golden standard for packages/modules.
- import os
- import sys
- from os.path import dirname
- # We want ../../../methods.py.
- script_path = dirname(os.path.abspath(__file__))
- root_path = dirname(dirname(dirname(script_path)))
- sys.path.insert(0, root_path)
- from methods import get_version_info
- version_info = get_version_info("")
- sys.path.remove(root_path)
- version_str = "{major}.{minor}.{patch}".format(**version_info)
- version_status = version_info["status"]
- if version_status != "stable": # Pre-release
- # If version was overridden to be e.g. "beta3", we insert a dot between
- # "beta" and "3" to follow SemVer 2.0.
- import re
- match = re.search(r"[\d]+$", version_status)
- if match:
- pos = match.start()
- version_status = version_status[:pos] + "." + version_status[pos:]
- version_str += "-" + version_status
- import version
- version_defines = (
- [
- f"GODOT{version.major}",
- f"GODOT{version.major}_{version.minor}",
- f"GODOT{version.major}_{version.minor}_{version.patch}",
- ]
- + [f"GODOT{v}_OR_GREATER" for v in range(4, version.major + 1)]
- + [f"GODOT{version.major}_{v}_OR_GREATER" for v in range(0, version.minor + 1)]
- + [f"GODOT{version.major}_{version.minor}_{v}_OR_GREATER" for v in range(0, version.patch + 1)]
- )
- props = """<Project>
- <PropertyGroup>
- <PackageVersion_GodotSharp>{0}</PackageVersion_GodotSharp>
- <PackageVersion_Godot_NET_Sdk>{0}</PackageVersion_Godot_NET_Sdk>
- <PackageVersion_Godot_SourceGenerators>{0}</PackageVersion_Godot_SourceGenerators>
- <GodotVersionConstants>{1}</GodotVersionConstants>
- </PropertyGroup>
- </Project>
- """.format(
- version_str, ";".join(version_defines)
- )
- # We write in ../SdkPackageVersions.props.
- with open(os.path.join(dirname(script_path), "SdkPackageVersions.props"), "w", encoding="utf-8", newline="\n") as f:
- f.write(props)
- # Also write the versioned docs URL to a constant for the Source Generators.
- constants = """namespace Godot.SourceGenerators
- {{
- // TODO: This is currently disabled because of https://github.com/dotnet/roslyn/issues/52904
- #pragma warning disable IDE0040 // Add accessibility modifiers.
- partial class Common
- {{
- public const string VersionDocsUrl = "https://docs.godotengine.org/en/{docs_branch}";
- }}
- }}
- """.format(
- **version_info
- )
- generators_dir = os.path.join(
- dirname(script_path),
- "editor",
- "Godot.NET.Sdk",
- "Godot.SourceGenerators",
- "Generated",
- )
- os.makedirs(generators_dir, exist_ok=True)
- with open(os.path.join(generators_dir, "Common.Constants.cs"), "w", encoding="utf-8", newline="\n") as f:
- f.write(constants)
- def build_all(msbuild_tool, module_dir, output_dir, godot_platform, dev_debug, push_nupkgs_local, precision):
- # Generate SdkPackageVersions.props and VersionDocsUrl constant
- generate_sdk_package_versions()
- # Godot API
- exit_code = build_godot_api(msbuild_tool, module_dir, output_dir, push_nupkgs_local, precision)
- if exit_code != 0:
- return exit_code
- # GodotTools
- sln = os.path.join(module_dir, "editor/GodotTools/GodotTools.sln")
- args = ["/restore", "/t:Build", "/p:Configuration=" + ("Debug" if dev_debug else "Release")] + (
- ["/p:GodotPlatform=" + godot_platform] if godot_platform else []
- )
- if push_nupkgs_local:
- args += ["/p:ClearNuGetLocalCache=true", "/p:PushNuGetToLocalSource=" + push_nupkgs_local]
- if precision == "double":
- args += ["/p:GodotFloat64=true"]
- exit_code = run_msbuild(msbuild_tool, sln=sln, chdir_to=module_dir, msbuild_args=args)
- if exit_code != 0:
- return exit_code
- # Godot.NET.Sdk
- args = ["/restore", "/t:Build", "/p:Configuration=Release"]
- if push_nupkgs_local:
- args += ["/p:ClearNuGetLocalCache=true", "/p:PushNuGetToLocalSource=" + push_nupkgs_local]
- if precision == "double":
- args += ["/p:GodotFloat64=true"]
- sln = os.path.join(module_dir, "editor/Godot.NET.Sdk/Godot.NET.Sdk.sln")
- exit_code = run_msbuild(msbuild_tool, sln=sln, chdir_to=module_dir, msbuild_args=args)
- if exit_code != 0:
- return exit_code
- return 0
- def main():
- import argparse
- import sys
- parser = argparse.ArgumentParser(description="Builds all Godot .NET solutions")
- parser.add_argument("--godot-output-dir", type=str, required=True)
- parser.add_argument(
- "--dev-debug",
- action="store_true",
- default=False,
- help="Build GodotTools and Godot.NET.Sdk with 'Configuration=Debug'",
- )
- parser.add_argument("--godot-platform", type=str, default="")
- parser.add_argument("--mono-prefix", type=str, default="")
- parser.add_argument("--push-nupkgs-local", type=str, default="")
- parser.add_argument(
- "--precision", type=str, default="single", choices=["single", "double"], help="Floating-point precision level"
- )
- args = parser.parse_args()
- this_script_dir = os.path.dirname(os.path.realpath(__file__))
- module_dir = os.path.abspath(os.path.join(this_script_dir, os.pardir))
- output_dir = os.path.abspath(args.godot_output_dir)
- push_nupkgs_local = os.path.abspath(args.push_nupkgs_local) if args.push_nupkgs_local else None
- msbuild_tool = find_any_msbuild_tool(args.mono_prefix)
- if msbuild_tool is None:
- print("Unable to find MSBuild")
- sys.exit(1)
- exit_code = build_all(
- msbuild_tool,
- module_dir,
- output_dir,
- args.godot_platform,
- args.dev_debug,
- push_nupkgs_local,
- args.precision,
- )
- sys.exit(exit_code)
- if __name__ == "__main__":
- main()
|