Browse Source

Merge pull request #86255 from bruvzg/_bundle_build

[iOS/macOS] Add option to automatically build (and sign / archive) bundles.
Rémi Verschelde 1 year ago
parent
commit
de77f0ac7f

+ 0 - 1
SConstruct

@@ -182,7 +182,6 @@ opts.Add(BoolVariable("debug_symbols", "Build with debugging symbols", False))
 opts.Add(BoolVariable("separate_debug_symbols", "Extract debugging symbols to a separate file", False))
 opts.Add(BoolVariable("separate_debug_symbols", "Extract debugging symbols to a separate file", False))
 opts.Add(EnumVariable("lto", "Link-time optimization (production builds)", "none", ("none", "auto", "thin", "full")))
 opts.Add(EnumVariable("lto", "Link-time optimization (production builds)", "none", ("none", "auto", "thin", "full")))
 opts.Add(BoolVariable("production", "Set defaults to build Godot for use in production", False))
 opts.Add(BoolVariable("production", "Set defaults to build Godot for use in production", False))
-opts.Add(BoolVariable("generate_apk", "Generate an APK/AAB after building Android library by calling Gradle", False))
 opts.Add(BoolVariable("threads", "Enable threading support", True))
 opts.Add(BoolVariable("threads", "Enable threading support", True))
 
 
 # Components
 # Components

+ 22 - 0
misc/dist/macos/editor_debug.entitlements

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>com.apple.security.cs.allow-dyld-environment-variables</key>
+ <true/>
+ <key>com.apple.security.cs.allow-jit</key>
+ <true/>
+ <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
+ <true/>
+ <key>com.apple.security.cs.disable-executable-page-protection</key>
+ <true/>
+ <key>com.apple.security.cs.disable-library-validation</key>
+ <true/>
+ <key>com.apple.security.device.audio-input</key>
+ <true/>
+ <key>com.apple.security.device.camera</key>
+ <true/>
+ <key>com.apple.security.get-task-allow</key>
+ <true/>
+</dict>
+</plist>

+ 199 - 0
misc/dist/macos/editor_info_plist.template

@@ -0,0 +1,199 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>English</string>
+	<key>CFBundleExecutable</key>
+	<string>Godot</string>
+	<key>CFBundleName</key>
+	<string>Godot</string>
+	<key>CFBundleIconFile</key>
+	<string>Godot.icns</string>
+	<key>CFBundleIdentifier</key>
+	<string>org.godotengine.godot</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>$short_version</string>
+	<key>CFBundleSignature</key>
+	<string>godot</string>
+	<key>CFBundleVersion</key>
+	<string>$version</string>
+	<key>NSMicrophoneUsageDescription</key>
+	<string>Microphone access is required to capture audio.</string>
+	<key>NSCameraUsageDescription</key>
+	<string>Camera access is required to capture video.</string>
+	<key>NSRequiresAquaSystemAppearance</key>
+	<false/>
+	<key>NSHumanReadableCopyright</key>
+	<string>© 2007-present Juan Linietsky, Ariel Manzur &amp; Godot Engine contributors</string>
+	<key>CFBundleSupportedPlatforms</key>
+	<array>
+		<string>MacOSX</string>
+	</array>
+	<key>NSPrincipalClass</key>
+	<string>NSApplication</string>
+	<key>LSApplicationCategoryType</key>
+	<string>public.app-category.developer-tools</string>
+	<key>LSMinimumSystemVersion</key>
+	<string>10.12</string>
+	<key>LSMinimumSystemVersionByArchitecture</key>
+	<dict>
+		<key>x86_64</key>
+		<string>10.12</string>
+	</dict>
+	<key>NSHighResolutionCapable</key>
+	<true/>
+	<key>CFBundleDocumentTypes</key>
+	<array>
+		<dict>
+			<key>CFBundleTypeRole</key>
+			<string>Editor</string>
+			<key>LSItemContentTypes</key>
+			<array>
+				<string>public.tscn</string>
+			</array>
+			<key>NSExportableTypes</key>
+			<array>
+				<string>public.tscn</string>
+			</array>
+		</dict>
+		<dict>
+			<key>CFBundleTypeRole</key>
+			<string>Editor</string>
+			<key>LSItemContentTypes</key>
+			<array>
+				<string>public.godot</string>
+			</array>
+			<key>NSExportableTypes</key>
+			<array>
+				<string>public.godot</string>
+			</array>
+		</dict>
+	</array>
+	<key>UTExportedTypeDeclarations</key>
+	<array>
+		<dict>
+			<key>UTTypeIdentifier</key>
+			<string>public.tscn</string>
+			<key>UTTypeReferenceURL</key>
+			<string></string>
+			<key>UTTypeDescription</key>
+			<string>Godot Engine scene</string>
+			<key>UTTypeIconFile</key>
+			<string>Scene.icns</string>
+			<key>UTTypeConformsTo</key>
+			<array>
+				<string>public.data</string>
+			</array>
+			<key>UTTypeTagSpecification</key>
+			<dict>
+				<key>public.filename-extension</key>
+				<array>
+					<string>scn</string>
+					<string>tscn</string>
+					<string>escn</string>
+				</array>
+				<key>public.mime-type</key>
+				<string>application/x-godot-scene</string>
+			</dict>
+		</dict>
+		<dict>
+			<key>UTTypeIdentifier</key>
+			<string>public.gd</string>
+			<key>UTTypeReferenceURL</key>
+			<string></string>
+			<key>UTTypeDescription</key>
+			<string>GDScript script</string>
+			<key>UTTypeIconFile</key>
+			<string>GDScript.icns</string>
+			<key>UTTypeConformsTo</key>
+			<array>
+				<string>public.script</string>
+			</array>
+			<key>UTTypeTagSpecification</key>
+			<dict>
+				<key>public.filename-extension</key>
+				<array>
+					<string>gd</string>
+				</array>
+				<key>public.mime-type</key>
+				<string>application/x-gdscript</string>
+			</dict>
+		</dict>
+		<dict>
+			<key>UTTypeIdentifier</key>
+			<string>public.res</string>
+			<key>UTTypeReferenceURL</key>
+			<string></string>
+			<key>UTTypeDescription</key>
+			<string>Godot Engine resource</string>
+			<key>UTTypeIconFile</key>
+			<string>Resource.icns</string>
+			<key>UTTypeConformsTo</key>
+			<array>
+				<string>public.data</string>
+			</array>
+			<key>UTTypeTagSpecification</key>
+			<dict>
+				<key>public.filename-extension</key>
+				<array>
+					<string>res</string>
+					<string>tres</string>
+				</array>
+				<key>public.mime-type</key>
+				<string>application/x-godot-resource</string>
+			</dict>
+		</dict>
+		<dict>
+			<key>UTTypeIdentifier</key>
+			<string>public.gdshader</string>
+			<key>UTTypeReferenceURL</key>
+			<string></string>
+			<key>UTTypeDescription</key>
+			<string>Godot Engine shader</string>
+			<key>UTTypeIconFile</key>
+			<string>Shader.icns</string>
+			<key>UTTypeConformsTo</key>
+			<array>
+				<string>public.script</string>
+			</array>
+			<key>UTTypeTagSpecification</key>
+			<dict>
+				<key>public.filename-extension</key>
+				<array>
+					<string>gdshader</string>
+				</array>
+				<key>public.mime-type</key>
+				<string>application/x-godot-shader</string>
+			</dict>
+		</dict>
+		<dict>
+			<key>UTTypeIdentifier</key>
+			<string>public.godot</string>
+			<key>UTTypeReferenceURL</key>
+			<string></string>
+			<key>UTTypeDescription</key>
+			<string>Godot Engine project</string>
+			<key>UTTypeIconFile</key>
+			<string>Project.icns</string>
+			<key>UTTypeConformsTo</key>
+			<array>
+				<string>public.data</string>
+			</array>
+			<key>UTTypeTagSpecification</key>
+			<dict>
+				<key>public.filename-extension</key>
+				<array>
+					<string>godot</string>
+				</array>
+				<key>public.mime-type</key>
+				<string>application/x-godot-project</string>
+			</dict>
+		</dict>
+	</array>
+</dict>
+</plist>

+ 1 - 0
platform/android/detect.py

@@ -28,6 +28,7 @@ def get_opts():
             "android-" + str(get_min_target_api()),
             "android-" + str(get_min_target_api()),
         ),
         ),
         BoolVariable("store_release", "Editor build for Google Play Store (for official builds only)", False),
         BoolVariable("store_release", "Editor build for Google Play Store (for official builds only)", False),
+        BoolVariable("generate_apk", "Generate an APK/AAB after building Android library by calling Gradle", False),
     ]
     ]
 
 
 
 

+ 61 - 0
platform/ios/SCsub

@@ -2,6 +2,62 @@
 
 
 Import("env")
 Import("env")
 
 
+import os, json
+from platform_methods import run_in_subprocess, architectures, lipo, get_build_version, detect_mvk
+import subprocess
+import shutil
+
+
+def generate_bundle(target, source, env):
+    bin_dir = Dir("#bin").abspath
+
+    # Template bundle.
+    app_prefix = "godot." + env["platform"]
+    rel_prefix = "libgodot." + env["platform"] + "." + "template_release"
+    dbg_prefix = "libgodot." + env["platform"] + "." + "template_debug"
+    if env.dev_build:
+        app_prefix += ".dev"
+        rel_prefix += ".dev"
+        dbg_prefix += ".dev"
+    if env["precision"] == "double":
+        app_prefix += ".double"
+        rel_prefix += ".double"
+        dbg_prefix += ".double"
+
+    # Lipo template libraries.
+    rel_target_bin = lipo(bin_dir + "/" + rel_prefix, env.extra_suffix + ".a")
+    dbg_target_bin = lipo(bin_dir + "/" + dbg_prefix, env.extra_suffix + ".a")
+    rel_target_bin_sim = lipo(bin_dir + "/" + rel_prefix, ".simulator" + env.extra_suffix + ".a")
+    dbg_target_bin_sim = lipo(bin_dir + "/" + dbg_prefix, ".simulator" + env.extra_suffix + ".a")
+
+    # Assemble Xcode project bundle.
+    app_dir = Dir("#bin/ios_xcode").abspath
+    templ = Dir("#misc/dist/ios_xcode").abspath
+    if os.path.exists(app_dir):
+        shutil.rmtree(app_dir)
+    shutil.copytree(templ, app_dir)
+    if rel_target_bin != "":
+        shutil.copy(rel_target_bin, app_dir + "/libgodot.ios.release.xcframework/ios-arm64/libgodot.a")
+    if dbg_target_bin != "":
+        shutil.copy(dbg_target_bin, app_dir + "/libgodot.ios.debug.xcframework/ios-arm64/libgodot.a")
+    if rel_target_bin_sim != "":
+        shutil.copy(
+            rel_target_bin_sim, app_dir + "/libgodot.ios.release.xcframework/ios-arm64_x86_64-simulator/libgodot.a"
+        )
+    if dbg_target_bin_sim != "":
+        shutil.copy(
+            dbg_target_bin_sim, app_dir + "/libgodot.ios.debug.xcframework/ios-arm64_x86_64-simulator/libgodot.a"
+        )
+    mvk_path = detect_mvk(env, "ios-arm64")
+    if mvk_path != "":
+        shutil.copytree(mvk_path, app_dir + "/MoltenVK.xcframework")
+
+    # ZIP Xcode project bundle.
+    zip_dir = Dir("#bin/" + (app_prefix + env.extra_suffix).replace(".", "_")).abspath
+    shutil.make_archive(zip_dir, "zip", root_dir=app_dir)
+    shutil.rmtree(app_dir)
+
+
 ios_lib = [
 ios_lib = [
     "godot_ios.mm",
     "godot_ios.mm",
     "os_ios.mm",
     "os_ios.mm",
@@ -42,3 +98,8 @@ def combine_libs(target=None, source=None, env=None):
 
 
 
 
 combine_command = env_ios.Command("#bin/libgodot" + env_ios["LIBSUFFIX"], [ios_lib] + env_ios["LIBS"], combine_libs)
 combine_command = env_ios.Command("#bin/libgodot" + env_ios["LIBSUFFIX"], [ios_lib] + env_ios["LIBS"], combine_libs)
+
+if env["generate_bundle"]:
+    generate_bundle_command = env.Command("generate_bundle", [], generate_bundle)
+    command = env.AlwaysBuild(generate_bundle_command)
+    env.Depends(command, [combine_command])

+ 2 - 0
platform/ios/detect.py

@@ -23,6 +23,7 @@ def get_opts():
     from SCons.Variables import BoolVariable
     from SCons.Variables import BoolVariable
 
 
     return [
     return [
+        ("vulkan_sdk_path", "Path to the Vulkan SDK", ""),
         (
         (
             "IOS_TOOLCHAIN_PATH",
             "IOS_TOOLCHAIN_PATH",
             "Path to iOS toolchain",
             "Path to iOS toolchain",
@@ -31,6 +32,7 @@ def get_opts():
         ("IOS_SDK_PATH", "Path to the iOS SDK", ""),
         ("IOS_SDK_PATH", "Path to the iOS SDK", ""),
         BoolVariable("ios_simulator", "Build for iOS Simulator", False),
         BoolVariable("ios_simulator", "Build for iOS Simulator", False),
         ("ios_triple", "Triple for ios toolchain", ""),
         ("ios_triple", "Triple for ios toolchain", ""),
+        BoolVariable("generate_bundle", "Generate an APP bundle after building iOS/macOS binaries", False),
     ]
     ]
 
 
 
 

+ 97 - 1
platform/macos/SCsub

@@ -2,8 +2,99 @@
 
 
 Import("env")
 Import("env")
 
 
-from platform_methods import run_in_subprocess
+import os, json
+from platform_methods import run_in_subprocess, architectures, lipo, get_build_version
 import platform_macos_builders
 import platform_macos_builders
+import subprocess
+import shutil
+
+
+def generate_bundle(target, source, env):
+    bin_dir = Dir("#bin").abspath
+
+    if env.editor_build:
+        # Editor bundle.
+        prefix = "godot." + env["platform"] + "." + env["target"]
+        if env.dev_build:
+            prefix += ".dev"
+        if env["precision"] == "double":
+            prefix += ".double"
+
+        # Lipo editor executable.
+        target_bin = lipo(bin_dir + "/" + prefix, env.extra_suffix)
+
+        # Assemble .app bundle and update version info.
+        app_dir = Dir("#bin/" + (prefix + env.extra_suffix).replace(".", "_") + ".app").abspath
+        templ = Dir("#misc/dist/macos_tools.app").abspath
+        if os.path.exists(app_dir):
+            shutil.rmtree(app_dir)
+        shutil.copytree(templ, app_dir, ignore=shutil.ignore_patterns("Contents/Info.plist"))
+        if not os.path.isdir(app_dir + "/Contents/MacOS"):
+            os.mkdir(app_dir + "/Contents/MacOS")
+        if target_bin != "":
+            shutil.copy(target_bin, app_dir + "/Contents/MacOS/Godot")
+        version = get_build_version(False)
+        short_version = get_build_version(True)
+        with open(Dir("#misc/dist/macos").abspath + "/editor_info_plist.template", "rt") as fin:
+            with open(app_dir + "/Contents/Info.plist", "wt") as fout:
+                for line in fin:
+                    line = line.replace("$version", version)
+                    line = line.replace("$short_version", short_version)
+                    fout.write(line)
+
+        # Sign .app bundle.
+        if env["bundle_sign_identity"] != "":
+            sign_command = [
+                "codesign",
+                "-s",
+                env["bundle_sign_identity"],
+                "--deep",
+                "--force",
+                "--options=runtime",
+                "--entitlements",
+            ]
+            if env.dev_build:
+                sign_command += [Dir("#misc/dist/macos").abspath + "/editor_debug.entitlements"]
+            else:
+                sign_command += [Dir("#misc/dist/macos").abspath + "/editor.entitlements"]
+            sign_command += [app_dir]
+            subprocess.run(sign_command)
+    else:
+        # Template bundle.
+        app_prefix = "godot." + env["platform"]
+        rel_prefix = "godot." + env["platform"] + "." + "template_release"
+        dbg_prefix = "godot." + env["platform"] + "." + "template_debug"
+        if env.dev_build:
+            app_prefix += ".dev"
+            rel_prefix += ".dev"
+            dbg_prefix += ".dev"
+        if env["precision"] == "double":
+            app_prefix += ".double"
+            rel_prefix += ".double"
+            dbg_prefix += ".double"
+
+        # Lipo template executables.
+        rel_target_bin = lipo(bin_dir + "/" + rel_prefix, env.extra_suffix)
+        dbg_target_bin = lipo(bin_dir + "/" + dbg_prefix, env.extra_suffix)
+
+        # Assemble .app bundle.
+        app_dir = Dir("#bin/macos_template.app").abspath
+        templ = Dir("#misc/dist/macos_template.app").abspath
+        if os.path.exists(app_dir):
+            shutil.rmtree(app_dir)
+        shutil.copytree(templ, app_dir)
+        if not os.path.isdir(app_dir + "/Contents/MacOS"):
+            os.mkdir(app_dir + "/Contents/MacOS")
+        if rel_target_bin != "":
+            shutil.copy(rel_target_bin, app_dir + "/Contents/MacOS/godot_macos_release.universal")
+        if dbg_target_bin != "":
+            shutil.copy(dbg_target_bin, app_dir + "/Contents/MacOS/godot_macos_debug.universal")
+
+        # ZIP .app bundle.
+        zip_dir = Dir("#bin/" + (app_prefix + env.extra_suffix).replace(".", "_")).abspath
+        shutil.make_archive(zip_dir, "zip", root_dir=bin_dir, base_dir="macos_template.app")
+        shutil.rmtree(app_dir)
+
 
 
 files = [
 files = [
     "os_macos.mm",
     "os_macos.mm",
@@ -34,3 +125,8 @@ prog = env.add_program("#bin/godot", files)
 
 
 if env["debug_symbols"] and env["separate_debug_symbols"]:
 if env["debug_symbols"] and env["separate_debug_symbols"]:
     env.AddPostAction(prog, run_in_subprocess(platform_macos_builders.make_debug_macos))
     env.AddPostAction(prog, run_in_subprocess(platform_macos_builders.make_debug_macos))
+
+if env["generate_bundle"]:
+    generate_bundle_command = env.Command("generate_bundle", [], generate_bundle)
+    command = env.AlwaysBuild(generate_bundle_command)
+    env.Depends(command, [prog])

+ 11 - 67
platform/macos/detect.py

@@ -1,7 +1,7 @@
 import os
 import os
 import sys
 import sys
 from methods import detect_darwin_sdk_path, get_compiler_version, is_vanilla_clang
 from methods import detect_darwin_sdk_path, get_compiler_version, is_vanilla_clang
-from platform_methods import detect_arch
+from platform_methods import detect_arch, detect_mvk
 
 
 from typing import TYPE_CHECKING
 from typing import TYPE_CHECKING
 
 
@@ -33,6 +33,12 @@ def get_opts():
         BoolVariable("use_tsan", "Use LLVM/GCC compiler thread sanitizer (TSAN)", False),
         BoolVariable("use_tsan", "Use LLVM/GCC compiler thread sanitizer (TSAN)", False),
         BoolVariable("use_coverage", "Use instrumentation codes in the binary (e.g. for code coverage)", False),
         BoolVariable("use_coverage", "Use instrumentation codes in the binary (e.g. for code coverage)", False),
         ("angle_libs", "Path to the ANGLE static libraries", ""),
         ("angle_libs", "Path to the ANGLE static libraries", ""),
+        (
+            "bundle_sign_identity",
+            "The 'Full Name', 'Common Name' or SHA-1 hash of the signing identity used to sign editor .app bundle.",
+            "-",
+        ),
+        BoolVariable("generate_bundle", "Generate an APP bundle after building iOS/macOS binaries", False),
     ]
     ]
 
 
 
 
@@ -53,47 +59,6 @@ def get_flags():
     ]
     ]
 
 
 
 
-def get_mvk_sdk_path():
-    def int_or_zero(i):
-        try:
-            return int(i)
-        except:
-            return 0
-
-    def ver_parse(a):
-        return [int_or_zero(i) for i in a.split(".")]
-
-    dirname = os.path.expanduser("~/VulkanSDK")
-    if not os.path.exists(dirname):
-        return ""
-
-    ver_min = ver_parse("1.3.231.0")
-    ver_num = ver_parse("0.0.0.0")
-    files = os.listdir(dirname)
-    lib_name_out = dirname
-    for file in files:
-        if os.path.isdir(os.path.join(dirname, file)):
-            ver_comp = ver_parse(file)
-            if ver_comp > ver_num and ver_comp >= ver_min:
-                # Try new SDK location.
-                lib_name = os.path.join(
-                    os.path.join(dirname, file), "macOS/lib/MoltenVK.xcframework/macos-arm64_x86_64/"
-                )
-                if os.path.isfile(os.path.join(lib_name, "libMoltenVK.a")):
-                    ver_num = ver_comp
-                    lib_name_out = lib_name
-                else:
-                    # Try old SDK location.
-                    lib_name = os.path.join(
-                        os.path.join(dirname, file), "MoltenVK/MoltenVK.xcframework/macos-arm64_x86_64/"
-                    )
-                    if os.path.isfile(os.path.join(lib_name, "libMoltenVK.a")):
-                        ver_num = ver_comp
-                        lib_name_out = lib_name
-
-    return lib_name_out
-
-
 def configure(env: "Environment"):
 def configure(env: "Environment"):
     # Validate arch.
     # Validate arch.
     supported_arches = ["x86_64", "arm64"]
     supported_arches = ["x86_64", "arm64"]
@@ -274,32 +239,11 @@ def configure(env: "Environment"):
         env.Append(LINKFLAGS=["-framework", "Metal", "-framework", "IOSurface"])
         env.Append(LINKFLAGS=["-framework", "Metal", "-framework", "IOSurface"])
         if not env["use_volk"]:
         if not env["use_volk"]:
             env.Append(LINKFLAGS=["-lMoltenVK"])
             env.Append(LINKFLAGS=["-lMoltenVK"])
-            mvk_found = False
-
-            mvk_list = [get_mvk_sdk_path(), "/opt/homebrew/lib", "/usr/local/homebrew/lib", "/opt/local/lib"]
-            if env["vulkan_sdk_path"] != "":
-                mvk_list.insert(0, os.path.expanduser(env["vulkan_sdk_path"]))
-                mvk_list.insert(
-                    0,
-                    os.path.join(
-                        os.path.expanduser(env["vulkan_sdk_path"]), "macOS/lib/MoltenVK.xcframework/macos-arm64_x86_64/"
-                    ),
-                )
-                mvk_list.insert(
-                    0,
-                    os.path.join(
-                        os.path.expanduser(env["vulkan_sdk_path"]), "MoltenVK/MoltenVK.xcframework/macos-arm64_x86_64/"
-                    ),
-                )
-
-            for mvk_path in mvk_list:
-                if mvk_path and os.path.isfile(os.path.join(mvk_path, "libMoltenVK.a")):
-                    mvk_found = True
-                    print("MoltenVK found at: " + mvk_path)
-                    env.Append(LINKFLAGS=["-L" + mvk_path])
-                    break
+            mvk_path = detect_mvk(env, "macos-arm64_x86_64")
 
 
-            if not mvk_found:
+            if mvk_path != "":
+                env.Append(LINKFLAGS=["-L" + os.path.join(mvk_path, "macos-arm64_x86_64")])
+            else:
                 print(
                 print(
                     "MoltenVK SDK installation directory not found, use 'vulkan_sdk_path' SCons parameter to specify SDK path."
                     "MoltenVK SDK installation directory not found, use 'vulkan_sdk_path' SCons parameter to specify SDK path."
                 )
                 )

+ 105 - 0
platform_methods.py

@@ -140,3 +140,108 @@ def generate_export_icons(platform_path, platform_name):
         wf = export_path + "/" + name + "_svg.gen.h"
         wf = export_path + "/" + name + "_svg.gen.h"
         with open(wf, "w") as svgw:
         with open(wf, "w") as svgw:
             svgw.write(svg_str)
             svgw.write(svg_str)
+
+
+def get_build_version(short):
+    import version
+
+    name = "custom_build"
+    if os.getenv("BUILD_NAME") != None:
+        name = os.getenv("BUILD_NAME")
+    v = "%d.%d" % (version.major, version.minor)
+    if version.patch > 0:
+        v += ".%d" % version.patch
+    status = version.status
+    if not short:
+        if os.getenv("GODOT_VERSION_STATUS") != None:
+            status = str(os.getenv("GODOT_VERSION_STATUS"))
+        v += ".%s.%s" % (status, name)
+    return v
+
+
+def lipo(prefix, suffix):
+    from pathlib import Path
+
+    target_bin = ""
+    lipo_command = ["lipo", "-create"]
+    arch_found = 0
+
+    for arch in architectures:
+        bin_name = prefix + "." + arch + suffix
+        if Path(bin_name).is_file():
+            target_bin = bin_name
+            lipo_command += [bin_name]
+            arch_found += 1
+
+    if arch_found > 1:
+        target_bin = prefix + ".fat" + suffix
+        lipo_command += ["-output", target_bin]
+        subprocess.run(lipo_command)
+
+    return target_bin
+
+
+def get_mvk_sdk_path(osname):
+    def int_or_zero(i):
+        try:
+            return int(i)
+        except:
+            return 0
+
+    def ver_parse(a):
+        return [int_or_zero(i) for i in a.split(".")]
+
+    dirname = os.path.expanduser("~/VulkanSDK")
+    if not os.path.exists(dirname):
+        return ""
+
+    ver_min = ver_parse("1.3.231.0")
+    ver_num = ver_parse("0.0.0.0")
+    files = os.listdir(dirname)
+    lib_name_out = dirname
+    for file in files:
+        if os.path.isdir(os.path.join(dirname, file)):
+            ver_comp = ver_parse(file)
+            if ver_comp > ver_num and ver_comp >= ver_min:
+                # Try new SDK location.
+                lib_name = os.path.join(os.path.join(dirname, file), "macOS/lib/MoltenVK.xcframework/" + osname + "/")
+                if os.path.isfile(os.path.join(lib_name, "libMoltenVK.a")):
+                    ver_num = ver_comp
+                    lib_name_out = os.path.join(os.path.join(dirname, file), "macOS/lib/MoltenVK.xcframework")
+                else:
+                    # Try old SDK location.
+                    lib_name = os.path.join(
+                        os.path.join(dirname, file), "MoltenVK/MoltenVK.xcframework/" + osname + "/"
+                    )
+                    if os.path.isfile(os.path.join(lib_name, "libMoltenVK.a")):
+                        ver_num = ver_comp
+                        lib_name_out = os.path.join(os.path.join(dirname, file), "MoltenVK/MoltenVK.xcframework")
+
+    return lib_name_out
+
+
+def detect_mvk(env, osname):
+    mvk_list = [
+        get_mvk_sdk_path(osname),
+        "/opt/homebrew/Frameworks/MoltenVK.xcframework",
+        "/usr/local/homebrew/Frameworks/MoltenVK.xcframework",
+        "/opt/local/Frameworks/MoltenVK.xcframework",
+    ]
+    if env["vulkan_sdk_path"] != "":
+        mvk_list.insert(0, os.path.expanduser(env["vulkan_sdk_path"]))
+        mvk_list.insert(
+            0,
+            os.path.join(os.path.expanduser(env["vulkan_sdk_path"]), "macOS/lib/MoltenVK.xcframework"),
+        )
+        mvk_list.insert(
+            0,
+            os.path.join(os.path.expanduser(env["vulkan_sdk_path"]), "MoltenVK/MoltenVK.xcframework"),
+        )
+
+    for mvk_path in mvk_list:
+        if mvk_path and os.path.isfile(os.path.join(mvk_path, osname + "/libMoltenVK.a")):
+            mvk_found = True
+            print("MoltenVK found at: " + mvk_path)
+            return mvk_path
+
+    return ""