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