423 lines
14 KiB
Python
Executable File
423 lines
14 KiB
Python
Executable File
#!/usr/bin/python3
|
|
|
|
import os
|
|
import os.path
|
|
import shlex
|
|
import subprocess
|
|
from dataclasses import dataclass
|
|
from typing import List, Optional
|
|
|
|
|
|
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()
|