From 94238d046228dcca57ec3d5eeb94b8c0110f6d01 Mon Sep 17 00:00:00 2001 From: bruvzg <7645683+bruvzg@users.noreply.github.com> Date: Mon, 11 Dec 2023 20:50:44 +0200 Subject: [PATCH] [iOS/macOS] Add option to automatically build (and sign / archive) bundles. --- SConstruct | 1 - misc/dist/macos/editor_debug.entitlements | 22 +++ misc/dist/macos/editor_info_plist.template | 199 +++++++++++++++++++++ platform/android/detect.py | 1 + platform/ios/SCsub | 61 +++++++ platform/ios/detect.py | 2 + platform/macos/SCsub | 98 +++++++++- platform/macos/detect.py | 78 ++------ platform_methods.py | 105 +++++++++++ 9 files changed, 498 insertions(+), 69 deletions(-) create mode 100644 misc/dist/macos/editor_debug.entitlements create mode 100644 misc/dist/macos/editor_info_plist.template diff --git a/SConstruct b/SConstruct index 04564ad149bf..47a039fb148d 100644 --- a/SConstruct +++ b/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(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("generate_apk", "Generate an APK/AAB after building Android library by calling Gradle", False)) opts.Add(BoolVariable("threads", "Enable threading support", True)) # Components diff --git a/misc/dist/macos/editor_debug.entitlements b/misc/dist/macos/editor_debug.entitlements new file mode 100644 index 000000000000..ff3f58912139 --- /dev/null +++ b/misc/dist/macos/editor_debug.entitlements @@ -0,0 +1,22 @@ + + + + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-executable-page-protection + + com.apple.security.cs.disable-library-validation + + com.apple.security.device.audio-input + + com.apple.security.device.camera + + com.apple.security.get-task-allow + + + diff --git a/misc/dist/macos/editor_info_plist.template b/misc/dist/macos/editor_info_plist.template new file mode 100644 index 000000000000..4b5399c34518 --- /dev/null +++ b/misc/dist/macos/editor_info_plist.template @@ -0,0 +1,199 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + Godot + CFBundleName + Godot + CFBundleIconFile + Godot.icns + CFBundleIdentifier + org.godotengine.godot + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + APPL + CFBundleShortVersionString + $short_version + CFBundleSignature + godot + CFBundleVersion + $version + NSMicrophoneUsageDescription + Microphone access is required to capture audio. + NSCameraUsageDescription + Camera access is required to capture video. + NSRequiresAquaSystemAppearance + + NSHumanReadableCopyright + © 2007-present Juan Linietsky, Ariel Manzur & Godot Engine contributors + CFBundleSupportedPlatforms + + MacOSX + + NSPrincipalClass + NSApplication + LSApplicationCategoryType + public.app-category.developer-tools + LSMinimumSystemVersion + 10.12 + LSMinimumSystemVersionByArchitecture + + x86_64 + 10.12 + + NSHighResolutionCapable + + CFBundleDocumentTypes + + + CFBundleTypeRole + Editor + LSItemContentTypes + + public.tscn + + NSExportableTypes + + public.tscn + + + + CFBundleTypeRole + Editor + LSItemContentTypes + + public.godot + + NSExportableTypes + + public.godot + + + + UTExportedTypeDeclarations + + + UTTypeIdentifier + public.tscn + UTTypeReferenceURL + + UTTypeDescription + Godot Engine scene + UTTypeIconFile + Scene.icns + UTTypeConformsTo + + public.data + + UTTypeTagSpecification + + public.filename-extension + + scn + tscn + escn + + public.mime-type + application/x-godot-scene + + + + UTTypeIdentifier + public.gd + UTTypeReferenceURL + + UTTypeDescription + GDScript script + UTTypeIconFile + GDScript.icns + UTTypeConformsTo + + public.script + + UTTypeTagSpecification + + public.filename-extension + + gd + + public.mime-type + application/x-gdscript + + + + UTTypeIdentifier + public.res + UTTypeReferenceURL + + UTTypeDescription + Godot Engine resource + UTTypeIconFile + Resource.icns + UTTypeConformsTo + + public.data + + UTTypeTagSpecification + + public.filename-extension + + res + tres + + public.mime-type + application/x-godot-resource + + + + UTTypeIdentifier + public.gdshader + UTTypeReferenceURL + + UTTypeDescription + Godot Engine shader + UTTypeIconFile + Shader.icns + UTTypeConformsTo + + public.script + + UTTypeTagSpecification + + public.filename-extension + + gdshader + + public.mime-type + application/x-godot-shader + + + + UTTypeIdentifier + public.godot + UTTypeReferenceURL + + UTTypeDescription + Godot Engine project + UTTypeIconFile + Project.icns + UTTypeConformsTo + + public.data + + UTTypeTagSpecification + + public.filename-extension + + godot + + public.mime-type + application/x-godot-project + + + + + diff --git a/platform/android/detect.py b/platform/android/detect.py index b396e5eb2d6e..8976e218b37f 100644 --- a/platform/android/detect.py +++ b/platform/android/detect.py @@ -28,6 +28,7 @@ def get_opts(): "android-" + str(get_min_target_api()), ), 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), ] diff --git a/platform/ios/SCsub b/platform/ios/SCsub index d7c950967c06..5a57f3840bd2 100644 --- a/platform/ios/SCsub +++ b/platform/ios/SCsub @@ -2,6 +2,62 @@ 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 = [ "godot_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) + +if env["generate_bundle"]: + generate_bundle_command = env.Command("generate_bundle", [], generate_bundle) + command = env.AlwaysBuild(generate_bundle_command) + env.Depends(command, [combine_command]) diff --git a/platform/ios/detect.py b/platform/ios/detect.py index 23f688501b81..f8468e3d9e6c 100644 --- a/platform/ios/detect.py +++ b/platform/ios/detect.py @@ -23,6 +23,7 @@ def get_opts(): from SCons.Variables import BoolVariable return [ + ("vulkan_sdk_path", "Path to the Vulkan SDK", ""), ( "IOS_TOOLCHAIN_PATH", "Path to iOS toolchain", @@ -31,6 +32,7 @@ def get_opts(): ("IOS_SDK_PATH", "Path to the iOS SDK", ""), BoolVariable("ios_simulator", "Build for iOS Simulator", False), ("ios_triple", "Triple for ios toolchain", ""), + BoolVariable("generate_bundle", "Generate an APP bundle after building iOS/macOS binaries", False), ] diff --git a/platform/macos/SCsub b/platform/macos/SCsub index 5a93c3a09ff3..559e5c3f1f0f 100644 --- a/platform/macos/SCsub +++ b/platform/macos/SCsub @@ -2,8 +2,99 @@ 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 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 = [ "os_macos.mm", @@ -33,3 +124,8 @@ prog = env.add_program("#bin/godot", files) if env["debug_symbols"] and env["separate_debug_symbols"]: 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]) diff --git a/platform/macos/detect.py b/platform/macos/detect.py index 0d1e40fb3d0d..54eeb833faea 100644 --- a/platform/macos/detect.py +++ b/platform/macos/detect.py @@ -1,7 +1,7 @@ import os import sys 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 @@ -33,6 +33,12 @@ def get_opts(): 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), ("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"): # Validate arch. supported_arches = ["x86_64", "arm64"] @@ -274,32 +239,11 @@ def configure(env: "Environment"): env.Append(LINKFLAGS=["-framework", "Metal", "-framework", "IOSurface"]) if not env["use_volk"]: 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( "MoltenVK SDK installation directory not found, use 'vulkan_sdk_path' SCons parameter to specify SDK path." ) diff --git a/platform_methods.py b/platform_methods.py index 8b2c62ad4a38..a05298bfa5a1 100644 --- a/platform_methods.py +++ b/platform_methods.py @@ -140,3 +140,108 @@ def generate_export_icons(platform_path, platform_name): wf = export_path + "/" + name + "_svg.gen.h" with open(wf, "w") as svgw: 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 ""