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