From 60699bc0092120e6589d4688c67f1d5db5a58058 Mon Sep 17 00:00:00 2001 From: Cengiz Date: Sat, 20 Jun 2026 03:09:45 +0600 Subject: [PATCH] Synced bash script options --- .editorconfig | 4 + .github/ISSUE_TEMPLATE/bug_report.yaml | 22 +- addon/addon-build.gradle.kts | 72 +- addon/src/main/PluginTemplatePlugin.gd | 11 +- addon/src/shared/GmpLogger.gd | 22 +- addon/src/shared/SpmDependency.gd | 2 +- addon/src/shared/menu/GmpMenu.gd | 1124 +++++++++++++++++ addon/src/shared/menu/gmp.json | 350 +++++ addon/src/shared/menu/plugin.cfg | 7 + android/android-build.gradle.kts | 20 + .../PluginTemplatePluginTest.java | 2 +- common/build-logic/logic.gradle.kts | 41 +- common/gradle/libs.versions.toml | 6 +- docs/CONTRIBUTING.md | 101 +- docs/README.md | 14 +- ios/ios-build.gradle.kts | 199 +-- ios/plugin.xcodeproj/project.pbxproj | 53 +- ios/test/unit/PluginTemplateTests.swift | 2 +- script/build.sh | 13 +- script/build_android.sh | 42 +- script/build_ios.sh | 38 +- script/spm_manager.rb | 5 +- 22 files changed, 1906 insertions(+), 244 deletions(-) create mode 100644 addon/src/shared/menu/GmpMenu.gd create mode 100644 addon/src/shared/menu/gmp.json create mode 100644 addon/src/shared/menu/plugin.cfg diff --git a/.editorconfig b/.editorconfig index 5fbf78c..e4bd2da 100644 --- a/.editorconfig +++ b/.editorconfig @@ -30,3 +30,7 @@ indent_style = unset indent_style = space indent_size = 2 max_line_length = unset + +# Properties +[*.json] +max_line_length = unset diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index a192d75..95be751 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -43,14 +43,16 @@ body: label: Godot Version description: What version of Godot are you running? options: - - 4.5.stable - - 4.5.1.stable - - 4.6.stable - - 4.6.1.stable + - 4.7.stable + - 4.6.3.stable - 4.6.2.stable + - 4.6.1.stable + - 4.6.stable + - 4.5.1.stable + - 4.5.stable - 4.x - Other - default: 2 + default: 0 validations: required: true - type: dropdown @@ -89,15 +91,15 @@ body: label: OS Plugin Is Running On description: What operating system is the plugin (and your app) running on? options: - - Android 13 - - Android 14 - - Android 15 - Android 16 + - Android 15 + - Android 14 + - Android 13 - Android (other) - - iOS 18.x - iOS 26.x + - iOS 18.x - iOS (other) - default: 3 + default: 0 validations: required: true - type: textarea diff --git a/addon/addon-build.gradle.kts b/addon/addon-build.gradle.kts index 011339a..6594815 100644 --- a/addon/addon-build.gradle.kts +++ b/addon/addon-build.gradle.kts @@ -110,6 +110,8 @@ fun TaskContainerScope.registerGdscriptFormatTask( fileTree("${rootProject.projectDir}/../demo") { include("**/*.gd") exclude("addons/**") + exclude("android/**") + exclude("ios/**") }.files, ) }.map { it.relativeTo(addonSrcDir).path } @@ -185,41 +187,42 @@ tasks { inputs.files(fileTree(addonSrcDir) { include("**/*.png") }) } + val allTokens: Map by lazy { + buildMap { + project.extra.properties.forEach { (k, v) -> + val raw = v.toString() + put( + k, + if (raw.contains(",")) { + raw.split(",").joinToString(", ") { "\"${it.trim()}\"" } + } else { + raw + }, + ) + } + put("androidDependencies", androidDependencies.joinToString(", ") { "\"$it\"" }) + put("iosFrameworks", iosFrameworks.toQuotedString()) + put("iosEmbeddedFrameworks", iosEmbeddedFrameworks.toQuotedString()) + put("iosLinkerFlags", iosLinkerFlags.toQuotedString()) + put("iosBundleFiles", iosBundleFiles.toQuotedString()) + put("spmDependencies", iosSpmDependencies.toGdscriptFormat()) + } + } + register("generateSharedGDScript") { description = "Copies shared GDScript templates to the GMPShared output directory and replaces tokens" group = "generate" onlyIf("shared source directory contains GDScript or config files") { sharedSrcDir.exists() && - fileTree(sharedSrcDir) { include("**/*.gd", "**/*.cfg") }.files.isNotEmpty() + fileTree(sharedSrcDir) { include("**/*.gd", "**/*.cfg", "**/*.json") }.files.isNotEmpty() } from(sharedSrcDir) into("$outputDir/addons/GMPShared") - include("**/*.gd", "**/*.cfg") + include("**/*.gd", "**/*.cfg", "**/*.json") eachFile { println("[DEBUG] Processing shared file $relativePath") } - val allTokens: Map = - buildMap { - project.extra.properties.forEach { (k, v) -> - val raw = v.toString() - put( - k, - if (raw.contains(",")) { - raw.split(",").joinToString(", ") { "\"${it.trim()}\"" } - } else { - raw - }, - ) - } - put("androidDependencies", androidDependencies.joinToString(", ") { "\"$it\"" }) - put("iosFrameworks", iosFrameworks.toQuotedString()) - put("iosEmbeddedFrameworks", iosEmbeddedFrameworks.toQuotedString()) - put("iosLinkerFlags", iosLinkerFlags.toQuotedString()) - put("iosBundleFiles", iosBundleFiles.toQuotedString()) - put("spmDependencies", iosSpmDependencies.toGdscriptFormat()) - } - filter { line: String -> allTokens.entries.fold(line) { acc, (key, value) -> val token = "@$key@" @@ -235,6 +238,7 @@ tasks { if (sharedSrcDir.exists()) inputs.dir(sharedSrcDir) inputs.files( rootProject.file("config/plugin.properties"), + rootProject.file("../addon/config/addon-build.properties"), rootProject.file("../ios/config/ios.properties"), rootProject.file("../ios/config/spm_dependencies.json"), ) @@ -265,27 +269,6 @@ tasks { eachFile { println("[DEBUG] Processing file: $relativePath") } - val allTokens: Map = - buildMap { - project.extra.properties.forEach { (k, v) -> - val raw = v.toString() - put( - k, - if (raw.contains(",")) { - raw.split(",").joinToString(", ") { "\"${it.trim()}\"" } - } else { - raw - }, - ) - } - put("androidDependencies", androidDependencies.joinToString(", ") { "\"$it\"" }) - put("iosFrameworks", iosFrameworks.toQuotedString()) - put("iosEmbeddedFrameworks", iosEmbeddedFrameworks.toQuotedString()) - put("iosLinkerFlags", iosLinkerFlags.toQuotedString()) - put("iosBundleFiles", iosBundleFiles.toQuotedString()) - put("spmDependencies", iosSpmDependencies.toGdscriptFormat()) - } - filter { line: String -> allTokens.entries.fold(line) { acc, (key, value) -> val token = "@$key@" @@ -301,6 +284,7 @@ tasks { inputs.dir(addonSrcDir) inputs.files( rootProject.file("config/plugin.properties"), + rootProject.file("../addon/config/addon-build.properties"), rootProject.file("../ios/config/ios.properties"), rootProject.file("../ios/config/spm_dependencies.json"), ) diff --git a/addon/src/main/PluginTemplatePlugin.gd b/addon/src/main/PluginTemplatePlugin.gd index 16c2baa..130b649 100644 --- a/addon/src/main/PluginTemplatePlugin.gd +++ b/addon/src/main/PluginTemplatePlugin.gd @@ -33,8 +33,6 @@ func _exit_tree() -> void: class AndroidExportPlugin extends EditorExportPlugin: - var _plugin_name = PLUGIN_NAME - func _supports_platform(platform: EditorExportPlatform) -> bool: return platform is EditorExportPlatformAndroid @@ -42,13 +40,13 @@ class AndroidExportPlugin extends EditorExportPlugin: func _get_android_libraries(platform: EditorExportPlatform, debug: bool) -> PackedStringArray: if debug: - return PackedStringArray(["%s/bin/debug/%s-debug.aar" % [_plugin_name, _plugin_name]]) + return PackedStringArray(["%s/bin/debug/%s-debug.aar" % [PLUGIN_NAME, PLUGIN_NAME]]) else: - return PackedStringArray(["%s/bin/release/%s-release.aar" % [_plugin_name, _plugin_name]]) + return PackedStringArray(["%s/bin/release/%s-release.aar" % [PLUGIN_NAME, PLUGIN_NAME]]) func _get_name() -> String: - return _plugin_name + return PLUGIN_NAME func _get_android_dependencies(platform: EditorExportPlatform, debug: bool) -> PackedStringArray: @@ -56,7 +54,6 @@ class AndroidExportPlugin extends EditorExportPlugin: class IosExportPlugin extends EditorExportPlugin: - var _plugin_name = PLUGIN_NAME var _spm_dependencies = [] @@ -65,7 +62,7 @@ class IosExportPlugin extends EditorExportPlugin: func _get_name() -> String: - return _plugin_name + return PLUGIN_NAME func _export_begin(_features: PackedStringArray, _is_debug: bool, _path: String, _flags: int) -> void: diff --git a/addon/src/shared/GmpLogger.gd b/addon/src/shared/GmpLogger.gd index 6682c4c..a05d66c 100644 --- a/addon/src/shared/GmpLogger.gd +++ b/addon/src/shared/GmpLogger.gd @@ -1,19 +1,29 @@ # -# © 2026-present https://github.com/<> +# © 2026-present Godot Mobile Plugins (https://github.com/godot-mobile-plugins) # class_name GmpLogger extends Object -const PLUGIN_SINGLETON_NAME: String = "@pluginName@" - static func log_error(a_description: String) -> void: - push_error("%s: %s" % [PLUGIN_SINGLETON_NAME, a_description]) + push_error("%s" % [a_description]) static func log_warn(a_description: String) -> void: - push_warning("%s: %s" % [PLUGIN_SINGLETON_NAME, a_description]) + push_warning("%s" % [a_description]) static func log_info(a_description: String) -> void: - print_rich("[color=lime]%s: INFO: %s[/color]" % [PLUGIN_SINGLETON_NAME, a_description]) + print_rich("[color=lime]INFO: %s[/color]" % [a_description]) + + +static func error(a_plugin_name: String, a_description: String) -> void: + log_error("%s: %s" % [a_plugin_name, a_description]) + + +static func warn(a_plugin_name: String, a_description: String) -> void: + log_warn("%s: %s" % [a_plugin_name, a_description]) + + +static func info(a_plugin_name: String, a_description: String) -> void: + log_info("%s: %s" % [a_plugin_name, a_description]) diff --git a/addon/src/shared/SpmDependency.gd b/addon/src/shared/SpmDependency.gd index e5228f5..dd0858c 100644 --- a/addon/src/shared/SpmDependency.gd +++ b/addon/src/shared/SpmDependency.gd @@ -1,5 +1,5 @@ # -# © 2026-present https://github.com/<> +# © 2026-present Godot Mobile Plugins (https://github.com/godot-mobile-plugins) # class_name SpmDependency extends RefCounted diff --git a/addon/src/shared/menu/GmpMenu.gd b/addon/src/shared/menu/GmpMenu.gd new file mode 100644 index 0000000..519e296 --- /dev/null +++ b/addon/src/shared/menu/GmpMenu.gd @@ -0,0 +1,1124 @@ +# +# © 2026-present Godot Mobile Plugins (https://github.com/godot-mobile-plugins) +# + +@tool +extends EditorPlugin + +var gmp_menu: PopupMenu +var plugins_data: Dictionary = {} +var gmp_window: GmpPluginWindow +var local_json_version: String = "1.0" + + +func _enter_tree(): + # 1. Create the PopupMenu + gmp_menu = PopupMenu.new() + gmp_menu.name = "GMP" + + # 2. Parse gmp.json and populate menu + _load_plugins_data() + + # 3. Connect the selection signal + gmp_menu.id_pressed.connect(_on_menu_item_pressed) + + # 4. Inject into the Root Menu Bar (Next to Scene, Project, Editor, Help) + var menu_bar = _get_editor_menu_bar() + if menu_bar: + menu_bar.add_child(gmp_menu) + else: + # Fallback to standard Godot Tools menu if the UI tree changes in future versions + push_warning("GMP Addon: Could not find root MenuBar. Falling back to Project > Tools.") + add_tool_submenu_item("GMP", gmp_menu) + + # 5. Check for updates to gmp.json remotely + _check_and_update_gmp_json() + + +func _load_plugins_data(): + if gmp_menu: + gmp_menu.clear() + plugins_data.clear() + + # Explicitly target the shared menu directory + var json_path = "res://addons/GMPShared/menu/gmp.json" + + if not FileAccess.file_exists(json_path): + push_error("GMP Addon: Could not find gmp.json at " + json_path) + return + + var file = FileAccess.open(json_path, FileAccess.READ) + var json = JSON.new() + var err = json.parse(file.get_as_text()) + + if err != OK: + push_error("GMP Addon: Failed to parse gmp.json. Error at line " + str(json.get_error_line())) + return + + var data = json.get_data() + if not typeof(data) == TYPE_DICTIONARY: + push_error("GMP Addon: gmp.json root is not a dictionary.") + return + + if data.has("version"): + local_json_version = str(data["version"]) + + var item_id = 0 + for key in data: + if key == "version": + continue + + var plugin_info = data[key] + var plugin_name = plugin_info.get("name", "Unnamed Plugin") + + # Add to menu and store data for window mapping + gmp_menu.add_item(plugin_name, item_id) + plugins_data[item_id] = plugin_info + item_id += 1 + + +func _check_and_update_gmp_json(): + var json_http = HTTPRequest.new() + add_child(json_http) + + # Download directly to a temporary file first + var temp_path = "user://temp_gmp.json" + json_http.download_file = temp_path + + json_http.request_completed.connect( + func(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray): + if result == HTTPRequest.RESULT_SUCCESS and response_code == 200: + var file = FileAccess.open(temp_path, FileAccess.READ) + if file: + var json = JSON.new() + if json.parse(file.get_as_text()) == OK: + var remote_data = json.get_data() + if typeof(remote_data) == TYPE_DICTIONARY and remote_data.has("version"): + var remote_ver = str(remote_data["version"]) + file.close() # Always close before doing file-system operations + + # Compare remote version to local version + if GmpPluginWindow._compare_versions(remote_ver, local_json_version) > 0: + var target_path = "res://addons/GMPShared/menu/gmp.json" + + # Replace at the file level + if FileAccess.file_exists(target_path): + DirAccess.remove_absolute(target_path) + + var err = DirAccess.copy_absolute(temp_path, target_path) + + if err == OK: + # Force the editor to acknowledge the replaced file + EditorInterface.get_resource_filesystem().scan() + + # Re-populate UI layout with updated contents + _load_plugins_data() + else: + push_error("GMP Addon: Failed to copy updated gmp.json to res://") + + # Clean up the temporary file and HTTP node + if FileAccess.file_exists(temp_path): + DirAccess.remove_absolute(temp_path) + + json_http.queue_free() + ) + + json_http.request("https://raw.githubusercontent.com/godot-mobile-plugins/gmp-menu/refs/heads/main/src/gmp.json") + + +func _exit_tree(): + # 1. Remove the menu safely + if gmp_menu: + if gmp_menu.get_parent(): + gmp_menu.get_parent().remove_child(gmp_menu) + gmp_menu.queue_free() + + # 2. Clean up any open plugin windows + if is_instance_valid(gmp_window): + gmp_window.queue_free() + + +func _on_menu_item_pressed(id: int): + if plugins_data.has(id): + _open_sub_window(GmpPluginData.new(plugins_data[id])) + + +func _open_sub_window(plugin_data: GmpPluginData): + if is_instance_valid(gmp_window): + gmp_window.queue_free() + + gmp_window = GmpPluginWindow.new(plugin_data) + gmp_window.close_requested.connect(func(): gmp_window.queue_free()) + EditorInterface.get_base_control().add_child(gmp_window) + gmp_window.popup() + + # Download Action + gmp_window.download_btn.pressed.connect( + func(): + var selected_version := gmp_window.get_selected_version() + var platform_str = gmp_window.get_selected_platform() + + var url = ( + "%s/releases/download/v%s/%s-%s-v%s.zip" + % [ + plugin_data.get_repository(), + selected_version.get_version(), + plugin_data.get_directory(), + platform_str, + selected_version.get_version() + ] + ) + + print("URL: %s" % url) + gmp_window.plugin_archive_request.download_file = "user://gmp_download.zip" + var err = gmp_window.plugin_archive_request.request(url) + + if err == OK: + gmp_window.download_btn.disabled = true + gmp_window.download_btn.text = "Downloading..." + gmp_window.progress_bar.value = 0 + gmp_window.progress_bar.show() + gmp_window.install_btn.hide() + gmp_window.progress_timer.start() + else: + push_error("GMP Addon: Failed to start download request.") + ) + + gmp_window.progress_timer.timeout.connect( + func(): + var body_size = gmp_window.plugin_archive_request.get_body_size() + var downloaded = gmp_window.plugin_archive_request.get_downloaded_bytes() + if body_size > 0: + gmp_window.progress_bar.max_value = body_size + gmp_window.progress_bar.value = downloaded + ) + + gmp_window.plugin_archive_request.request_completed.connect( + func(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray): + gmp_window.progress_timer.stop() + if result == HTTPRequest.RESULT_SUCCESS and response_code in [200, 301, 302, 303]: + gmp_window.progress_bar.value = gmp_window.progress_bar.max_value + gmp_window.download_btn.text = "Download Complete" + gmp_window.install_btn.show() + else: + push_error("GMP Addon: Download failed with response code: " + str(response_code)) + gmp_window.download_btn.text = "Download Failed" + gmp_window.download_btn.disabled = false + gmp_window.progress_bar.hide() + ) + + # Install Action + gmp_window.install_btn.pressed.connect( + func(): + gmp_window.install_btn.disabled = true + gmp_window.install_btn.text = "Installing..." + + var reader = ZIPReader.new() + var err = reader.open("user://gmp_download.zip") + if err == OK: + for file_path in reader.get_files(): + # Ignore the root directory in the archive + var slash_idx = file_path.find("/") + if slash_idx == -1 or slash_idx == file_path.length() - 1: + continue + + var stripped_path = file_path.substr(slash_idx + 1) + var target_path = "res://" + stripped_path + + # Ensure directory architecture is built + if stripped_path.ends_with("/"): + DirAccess.make_dir_recursive_absolute(target_path) + else: + var base_dir = target_path.get_base_dir() + if not DirAccess.dir_exists_absolute(base_dir): + DirAccess.make_dir_recursive_absolute(base_dir) + + var content = reader.read_file(file_path) + var fa = FileAccess.open(target_path, FileAccess.WRITE) + if fa: + fa.store_buffer(content) + fa.close() + reader.close() + + # Force Godot to scan the file system so new files appear immediately + EditorInterface.get_resource_filesystem().scan() + + gmp_window.install_btn.text = "Install Complete!" + + # Update UI to reflect the installation + var newly_installed_version := gmp_window.get_selected_version() + gmp_window.set_installed_version_label(newly_installed_version.get_version()) + gmp_window.uninstall_btn.show() + else: + gmp_window.install_btn.text = "Extraction Failed" + push_error("GMP Addon: Failed to open downloaded zip archive.") + ) + + # Uninstall Action + gmp_window.uninstall_btn.pressed.connect( + func(): + gmp_window.uninstall_btn.disabled = true + gmp_window.uninstall_btn.text = "Uninstalling..." + + var plugin_dir = gmp_window.plugin_info.get_directory() + if not plugin_dir.is_empty(): + # Remove addon dir + _remove_recursive("res://addons/" + plugin_dir) + + # Remove iOS directories and files matching plugin_dir* + var ios_path = "res://ios/plugins" + if DirAccess.dir_exists_absolute(ios_path): + var dir = DirAccess.open(ios_path) + if dir: + dir.list_dir_begin() + var file_name = dir.get_next() + while file_name != "": + if file_name != "." and file_name != ".." and file_name.begins_with(plugin_dir): + if dir.current_is_dir(): + _remove_recursive(ios_path + "/" + file_name) + else: + DirAccess.remove_absolute(ios_path + "/" + file_name) + file_name = dir.get_next() + dir.list_dir_end() + + # Force Godot to scan the file system + EditorInterface.get_resource_filesystem().scan() + + # Update UI + gmp_window.set_installed_version_label("None", Color(0.6, 0.6, 0.6)) + gmp_window.uninstall_btn.hide() + gmp_window.uninstall_btn.text = "Uninstall" + gmp_window.uninstall_btn.disabled = false + + gmp_window.reset_download_ui() + ) + + gmp_window.readme_request.request(plugin_data.get_readme_url()) + + +# --- Helper Methods --- + + +static func _remove_recursive(path: String) -> void: + if DirAccess.dir_exists_absolute(path): + var dir = DirAccess.open(path) + if dir: + dir.list_dir_begin() + var file_name = dir.get_next() + while file_name != "": + if file_name != "." and file_name != "..": + var child_path = path + "/" + file_name + if dir.current_is_dir(): + _remove_recursive(child_path) + else: + DirAccess.remove_absolute(child_path) + file_name = dir.get_next() + dir.list_dir_end() + DirAccess.remove_absolute(path) + + +func _get_editor_menu_bar() -> MenuBar: + var base_control = get_editor_interface().get_base_control() + return _find_node_by_class(base_control, "MenuBar") as MenuBar + + +func _find_node_by_class(node: Node, class_name_str: String) -> Node: + if node.is_class(class_name_str): + return node + for child in node.get_children(): + var found = _find_node_by_class(child, class_name_str) + if found: + return found + return null + + +class GmpPluginData: + extends RefCounted + + const NAME_PROPERTY := &"name" + const DIRECTORY_PROPERTY := &"directory" + const DESCRIPTION_PROPERTY := &"description" + const AUTHOR_PROPERTY := &"author" + const REPOSITORY_PROPERTY := &"repository" + const VERSIONS_PROPERTY := &"versions" + const SPONSORSHIP_URL_PROPERTY := &"sponsorship_url" + + var plugin_data: Dictionary + + func _init(plugin_data: Dictionary): + self.plugin_data = plugin_data + + func get_name() -> String: + return plugin_data.get(NAME_PROPERTY, "GMP Plugin") + + func get_directory() -> String: + return plugin_data.get(DIRECTORY_PROPERTY, "GMPPlugin") + + func get_description() -> String: + return plugin_data.get(DESCRIPTION_PROPERTY, "GMP Plugin") + + func get_author() -> String: + return plugin_data.get(AUTHOR_PROPERTY, "Godot Mobile Plugins") + + func get_repository() -> String: + return plugin_data.get(REPOSITORY_PROPERTY, "") + + func get_versions() -> Array: + return plugin_data.get(VERSIONS_PROPERTY, []) + + func get_version(a_index: int) -> GmpPluginVersion: + var __versions := get_versions() + return GmpPluginVersion.new(__versions[a_index]) if a_index < __versions.size() else null + + func get_sponsorship_url() -> String: + return plugin_data.get(SPONSORSHIP_URL_PROPERTY, "") + + func get_readme_url() -> String: + var repo_url := get_repository() + var raw_repo_url := repo_url.replace("github.com", "raw.githubusercontent.com") + return raw_repo_url + "/refs/heads/main/docs/README.md" + + +class GmpPluginVersion: + extends RefCounted + + const VERSION_PROPERTY := &"version" + const MIN_GODOT_PROPERTY := &"min_godot" + const MAX_GODOT_PROPERTY := &"max_godot" + const ANDROID_PROPERTY := &"ios" + const IOS_PROPERTY := &"android" + const MULTI_PROPERTY := &"multi" + + var version_data: Dictionary + + func _init(version_data: Dictionary): + self.version_data = version_data + + func get_version() -> String: + return version_data.get(VERSION_PROPERTY, "Unknown") + + func has_min_godot() -> bool: + return version_data.has(MIN_GODOT_PROPERTY) + + func get_min_godot() -> String: + return version_data.get(MIN_GODOT_PROPERTY) + + func has_max_godot() -> bool: + return version_data.has(MAX_GODOT_PROPERTY) + + func get_max_godot() -> String: + return version_data.get(MAX_GODOT_PROPERTY) + + func get_android() -> bool: + return version_data.has(ANDROID_PROPERTY) and version_data.get(ANDROID_PROPERTY) + + func get_ios() -> bool: + return version_data.has(IOS_PROPERTY) and version_data.get(IOS_PROPERTY) + + func get_multi() -> bool: + return version_data.has(MULTI_PROPERTY) and version_data.get(MULTI_PROPERTY) + + +class GmpPluginWindow: + extends Window + const LINK_COLOR := Color.CORNFLOWER_BLUE + + var plugin_info: GmpPluginData + + var download_btn: Button + var install_btn: Button + var uninstall_btn: Button + var installed_version_label: Label + var editor_version_val: Label + var compatibility_val: Label + var version_dropdown: OptionButton + var android_radio: CheckBox + var ios_radio: CheckBox + var progress_bar: ProgressBar + var progress_timer: Timer + var plugin_archive_request: HTTPRequest + var readme_request: HTTPRequest + + func _init(plugin_info: GmpPluginData): + self.plugin_info = plugin_info + download_btn = Button.new() + install_btn = Button.new() + uninstall_btn = Button.new() + installed_version_label = Label.new() + editor_version_val = Label.new() + compatibility_val = Label.new() + version_dropdown = OptionButton.new() + android_radio = CheckBox.new() + ios_radio = CheckBox.new() + progress_bar = ProgressBar.new() + progress_timer = Timer.new() + plugin_archive_request = HTTPRequest.new() + readme_request = HTTPRequest.new() + + var window_title = plugin_info.get_name() + + self.title = window_title + + # Size constraints: 600x500 or screen size if smaller + var screen_idx = DisplayServer.window_get_current_screen() + var screen_size = DisplayServer.screen_get_size(screen_idx) + self.size = Vector2i(ceil(screen_size.x * 0.7), ceil(screen_size.y * 0.7)) + + self.transient = true # Keeps the window on top of the editor + self.exclusive = false + + # Center the window relative to the editor + var editor_rect = EditorInterface.get_base_control().get_global_rect() + self.position = editor_rect.position + (editor_rect.size / 2) - (Vector2(self.size) / 2) + + # Setup Support Nodes for Downloading + self.add_child(plugin_archive_request) + + progress_timer.wait_time = 0.1 + progress_timer.autostart = false + self.add_child(progress_timer) + + # Setup UI Container structure + var margin_container = MarginContainer.new() + margin_container.set_anchors_preset(Control.PRESET_FULL_RECT) + margin_container.add_theme_constant_override("margin_left", 20) + margin_container.add_theme_constant_override("margin_right", 20) + margin_container.add_theme_constant_override("margin_top", 20) + margin_container.add_theme_constant_override("margin_bottom", 20) + self.add_child(margin_container) + + var vbox = VBoxContainer.new() + vbox.add_theme_constant_override("separation", 15) + margin_container.add_child(vbox) + + # Plugin Name + var name_label = Label.new() + name_label.text = window_title + name_label.add_theme_font_size_override("font_size", 60) + var font_variation := FontVariation.new() + font_variation.variation_embolden = 1.5 + name_label.add_theme_font_override("font", font_variation) + name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + vbox.add_child(name_label) + + # Plugin Description + var desc_label = Label.new() + desc_label.text = plugin_info.get_description() + desc_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + vbox.add_child(desc_label) + + # Plugin Author + var author_hbox = HBoxContainer.new() + author_hbox.add_theme_constant_override("separation", 40) + + var author_title_label = Label.new() + author_title_label.text = "Author:" + author_hbox.add_child(author_title_label) + + var author_value_label = Label.new() + author_value_label.text = plugin_info.get_author() + author_hbox.add_child(author_value_label) + vbox.add_child(author_hbox) + + # Links (Repository & Issues) + var repo_url = plugin_info.get_repository() + var links_hbox = HBoxContainer.new() + links_hbox.add_theme_constant_override("separation", 80) + links_hbox.size_flags_horizontal = Control.SIZE_SHRINK_CENTER + + var repo_link = LinkButton.new() + repo_link.text = "View Repository" + repo_link.uri = repo_url + repo_link.add_theme_color_override("font_color", LINK_COLOR) + links_hbox.add_child(repo_link) + + var issues_link = LinkButton.new() + issues_link.text = "View Issues" + issues_link.uri = repo_url + "/issues" + issues_link.add_theme_color_override("font_color", LINK_COLOR) + links_hbox.add_child(issues_link) + vbox.add_child(links_hbox) + + vbox.add_child(HSeparator.new()) + + # Main Split Layout (Left: README, Right: Controls) + var split_hbox = HBoxContainer.new() + split_hbox.size_flags_vertical = Control.SIZE_EXPAND_FILL + split_hbox.add_theme_constant_override("separation", 20) + vbox.add_child(split_hbox) + + # --- LEFT PANEL: README --- + var left_vbox = VBoxContainer.new() + left_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL + left_vbox.size_flags_stretch_ratio = 1.2 + split_hbox.add_child(left_vbox) + + var readme_scroll = ScrollContainer.new() + readme_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL + left_vbox.add_child(readme_scroll) + + var readme_label = RichTextLabel.new() + readme_label.bbcode_enabled = true + readme_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + readme_label.size_flags_vertical = Control.SIZE_EXPAND_FILL + readme_label.text = "Loading README..." + readme_label.meta_clicked.connect(func(meta): OS.shell_open(str(meta))) + readme_scroll.add_child(readme_label) + + # Fetch README Data + self.add_child(readme_request) + + readme_request.request_completed.connect( + func(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray): + if result == HTTPRequest.RESULT_SUCCESS and response_code == 200: + readme_label.text = ( + "[center][b][font_size=36]README.md[/font_size][/b][/center]\n" + + _markdown_to_bbcode(body.get_string_from_utf8()) + ) + else: + readme_label.text = ( + "[color=red]Failed to load README. Ensure it exists at:[/color]\n" + + plugin_info.get_readme_url() + ) + ) + + # --- RIGHT PANEL: CONTROLS --- + var right_vbox = VBoxContainer.new() + right_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL + right_vbox.size_flags_stretch_ratio = 0.8 + split_hbox.add_child(right_vbox) + + # --- INSTALLED VERSION LOGIC --- + var installed_title_label = Label.new() + installed_title_label.text = "INSTALLED VERSION" + installed_title_label.add_theme_font_size_override("font_size", 36) + installed_title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + right_vbox.add_child(installed_title_label) + + installed_version_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + installed_version_label.add_theme_font_size_override("font_size", 30) + + var plugin_dir = plugin_info.get_directory() + var installed_version_text = "None" + + if not plugin_dir.is_empty(): + var addon_path = "res://addons/" + plugin_dir + if DirAccess.dir_exists_absolute(addon_path): + var cfg_path = addon_path + "/plugin.cfg" + if FileAccess.file_exists(cfg_path): + var config = ConfigFile.new() + if config.load(cfg_path) == OK: + installed_version_text = config.get_value("plugin", "version", "Unknown") + + installed_version_label.text = installed_version_text + if installed_version_text == "None": + installed_version_label.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6)) + right_vbox.add_child(installed_version_label) + + uninstall_btn.text = "Uninstall" + uninstall_btn.custom_minimum_size = Vector2(200, 0) + uninstall_btn.size_flags_horizontal = Control.SIZE_SHRINK_CENTER + uninstall_btn.visible = (installed_version_text != "None") + right_vbox.add_child(uninstall_btn) + + right_vbox.add_child(HSeparator.new()) + + # --- AVAILABLE VERSIONS LOGIC --- + var available_title_label = Label.new() + available_title_label.text = "AVAILABLE VERSIONS" + available_title_label.add_theme_font_size_override("font_size", 36) + available_title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + right_vbox.add_child(available_title_label) + + # Versions Context Menu + var versions_array = plugin_info.get_versions() + var version_hbox = HBoxContainer.new() + var version_title = Label.new() + version_title.text = "Select Version:" + + for v in versions_array: + version_dropdown.add_item(v.get("version", "Unknown")) + + version_hbox.add_child(version_title) + version_hbox.add_child(version_dropdown) + right_vbox.add_child(version_hbox) + + # Min/Max Godot Version Labels + var min_godot_hbox = HBoxContainer.new() + var min_godot_title = Label.new() + min_godot_title.text = "Min Godot:" + var min_godot_val = Label.new() + min_godot_hbox.add_child(min_godot_title) + min_godot_hbox.add_child(min_godot_val) + right_vbox.add_child(min_godot_hbox) + + var max_godot_hbox = HBoxContainer.new() + var max_godot_title = Label.new() + max_godot_title.text = "Max Godot:" + var max_godot_val = Label.new() + max_godot_hbox.add_child(max_godot_title) + max_godot_hbox.add_child(max_godot_val) + right_vbox.add_child(max_godot_hbox) + + var editor_version_hbox = HBoxContainer.new() + var editor_version_title = Label.new() + editor_version_title.text = "Editor Version:" + editor_version_hbox.add_child(editor_version_title) + editor_version_hbox.add_child(editor_version_val) + editor_version_hbox.add_child(compatibility_val) + right_vbox.add_child(editor_version_hbox) + + right_vbox.add_child(HSeparator.new()) + + # Archive Type Radio Buttons + var platform_vbox = VBoxContainer.new() + var platform_title = Label.new() + platform_title.text = "Archive Type:" + platform_vbox.add_child(platform_title) + + var platform_hbox = HBoxContainer.new() + var radio_group = ButtonGroup.new() + + android_radio.text = "Android" + android_radio.button_group = radio_group + + ios_radio.text = "iOS" + ios_radio.button_group = radio_group + + var multi_radio = CheckBox.new() + multi_radio.text = "Multi" + multi_radio.button_group = radio_group + + platform_hbox.add_child(android_radio) + platform_hbox.add_child(ios_radio) + platform_hbox.add_child(multi_radio) + platform_vbox.add_child(platform_hbox) + right_vbox.add_child(platform_vbox) + + right_vbox.add_child(HSeparator.new()) + + # Download & Installation UI + download_btn.text = "Download Plugin" + download_btn.custom_minimum_size = Vector2(200, 0) + download_btn.size_flags_horizontal = Control.SIZE_SHRINK_CENTER + right_vbox.add_child(download_btn) + + progress_bar.hide() + progress_bar.custom_minimum_size = Vector2(200, 20) + progress_bar.size_flags_horizontal = Control.SIZE_EXPAND_FILL + right_vbox.add_child(progress_bar) + + install_btn = Button.new() + install_btn.text = "Install" + install_btn.hide() + install_btn.custom_minimum_size = Vector2(200, 0) + install_btn.size_flags_horizontal = Control.SIZE_SHRINK_CENTER + right_vbox.add_child(install_btn) + + # State Update Logic + var update_version_display = func(idx: int): + var selected_version_data := plugin_info.get_version(idx) + + # Fetch and format current editor version + var v_info = Engine.get_version_info() + var current_editor_version = str(v_info.major) + "." + str(v_info.minor) + if v_info.patch > 0: + current_editor_version += "." + str(v_info.patch) + editor_version_val.text = current_editor_version + + var is_compatible = true + + if selected_version_data.has_min_godot(): + min_godot_val.text = selected_version_data.get_min_godot() + min_godot_hbox.show() + if _compare_versions(current_editor_version, selected_version_data.get_min_godot()) < 0: + is_compatible = false + else: + min_godot_hbox.hide() + + if selected_version_data.has_max_godot(): + max_godot_val.text = selected_version_data.get_max_godot() + max_godot_hbox.show() + if _compare_versions(current_editor_version, selected_version_data.get_max_godot()) > 0: + is_compatible = false + else: + max_godot_hbox.hide() + + # Set Compatibility Text and Color + if is_compatible: + compatibility_val.text = "✅ Compatible" + compatibility_val.add_theme_color_override("font_color", Color.GREEN) + else: + compatibility_val.text = "❌ Incompatible" + compatibility_val.add_theme_color_override("font_color", Color.RED) + + # Platform radio buttons logic + android_radio.visible = selected_version_data.get_android() + ios_radio.visible = selected_version_data.get_ios() + multi_radio.visible = selected_version_data.get_multi() + + if selected_version_data.get_android(): + android_radio.button_pressed = true + elif selected_version_data.get_ios(): + ios_radio.button_pressed = true + elif selected_version_data.get_multi(): + multi_radio.button_pressed = true + + reset_download_ui() + + version_dropdown.item_selected.connect(update_version_display) + if versions_array.size() > 0: + update_version_display.call(0) + else: + version_dropdown.disabled = true + download_btn.disabled = true + + var delimiter_label = Label.new() + delimiter_label.text = "" + delimiter_label.size_flags_vertical = Control.SIZE_EXPAND_FILL + right_vbox.add_child(delimiter_label) + + right_vbox.add_child(HSeparator.new()) + + var sponsorship_label = Label.new() + sponsorship_label.text = "If this plugin saves you time or effort, consider supporting its continued development." + sponsorship_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + sponsorship_label.add_theme_color_override("font_color", Color.PLUM) + right_vbox.add_child(sponsorship_label) + + var sponsorship_link = LinkButton.new() + sponsorship_link.text = "Support the Project" + sponsorship_link.uri = plugin_info.get_sponsorship_url() + sponsorship_link.size_flags_horizontal = Control.SIZE_SHRINK_CENTER + sponsorship_link.add_theme_color_override("font_color", LINK_COLOR) + right_vbox.add_child(sponsorship_link) + + right_vbox.add_child(HSeparator.new()) + + var gmp_menu_label = Label.new() + var gmp_menu_version := "0.0" + + # Load the plugin.cfg to read the version + var menu_config := ConfigFile.new() + var cfg_path := "res://addons/GMPShared/menu/plugin.cfg" + if menu_config.load(cfg_path) == OK: + gmp_menu_version = menu_config.get_value("plugin", "version", "0.0") + + gmp_menu_label.text = "GMP Menu v%s" % gmp_menu_version + + var footer_hbox = HBoxContainer.new() + footer_hbox.size_flags_horizontal = Control.SIZE_SHRINK_END + right_vbox.add_child(footer_hbox) + + gmp_menu_label.size_flags_horizontal = Control.SIZE_SHRINK_END + footer_hbox.add_child(gmp_menu_label) + + # --- PLUGIN UPGRADE LOGIC --- + var menu_update_request = HTTPRequest.new() + self.add_child(menu_update_request) + menu_update_request.request_completed.connect( + func(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray): + if result == HTTPRequest.RESULT_SUCCESS and response_code == 200: + var prop_text = body.get_string_from_utf8() + var remote_version = _parse_property(prop_text, "pluginVersion") + + if not remote_version.is_empty() and _compare_versions(remote_version, gmp_menu_version) > 0: + var upgrade_btn = Button.new() + upgrade_btn.text = "Upgrade" + upgrade_btn.add_theme_color_override("font_color", Color.GREEN) + footer_hbox.add_child(upgrade_btn) + + upgrade_btn.pressed.connect( + func(): + upgrade_btn.disabled = true + upgrade_btn.text = "Upgrading..." + + var menu_download_request = HTTPRequest.new() + self.add_child(menu_download_request) + + var dl_url = ( + "https://github.com/godot-mobile-plugins/gmp-menu/releases/download/v%s/" + + "GmpMenu-v%s.zip" % [remote_version, remote_version] + ) + menu_download_request.download_file = "user://gmp_menu_upgrade.zip" + + menu_download_request.request_completed.connect( + func( + dl_result: int, + dl_code: int, + dl_headers: PackedStringArray, + dl_body: PackedByteArray + ): + if dl_result == HTTPRequest.RESULT_SUCCESS and dl_code in [200, 301, 302, 303]: + var reader = ZIPReader.new() + var zip_err = reader.open("user://gmp_menu_upgrade.zip") + if zip_err == OK: + for file_path in reader.get_files(): + var slash_idx = file_path.find("/") + if slash_idx == -1 or slash_idx == file_path.length() - 1: + continue + + var stripped_path = file_path.substr(slash_idx + 1) + var target_path = "res://" + stripped_path + + if stripped_path.ends_with("/"): + DirAccess.make_dir_recursive_absolute(target_path) + else: + var base_dir = target_path.get_base_dir() + if not DirAccess.dir_exists_absolute(base_dir): + DirAccess.make_dir_recursive_absolute(base_dir) + + var content = reader.read_file(file_path) + var fa = FileAccess.open(target_path, FileAccess.WRITE) + if fa: + fa.store_buffer(content) + fa.close() + reader.close() + EditorInterface.get_resource_filesystem().scan() + upgrade_btn.text = "Restart Editor" + else: + upgrade_btn.text = "Extract Failed" + upgrade_btn.disabled = false + else: + upgrade_btn.text = "Download Failed" + upgrade_btn.disabled = false + menu_download_request.queue_free() + ) + menu_download_request.request(dl_url) + ) + menu_update_request.queue_free() + ) + ready.connect( + func(): + menu_update_request.request( + ( + "https://raw.githubusercontent.com/godot-mobile-plugins/gmp-menu/refs/heads/main/config/" + + "plugin.properties" + ) + ) + ) + + func get_selected_version() -> GmpPluginVersion: + return plugin_info.get_version(version_dropdown.selected) + + func get_selected_platform() -> String: + var platform_str := "Multi" + if android_radio.button_pressed: + platform_str = "Android" + elif ios_radio.button_pressed: + platform_str = "iOS" + return platform_str + + func set_installed_version_label(a_text: String, a_color: Color = Color.SNOW) -> void: + installed_version_label.text = a_text + if a_color == Color.SNOW: + installed_version_label.remove_theme_color_override("font_color") + else: + installed_version_label.add_theme_color_override("font_color", a_color) + + func reset_download_ui() -> void: + # Reset Download/Install UI on version change + download_btn.text = "Download Plugin" + download_btn.disabled = false + progress_bar.hide() + progress_bar.value = 0 + install_btn.hide() + + static func _compare_versions(v1: String, v2: String) -> int: + var parts1 = v1.split(".") + var parts2 = v2.split(".") + var max_len = max(parts1.size(), parts2.size()) + + for i in range(max_len): + # to_int() cleanly handles strings like "7-dev3" by returning 7 + var p1 = parts1[i].to_int() if i < parts1.size() else 0 + var p2 = parts2[i].to_int() if i < parts2.size() else 0 + + if p1 < p2: + return -1 + if p1 > p2: + return 1 + + return 0 + + static func _parse_property(text: String, key: String) -> String: + var lines = text.split("\n") + for line in lines: + var trimmed = line.strip_edges() + if trimmed.begins_with("#") or trimmed.begins_with(";"): + continue + var eq_idx = trimmed.find("=") + if eq_idx != -1: + var k = trimmed.substr(0, eq_idx).strip_edges() + if k == key: + return trimmed.substr(eq_idx + 1).strip_edges() + return "" + + static func _markdown_to_bbcode(md: String) -> String: + var bbcode = md + + # 1. Clean up HTML layout constructs + bbcode = bbcode.replace("
", "\n") + bbcode = bbcode.replace(" ", " ") + bbcode = bbcode.replace('
', "[center]") + bbcode = bbcode.replace("
", "[/center]") + bbcode = bbcode.replace('

', "[center]") + bbcode = bbcode.replace("

", "[/center]") + + # 2. Strip HTML anchors + var anchor_rx = RegEx.new() + anchor_rx.compile('') + bbcode = anchor_rx.sub(bbcode, "", true) + + # 3. Simplify HTML images to avoid web texture load errors in Godot + var html_img_rx = RegEx.new() + html_img_rx.compile("]*>") + bbcode = html_img_rx.sub(bbcode, "[i]🖼️[/i]", true) + + # 4. Convert HTML links + var html_link_rx = RegEx.new() + html_link_rx.compile(']*href="([^"]+)"[^>]*>(.*?)') + bbcode = html_link_rx.sub(bbcode, "[url=$1]$2[/url]", true) + + # 5. Simplify Markdown images to avoid web texture load errors + var md_img_rx = RegEx.new() + md_img_rx.compile("!\\[(.*?)\\]\\((.*?)\\)") + bbcode = md_img_rx.sub(bbcode, "[i]🖼️: $1[/i]", true) + + # 6. Convert Markdown links + var md_link_rx = RegEx.new() + md_link_rx.compile("\\[([^\\]]+)\\]\\((.*?)\\)") + bbcode = md_link_rx.sub(bbcode, "[url=$2]$1[/url]", true) + + # 7. Bold + var bold_rx = RegEx.new() + bold_rx.compile("\\*\\*(.*?)\\*\\*") + bbcode = bold_rx.sub(bbcode, "[b]$1[/b]", true) + + # 8. Italic (Using _text_) + var italic_rx = RegEx.new() + italic_rx.compile("(^|\\s)_(.*?)_(\\s|$)") + bbcode = italic_rx.sub(bbcode, "$1[i]$2[/i]$3", true) + + # 9. Inline Code (Fix for Monospace font error) + var code_rx = RegEx.new() + code_rx.compile("`(.*?)`") + bbcode = code_rx.sub(bbcode, "[code]$1[/code]", true) + + # 10. Line-by-line parsing for blocks and tables + var lines = bbcode.split("\n") + bbcode = "" + var in_code_block = false + var in_table = false + + for line in lines: + # Code Blocks + if line.begins_with("```"): + if in_table: + in_table = false + bbcode += "[/table]\n" + in_code_block = !in_code_block + bbcode += "[code]\n" if in_code_block else "[/code]\n" + continue + + if in_code_block: + bbcode += line + "\n" + continue + + # Headers + if line.begins_with("# "): + line = "[b][font_size=36]" + line.substr(2) + "[/font_size][/b]" + elif line.begins_with("## "): + line = "[b][font_size=30]" + line.substr(3) + "[/font_size][/b]" + elif line.begins_with("### "): + line = "[b][font_size=24]" + line.substr(4) + "[/font_size][/b]" + + # Unordered Lists + if line.begins_with("- "): + line = " • " + line.substr(2) + + # Tables + if line.begins_with("|") and line.ends_with("|"): + if line.find("---") != -1: + continue # Skip the markdown table separator line + + var cells = line.split("|", false) + if not in_table: + in_table = true + bbcode += "[table=" + str(cells.size()) + "]\n" + + for cell in cells: + bbcode += "[cell]" + cell.strip_edges() + "[/cell]" + bbcode += "\n" + continue + elif in_table: + in_table = false + bbcode += "[/table]\n" + + bbcode += line + "\n" + + if in_table: + bbcode += "[/table]\n" + + # 11. Remove consecutive empty lines + var final_lines = bbcode.split("\n") + var cleaned_bbcode = "" + + var alpha_rx = RegEx.new() + alpha_rx.compile("[a-zA-Z]") + + var tag_rx = RegEx.new() + tag_rx.compile("\\[.*?\\]") + + var consecutive_empty_count = 0 + + var structural_tags = [ + "[table", + "[/table]", + "[cell]", + "[/cell]", + "[code]", + "[/code]", + "[center]", + "[/center]", + "[right]", + "[/right]", + "[fill]", + "[/fill]", + "[indent]", + "[/indent]", + "[ul]", + "[/ul]", + "[ol]", + "[/ol]" + ] + + for i in range(final_lines.size()): + var current_line = final_lines[i] + + if i == final_lines.size() - 1 and current_line == "": + break + + var is_structural = false + for tag in structural_tags: + if tag in current_line: + is_structural = true + break + + if is_structural: + consecutive_empty_count = 0 + cleaned_bbcode += current_line + "\n" + continue + + var text_without_tags = tag_rx.sub(current_line, "", true) + + if alpha_rx.search(text_without_tags) == null: + consecutive_empty_count += 1 + if consecutive_empty_count >= 2: + continue + else: + consecutive_empty_count = 0 + + cleaned_bbcode += current_line + "\n" + + return cleaned_bbcode diff --git a/addon/src/shared/menu/gmp.json b/addon/src/shared/menu/gmp.json new file mode 100644 index 0000000..c3db5b7 --- /dev/null +++ b/addon/src/shared/menu/gmp.json @@ -0,0 +1,350 @@ +{ + "version": "1.0", + "admob": { + "name": "AdMob Plugin", + "directory": "AdmobPlugin", + "description": "A Godot mobile plugin for integrating Google AdMob into your Godot games. This plugin allows you to display various types of ads, such as banner ads, interstitial ads, and rewarded video ads, to monetize your mobile games effectively.", + "author": "Godot Mobile Plugins (github.com/godot-mobile-plugins)", + "repository": "https://github.com/godot-sdk-integrations/godot-admob", + "versions": [ + { + "version": "7.0", + "min_godot": "4.7", + "ios": true, + "android": true, + "multi": true + }, + { + "version": "6.0", + "min_godot": "4.6", + "max_godot": "4.6.3", + "ios": true, + "android": true, + "multi": true + }, + { + "version": "5.3", + "min_godot": "4.5", + "max_godot": "4.5.2", + "ios": true, + "android": true, + "multi": true + } + ], + "sponsorship_url": "https://github.com/sponsors/cengiz-pz" + }, + "connection_state": { + "name": "Connection State Plugin", + "directory": "ConnectionStatePlugin", + "description": "A Godot mobile plugin for managing connection states in your Godot games. This plugin provides a simple and efficient way to check the network connection status, allowing you to handle online and offline scenarios gracefully in your mobile games.", + "author": "Godot Mobile Plugins (github.com/godot-mobile-plugins)", + "repository": "https://github.com/godot-mobile-plugins/godot-connection-state", + "versions": [ + { + "version": "1.2", + "min_godot": "4.7", + "ios": true, + "android": true, + "multi": true + }, + { + "version": "1.1", + "min_godot": "4.6", + "max_godot": "4.6.3", + "ios": true, + "android": true, + "multi": true + }, + { + "version": "1.0", + "min_godot": "4.5", + "max_godot": "4.5.2", + "ios": true, + "android": true, + "multi": true + } + ], + "sponsorship_url": "https://github.com/sponsors/cengiz-pz" + }, + "deeplink": { + "name": "Deeplink Plugin", + "directory": "DeeplinkPlugin", + "description": "A Godot mobile plugin for handling deep links in your Godot games. This plugin allows you to capture and process deep link URLs, enabling seamless integration with external services and improved user experience.", + "author": "Godot Mobile Plugins (github.com/godot-mobile-plugins)", + "repository": "https://github.com/godot-mobile-plugins/godot-deeplink", + "versions": [ + { + "version": "6.0", + "min_godot": "4.7", + "ios": true, + "android": true, + "multi": true + }, + { + "version": "5.3", + "min_godot": "4.6", + "max_godot": "4.6.3", + "ios": true, + "android": true, + "multi": true + }, + { + "version": "5.2", + "min_godot": "4.5", + "max_godot": "4.5.2", + "ios": true, + "android": true, + "multi": true + } + ], + "sponsorship_url": "https://github.com/sponsors/cengiz-pz" + }, + "firebase": { + "name": "Firebase Plugin", + "directory": "FirebasePlugin", + "description": "A Godot mobile plugin for integrating Firebase services into your Godot games. This plugin provides seamless integration with Firebase Authentication, Firestore, and other Firebase features, enabling powerful backend functionality for your mobile games.", + "author": "Godot Mobile Plugins (github.com/godot-mobile-plugins)", + "repository": "https://github.com/godot-mobile-plugins/godot-firebase", + "versions": [ + { + "version": "1.0", + "min_godot": "4.7", + "ios": true, + "android": true, + "multi": true + } + ], + "sponsorship_url": "https://github.com/sponsors/cengiz-pz" + }, + "inapp_review": { + "name": "In-App Review Plugin", + "directory": "InappReviewPlugin", + "description": "A Godot mobile plugin for handling in-app reviews in your Godot games. This plugin allows you to request and display in-app review prompts, encouraging users to rate and review your mobile games.", + "author": "Godot Mobile Plugins (github.com/godot-mobile-plugins)", + "repository": "https://github.com/godot-mobile-plugins/godot-inapp-review", + "versions": [ + { + "version": "5.3", + "min_godot": "4.7", + "ios": true, + "android": true, + "multi": true + }, + { + "version": "5.2", + "min_godot": "4.6", + "max_godot": "4.6.3", + "ios": true, + "android": true, + "multi": true + }, + { + "version": "5.1", + "min_godot": "4.5", + "max_godot": "4.5.2", + "ios": true, + "android": true, + "multi": true + } + ], + "sponsorship_url": "https://github.com/sponsors/cengiz-pz" + }, + "native_camera": { + "name": "Native Camera Plugin", + "directory": "NativeCameraPlugin", + "description": "A Godot mobile plugin for accessing the native camera functionality in your Godot games. This plugin provides a simple and efficient way to capture photos and record videos directly from your mobile devices.", + "author": "Godot Mobile Plugins (github.com/godot-mobile-plugins)", + "repository": "https://github.com/godot-mobile-plugins/godot-native-camera", + "versions": [ + { + "version": "3.0", + "min_godot": "4.7", + "ios": true, + "android": true, + "multi": true + }, + { + "version": "1.1", + "min_godot": "4.6", + "max_godot": "4.6.3", + "ios": true, + "android": true, + "multi": true + }, + { + "version": "1.0", + "min_godot": "4.5", + "max_godot": "4.5.2", + "ios": true, + "android": true, + "multi": true + } + ], + "sponsorship_url": "https://github.com/sponsors/cengiz-pz" + }, + "notification_scheduler": { + "name": "Notification Scheduler Plugin", + "directory": "NotificationSchedulerPlugin", + "description": "A Godot mobile plugin for scheduling notifications in your Godot games. This plugin allows you to set up and manage notifications, ensuring that players are informed about important events and updates.", + "author": "Godot Mobile Plugins (github.com/godot-mobile-plugins)", + "repository": "https://github.com/godot-mobile-plugins/godot-notification-scheduler", + "versions": [ + { + "version": "5.3", + "min_godot": "4.7", + "ios": true, + "android": true, + "multi": true + }, + { + "version": "5.2", + "min_godot": "4.6", + "max_godot": "4.6.3", + "ios": true, + "android": true, + "multi": true + }, + { + "version": "5.1", + "min_godot": "4.5", + "max_godot": "4.5.2", + "ios": true, + "android": true, + "multi": true + }, + { + "version": "5.0", + "min_godot": "4.4", + "max_godot": "4.4.1", + "ios": true, + "android": true, + "multi": true + } + ], + "sponsorship_url": "https://github.com/sponsors/cengiz-pz" + }, + "oauth2": { + "name": "OAuth2 Plugin", + "directory": "OAuth2Plugin", + "description": "A Godot mobile plugin for integrating OAuth2 authentication into your Godot games. This plugin provides a simple and efficient way to handle OAuth2 flows, enabling seamless integration with various OAuth2 providers.", + "author": "Godot Mobile Plugins (github.com/godot-mobile-plugins)", + "repository": "https://github.com/godot-mobile-plugins/godot-oauth2", + "versions": [ + { + "version": "2.0", + "min_godot": "4.7", + "ios": true, + "android": true, + "multi": true + }, + { + "version": "1.1", + "min_godot": "4.6", + "max_godot": "4.6.3", + "ios": true, + "android": true, + "multi": true + }, + { + "version": "1.0", + "min_godot": "4.5", + "max_godot": "4.5.2", + "ios": true, + "android": true, + "multi": true + } + ], + "sponsorship_url": "https://github.com/sponsors/cengiz-pz" + }, + "qr": { + "name": "QR Plugin", + "directory": "QRPlugin", + "description": "A Godot mobile plugin for scanning and generating QR codes in your Godot games. This plugin provides a simple and efficient way to integrate QR code functionality into your mobile games.", + "author": "Godot Mobile Plugins (github.com/godot-mobile-plugins)", + "repository": "https://github.com/godot-mobile-plugins/godot-qr", + "versions": [ + { + "version": "1.2", + "min_godot": "4.7", + "ios": true, + "android": true, + "multi": true + }, + { + "version": "1.1", + "min_godot": "4.6", + "max_godot": "4.6.3", + "ios": true, + "android": true, + "multi": true + }, + { + "version": "1.0", + "min_godot": "4.5", + "max_godot": "4.5.2", + "ios": true, + "android": true, + "multi": true + } + ], + "sponsorship_url": "https://github.com/sponsors/cengiz-pz" + }, + "Share": { + "name": "Share Plugin", + "directory": "SharePlugin", + "description": "A Godot mobile plugin for sharing content in your Godot games. This plugin provides a simple and efficient way to share text, images, and other media directly from your mobile games.", + "author": "Godot Mobile Plugins (github.com/godot-mobile-plugins)", + "repository": "https://github.com/godot-mobile-plugins/godot-share", + "versions": [ + { + "version": "6.0", + "min_godot": "4.7", + "ios": true, + "android": true, + "multi": true + }, + { + "version": "5.2", + "min_godot": "4.6", + "max_godot": "4.6.3", + "ios": true, + "android": true, + "multi": true + }, + { + "version": "5.1.1", + "min_godot": "4.5", + "max_godot": "4.5.2", + "ios": true, + "android": true, + "multi": true + }, + { + "version": "5.0", + "min_godot": "4.3", + "max_godot": "4.4.1", + "ios": true, + "android": true, + "multi": true + } + ], + "sponsorship_url": "https://github.com/sponsors/cengiz-pz" + }, + "Vision": { + "name": "Vision Plugin", + "directory": "VisionPlugin", + "description": "A Godot mobile plugin for integrating computer vision functionality into your Godot games. This plugin provides a simple and efficient way to analyze images and video feeds, enabling advanced gameplay features and interactive experiences.", + "author": "Godot Mobile Plugins (github.com/godot-mobile-plugins)", + "repository": "https://github.com/godot-mobile-plugins/godot-vision", + "versions": [ + { + "version": "1.1", + "min_godot": "4.7", + "ios": true, + "android": true, + "multi": true + } + ], + "sponsorship_url": "https://github.com/sponsors/cengiz-pz" + } +} diff --git a/addon/src/shared/menu/plugin.cfg b/addon/src/shared/menu/plugin.cfg new file mode 100644 index 0000000..6e3bb23 --- /dev/null +++ b/addon/src/shared/menu/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="GMP Menu" +description="Adds a root-level GMP menu with all plugins provided by GMP." +author="Godot Mobile Plugins (github.com/godot-mobile-plugins)" +version="1.0" +script="GmpMenu.gd" diff --git a/android/android-build.gradle.kts b/android/android-build.gradle.kts index 9ec19e3..2367d3b 100644 --- a/android/android-build.gradle.kts +++ b/android/android-build.gradle.kts @@ -618,6 +618,26 @@ tasks { } } + register("checkAndroidFormat") { + description = "Validates format in all source code" + group = "verification" + dependsOn( + project(":android").tasks.named("checkJavaFormat"), + project(":android").tasks.named("checkKotlinFormat"), + project(":android").tasks.named("checkXmlFormat"), + ) + } + + register("applyAndroidFormat") { + description = "Formats all source code" + group = "formatting" + dependsOn( + project(":android").tasks.named("rewriteRun"), + project(":android").tasks.named("formatKotlinSource"), + project(":android").tasks.named("formatXml"), + ) + } + named("preBuild") { dependsOn("downloadGodotAar") } diff --git a/android/src/test/java/org/godotengine/plugin/plugintemplate/PluginTemplatePluginTest.java b/android/src/test/java/org/godotengine/plugin/plugintemplate/PluginTemplatePluginTest.java index f8777f1..1c425fd 100644 --- a/android/src/test/java/org/godotengine/plugin/plugintemplate/PluginTemplatePluginTest.java +++ b/android/src/test/java/org/godotengine/plugin/plugintemplate/PluginTemplatePluginTest.java @@ -166,7 +166,7 @@ public void getPluginTemplate_returnsEmptyArrayByDefault() { @Test public void getPluginTemplate_isIdempotent() { - Object[] first = plugin.get_plugin_template(); + Object[] first = plugin.get_plugin_template(); Object[] second = plugin.get_plugin_template(); assertArrayEquals( first, diff --git a/common/build-logic/logic.gradle.kts b/common/build-logic/logic.gradle.kts index f586109..6ba83fe 100644 --- a/common/build-logic/logic.gradle.kts +++ b/common/build-logic/logic.gradle.kts @@ -14,9 +14,30 @@ plugins { `kotlin-dsl` + `java-gradle-plugin` alias(libs.plugins.kotlin.serialization) } +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +kotlin { + jvmToolchain(17) +} + +gradlePlugin { + plugins { + create("baseConventions") { + id = "base-conventions" + implementationClass = "BaseConventionsPlugin" + description = "Godot Mobile Plugins base conventions plugin" + } + } +} + val buildLogicDependencies = extensions .getByType() @@ -28,9 +49,9 @@ val buildLogicDependencies = } dependencies { - println("DEBUG: BUILD LOGIC IMPLEMENTATION Dependencies") + implementation(gradleKotlinDsl()) + buildLogicDependencies.forEach { - println("DEBUG: Adding to runtime: $it") implementation(it) } } @@ -38,20 +59,10 @@ dependencies { sourceSets { main { java.srcDirs("src/main/java") + resources.srcDirs("src/main/resources") } } -kotlin { - jvmToolchain(17) - - sourceSets { - getByName("main") { - kotlin.srcDir("src/main/java") - } - } -} - -tasks.withType().configureEach { - targetCompatibility = "17" - sourceCompatibility = "17" +kotlin.sourceSets.getByName("main") { + kotlin.srcDirs("src/main/kotlin", "src/main/java") } diff --git a/common/gradle/libs.versions.toml b/common/gradle/libs.versions.toml index e4619c3..8a09e17 100644 --- a/common/gradle/libs.versions.toml +++ b/common/gradle/libs.versions.toml @@ -27,7 +27,8 @@ mockk = "1.13.17" opengl-api = "gl1.1-android-2.1_r1" # -- Godot plugin dependencies ------------------------------------------------- -appcompat = "1.7.1" +androidx-appcompat = "1.7.1" + [libraries] @@ -46,7 +47,8 @@ test-runtime-junit-platform-launcher = { module = "org.junit.platform:junit-plat test-runtime-opengl-api = { module = "org.khronos:opengl-api", version.ref = "opengl-api" } # -- Godot plugin -------------------------------------------------------------- -runtime-androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } +runtime-androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } + [plugins] diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 105ff59..9cfb52e 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -109,7 +109,8 @@ Thank you for your interest in contributing to the Godot PluginTemplate Plugin! │ ├-- config/ │ │ ├-- ios.properties # iOS configuration │ │ ├-- ios-build.properties # Gradle build customization for ios module -│ │ ├-- spm_dependencies.json # SPM dependency configuration +│ │ ├-- spm_dependencies.json # SPM dependency configuration (main plugin target) +│ │ ├-- spm_test_dependencies.json # Optional: SPM dependency configuration (test target only) │ │ └-- *.gdip # Godot iOS plugin config │ │ │ └-- godot/ # Downloaded Godot source (default location; configurable via local.properties) @@ -154,7 +155,7 @@ val pluginConfig = loadPluginConfig() val godotConfig = loadGodotConfig() val iosConfig = loadIosConfig() -println(pluginConfig.pluginName) // "PluginTemplatePlugin" +println(pluginConfig.pluginName) // "*Plugin" println(godotConfig.godotAarUrl) // full GitHub download URL println(iosConfig.frameworks) // List - already parsed ``` @@ -206,6 +207,8 @@ const SPM_DEPENDENCIES: Array = [ @spmDependencies@ ] const SPM_DEPENDENCIES: Array = [ {&"url": "https://github.com/owner/repo", &"version": "1.2.3", &"products": ["ProductA", "ProductB"]}, {&"url": "https://github.com/other/pkg", &"version": "2.0.0", &"products": ["ProductC"]} ] ``` +> **Note:** `IosConfig.spmDependencies` only reflects `ios/config/spm_dependencies.json` (the **main plugin target**'s dependencies). The optional `ios/config/spm_test_dependencies.json` file (test-target-only SPM dependencies) is read directly by the iOS build's Gradle tasks at execution time and is **not** part of `IosConfig` or the `@spmDependencies@` token — see [SPM Dependencies](#-ios-configuration) below. + --- ## Prerequisites @@ -268,6 +271,7 @@ These tools are needed when running `checkFormat` or `applyFormat` tasks: - **clang-format** - ObjC/C++ formatter: `brew install clang-format` (iOS only) - **swiftlint** - Swift linter/formatter: `brew install swiftlint` (iOS only) - **gdformat** - GDScript formatter: install via the Godot toolchain +- **rubocop** - Ruby linter/formatter: `gem install rubocop` (also used to lint `script/spm_manager.rb`) ### Verifying Prerequisites @@ -315,7 +319,8 @@ The build files are static and shared across all GMP plugins. Any plugin-specifi └-- config/ ├-- ios.properties # iOS configuration ├-- ios-build.properties # Gradle build customization for ios module - └-- spm_dependencies.json # SPM dependency configuration + ├-- spm_dependencies.json # SPM dependency configuration (main plugin target) + └-- spm_test_dependencies.json # Optional: SPM dependency configuration (test target only) ``` ### Common Configuration @@ -469,10 +474,17 @@ flags=-ObjC,-Wl,... # Files to include in the app bundle bundle_files=res://assets/VisionPlugin/face_landmarker.task + +# iOS Simulator used by the testiOS task (see iOS Unit Tests below) +test_platform=iOS Simulator +test_destination_name=iPhone 17 +test_os=latest ``` The `frameworks`, `embedded_frameworks`, `flags`, and `bundle_files` values are comma-separated lists. The build system parses them into typed lists at configuration time (`IosConfig.kt`) - blank entries are ignored. Values are used as-is for token replacement in GDScript templates and passed directly to `xcodebuild`. +`test_platform`, `test_destination_name`, and `test_os` configure the simulator used to run iOS unit tests - they are matched against `xcrun simctl list devices` to boot a simulator and build the `xcodebuild -destination` value (see [iOS Unit Tests](#ios-unit-tests)). Unlike the other `ios.properties` values, these are **not** exposed as GDScript tokens. + GDScript templates may reference the following tokens for iOS values set in `ios.properties` and `spm_dependencies.json`: | Token | Source | GDScript type | @@ -515,6 +527,12 @@ If the plugin has no SPM dependencies: ] ``` +Dependencies declared in `spm_dependencies.json` are added to the **main plugin target** (`_plugin`) as compile-only (`--no-link`) dependencies via `script/spm_manager.rb` - they're available for the Swift compiler to resolve imports against, but are not linked into the plugin's static library. The consuming Godot app links them independently at export time using the committed `Package.resolved`. + +#### Test-only SPM Dependencies + +If a package is needed only by the XCTest target (e.g. a mocking or test-assertion library), declare it in the optional `ios/config/spm_test_dependencies.json` file, using the same `url`/`version`/`products` format as `spm_dependencies.json`. Dependencies in this file are added to the `_plugin_tests` target as normal (compile **and** link) dependencies and are never injected into the main plugin target. If the file does not exist, no test-only dependencies are added and `updateSPMDependencies` logs a warning and skips that step. + --- ## Development Workflow @@ -590,6 +608,7 @@ Cross-platform builds with the `build.sh` script. |--------|-------------| | `-a` | Build plugin for Android platform (`-a -- -h` for all options) | | `-i` | Build plugin for iOS platform (`-i -- -h` for all options) | +| `-b` | Build plugin for both Android and iOS platforms | | `-c` | Remove existing builds | | `-C` | Remove existing builds and archives | | `-d` | Uninstall plugin from demo app | @@ -599,6 +618,7 @@ Cross-platform builds with the `build.sh` script. | `-I` | Create iOS release archive | | `-M` | Create multi-platform release archive | | `-R` | Create all release archives | +| `-t` | Run tests (Android + iOS) | | `-v` | Verify source code format compliance | #### Output Locations @@ -642,10 +662,12 @@ Cross-platform builds with the `build.sh` script. | `-c` | Clean Android build | | `-d` | Uninstall Android plugin from demo app | | `-D` | Install Android plugin to demo app | +| `-f` | Fix source code format issues | | `-h` | Display script usage information | | `-r` | Build Android plugin with release build variant | | `-R` | Create Android release archive | | `-t` | Run Android unit tests (prints per-suite pass/fail table and code coverage) | +| `-v` | Verify source code format compliance | #### Android Studio @@ -702,6 +724,7 @@ If using Android Studio, make sure to open the root Gradle project from the `com | `-c` | Clean existing build | | `-d` | Uninstall iOS plugin from demo app | | `-D` | Install iOS plugin to demo app | +| `-f` | Fix source code format issues | | `-g` | Remove Godot headers directory | | `-G` | Download Godot headers | | `-h` | Display help | @@ -711,6 +734,7 @@ If using Android Studio, make sure to open the root Gradle project from the `com | `-R` | Create release archive | | `-s` | Simulator build; use with `-b` for simulator debug, `-B` for simulator release | | `-t` | Run iOS unit tests (requires macOS and Xcode; Godot headers must be present) | +| `-v` | Verify source code format compliance | #### Build Process Explained @@ -732,18 +756,19 @@ The iOS build process involves several steps that are orchestrated automatically - Confirms the `GODOT_VERSION` file in the Godot headers directory matches `godotVersion` in `godot.properties` 4. **Update & Resolve SPM Packages**: - - Reads dependency definitions from `ios/config/spm_dependencies.json` - - Injects package references into the Xcode project via `script/spm_manager.rb` (requires Ruby and the `xcodeproj` gem) - - Resolves the packages with `xcodebuild -resolvePackageDependencies` + - Reads main-target dependency definitions from `ios/config/spm_dependencies.json` and injects them into the `_plugin` target via `script/spm_manager.rb` (requires Ruby and the `xcodeproj` gem) as compile-only (`--no-link`) dependencies + - If `ios/config/spm_test_dependencies.json` exists, injects its dependencies into the `_plugin_tests` target as normal compile-and-link dependencies (see [Test-only SPM Dependencies](#test-only-spm-dependencies)) + - Resolves both the plugin and test schemes with `xcodebuild -resolvePackageDependencies` 5. **Build XCFrameworks**: - - Builds up to four variants via `xcodebuild archive`: + - Builds up to four variants via `xcodebuild archive` against the `plugin.xcodeproj/project.xcworkspace` workspace: - `buildiOSDebug` - device (arm64), debug - `buildiOSRelease` - device (arm64), release - `buildiOSDebugSimulator` - simulator (arm64/x86_64), debug - `buildiOSReleaseSimulator` - simulator (arm64/x86_64), release - The `-s` flag selects simulator variants; without it, device variants are built - Archives are created as `.xcarchive` bundles under `ios/build/lib/` + - After archiving, any compiled object files belonging to linked SPM dependencies are stripped from the plugin's own static library with `ar -d` (Xcode's `--no-link` only prevents Frameworks-phase linking - it still archives every compiled SPM object into the `.a` via `libtool`), so the resulting `{PluginName}.a` contains only the plugin's own object files - XCFrameworks combining device and simulator slices are assembled in `ios/build/framework/` - **Only the plugin's own xcframeworks** (`PluginName.debug.xcframework`, `PluginName.release.xcframework`) are copied into the plugin directory and included in release archives - SPM dependency xcframeworks produced in `ios/build/DerivedData/` are **not** bundled in the archive; they are resolved by Xcode at Godot iOS export time using the `Package.resolved` file that is committed alongside the Xcode project @@ -751,6 +776,7 @@ The iOS build process involves several steps that are orchestrated automatically #### Output Locations - **Godot headers:** `ios/godot/` (default) or path set by `godot.dir` in `common/local.properties` +- **Godot iOS Simulator library (test runs only):** `/bin/libgodot.ios.template_debug.arm64.simulator.a`, downloaded automatically by `downloadGodotiOSLibrary` the first time `testiOS` runs - **Build artifacts:** `ios/build/` - **xcarchives:** `ios/build/lib/ios_debug.xcarchive`, `ios_release.xcarchive`, `sim_debug.xcarchive`, `sim_release.xcarchive` - **Plugin XCFrameworks:** `ios/build/framework/PluginTemplatePlugin.debug.xcframework`, `PluginTemplatePlugin.release.xcframework` @@ -783,6 +809,7 @@ These delegate to the following per-language Gradle sub-tasks: | `checkSwiftFormat` | `formatSwiftSource` | Swift | swiftlint | ios | | `checkKtsFormat` | `formatKtsSource` | Gradle KTS | ktlint | common | | `checkBashScriptFormat` | `applyBashScriptFormat` | Bash | shellcheck | common | +| `checkRubyScriptFormat` | `applyRubyScriptFormat` | Ruby | Rubocop | common | | `checkEditorConfig` | _(n/a)_ | All files | editorconfig-checker | common | Sub-tasks can also be run individually. For example, to check only GDScript formatting: @@ -792,7 +819,7 @@ cd common ./gradlew :addon:checkGdscriptFormat ``` -Sub-tasks that require external tools (`ktlint`, `shellcheck`, `editorconfig-checker`, `clang-format`, `swiftlint`, `gdformat`) will fail with a clear error if the tool is not found on `PATH`. See [Prerequisites](#-prerequisites) for installation instructions. +Sub-tasks that require external tools (`ktlint`, `shellcheck`, `editorconfig-checker`, `clang-format`, `swiftlint`, `gdformat`, `rubocop`) will fail with a clear error if the tool is not found on `PATH`. See [Prerequisites](#-prerequisites) for installation instructions. --- @@ -880,12 +907,12 @@ iOS tests are written with **XCTest** and live under `ios/test/unit/`. There are | `PluginTemplateTests.swift` | Swift | `PluginTemplate` class — init, `isActiveKey`, `onThisHappened` callback, ObjC bridging, concurrency | | `PluginTemplateLoggerTests.mm` | Objective-C++ | `plugin_template_log` global — initialization, all log levels, format string handling | -The tests are compiled into the `plugin_template_plugin_tests` Xcode test bundle target, which declares a dependency on the main `plugin_template_plugin` static library target. The Xcode scheme used by the build system is `plugin_template_plugin_tests`. +The tests are compiled into the `plugin_template_plugin_tests` Xcode test bundle target, which declares a dependency on the main `plugin_template_plugin` static library target. The Xcode scheme used by the build system is `plugin_template_plugin_tests`, run against the `plugin.xcodeproj/project.xcworkspace` workspace (rather than the bare `.xcodeproj`) so that SPM test dependencies resolve correctly. #### Running iOS tests ```bash -# Run all iOS tests and print a pass/fail summary +# Run all iOS tests and print a pass/fail + coverage summary ./script/build_ios.sh -t # Or via Gradle directly @@ -893,20 +920,37 @@ cd common ./gradlew :ios:testiOS ``` -> **Note:** iOS tests require macOS and Xcode. The Godot headers must already be present in `ios/godot/` (or the path configured by `godot.dir`). If they are missing, run `./script/build_ios.sh -G` first. +> **Note:** iOS tests require macOS and Xcode. Godot headers and the Godot iOS Simulator debug library are downloaded automatically as part of the `testiOS` task graph the first time tests run (skipped if already present) - there's no need to run `./script/build_ios.sh -G` first. #### What the pipeline does -The `testiOS` Gradle task runs: +Running `testiOS` triggers the following, in order: -``` -xcodebuild test - -project ios/plugin.xcodeproj - -scheme plugin_template_plugin_tests - -destination 'platform=iOS Simulator,...' -``` +1. **Download prerequisites** - `downloadGodotHeaders` and `downloadGodotiOSLibrary` fetch the Godot headers and the `libgodot.ios.template_debug.arm64.simulator.a` static library into the configured Godot directory. +2. **Validate configuration** - `validateGodotVersion` and `validateSwiftVersion` check the downloaded Godot version and the configured `swift_version`; `syncSwiftVersionToPbxproj` writes `swift_version` into `project.pbxproj`. +3. **Resolve SPM dependencies** - `resolveSPMDependencies` resolves both the plugin and test schemes. +4. **Boot the simulator** - `bootiOSSimulator` looks up the simulator named by `test_destination_name` in `ios/config/ios.properties` via `xcrun simctl list devices`, boots it headlessly, and waits (up to 30s) for it to finish booting. +5. **Run the tests**, with code coverage enabled, against the booted simulator: + ``` + xcodebuild test + -workspace ios/plugin.xcodeproj/project.xcworkspace + -scheme plugin_template_plugin_tests + -destination 'id=' + -derivedDataPath ios/build/DerivedData + -resultBundlePath ios/build/TestResults/testiOS.xcresult + -enableCodeCoverage YES + GODOT_DIR= + SWIFT_VERSION= + ``` + If no simulator UDID is available (e.g. `bootiOSSimulator` was skipped and `SIMULATOR_UDID` isn't set in the environment), the destination falls back to `platform=,name=,OS=` built from `ios.properties`. +6. **Print a summary** - `testiOS` is `finalizedBy` `printTestSummaryiOS`, which reads the `.xcresult` bundle with `xcrun xcresulttool` and `xcrun xccov` and prints total/passed/failed/skipped counts, pass rate, per-configuration results, per-suite results, and per-target line coverage. + +The `testiOS` task itself exits non-zero if any test fails. + +#### Output -The task exits non-zero if any test fails and prints `xcodebuild`'s test output to the console. +- **Test result bundle:** `ios/build/TestResults/testiOS.xcresult` +- **Console summary:** printed automatically after every `testiOS` run by `printTestSummaryiOS` #### Xcode project wiring @@ -1408,6 +1452,14 @@ gem install xcodeproj --user-install # The build system will also install it automatically if Ruby is available. ``` +**Problem:** `ERROR: Simulator '' not found` when running tests +```bash +# bootiOSSimulator matches test_destination_name from ios/config/ios.properties +# against available simulators. Solution: list available simulators and update +# test_destination_name to match an installed device: +xcrun simctl list devices available +``` + #### Tests **Problem:** Android tests fail with `ClassNotFoundException` for JUnit 5 @@ -1450,6 +1502,15 @@ grep "plugin_template_plugin_tests" ios/plugin.xcodeproj/project.pbxproj # available in the XCTest host. Move those tests to GDScript integration tests. ``` +**Problem:** `printTestSummaryiOS` reports "No xcresult bundle found" +```bash +# testiOS didn't produce a result bundle - usually because plugin.xcodeproj/ +# project.xcworkspace is missing, or bootiOSSimulator failed to boot a +# simulator. Verify the simulator exists, then re-run: +xcrun simctl list devices available +./script/build_ios.sh -t +``` + ### Getting Help - Check existing [GitHub Issues](https://github.com/godot-mobile-plugins/godot-plugin-template/issues) @@ -1524,5 +1585,3 @@ Include: - [Android Developer Documentation](https://developer.android.com/) - [iOS Developer Documentation](https://developer.apple.com/documentation/) - [Gradle Documentation](https://docs.gradle.org/) - ---- diff --git a/docs/README.md b/docs/README.md index 43e675f..d093399 100644 --- a/docs/README.md +++ b/docs/README.md @@ -211,15 +211,15 @@ func _on_template_ready(template: PluginTemplateInfo): | ✦ | Plugin | Android | iOS | Latest Release | Downloads | Stars | | :--- | :--- | :---: | :---: | :---: | :---: | :---: | | | [Admob](https://github.com/godot-sdk-integrations/godot-admob) | ✅ | ✅ | | | | -| | [Connection State](https://github.com/godot-mobile-plugins/godot-connection-state) | ✅ | ✅ | | | | -| | [Deeplink](https://github.com/godot-mobile-plugins/godot-deeplink) | ✅ | ✅ | | | | +| | [Connection State](https://github.com/godot-mobile-plugins/godot-connection-state) | ✅ | ✅ | | | | +| | [Deeplink](https://github.com/godot-mobile-plugins/godot-deeplink) | ✅ | ✅ | | | | | | [Firebase](https://github.com/godot-mobile-plugins/godot-firebase) | ✅ | ✅ | | | | -| | [In-App Review](https://github.com/godot-mobile-plugins/godot-inapp-review) | ✅ | ✅ | | | | +| | [In-App Review](https://github.com/godot-mobile-plugins/godot-inapp-review) | ✅ | ✅ | | | | | | [Native Camera](https://github.com/godot-mobile-plugins/godot-native-camera) | ✅ | ✅ | | | | -| | [Notification Scheduler](https://github.com/godot-mobile-plugins/godot-notification-scheduler) | ✅ | ✅ | | | | -| | [OAuth 2.0](https://github.com/godot-mobile-plugins/godot-oauth2) | ✅ | ✅ | | | | -| | [QR](https://github.com/godot-mobile-plugins/godot-qr) | ✅ | ✅ | | | | -| | [Share](https://github.com/godot-mobile-plugins/godot-share) | ✅ | ✅ | | | | +| | [Notification Scheduler](https://github.com/godot-mobile-plugins/godot-notification-scheduler) | ✅ | ✅ | | | | +| | [OAuth 2.0](https://github.com/godot-mobile-plugins/godot-oauth2) | ✅ | ✅ | | | | +| | [QR](https://github.com/godot-mobile-plugins/godot-qr) | ✅ | ✅ | | | | +| | [Share](https://github.com/godot-mobile-plugins/godot-share) | ✅ | ✅ | | | | | | [Vision](https://github.com/godot-mobile-plugins/godot-vision) | ✅ | ✅ | | | | | | [Plugin Template](https://github.com/godot-mobile-plugins/godot-plugin-template) | ✅ | ✅ | - | - | | diff --git a/ios/ios-build.gradle.kts b/ios/ios-build.gradle.kts index b61545f..39e3487 100644 --- a/ios/ios-build.gradle.kts +++ b/ios/ios-build.gradle.kts @@ -396,18 +396,20 @@ fun TaskContainerScope.registerSwiftFormatTask( .sorted() if (sourceFiles.isEmpty()) { - throw GradleException("$name: no Swift source files found under ${projectDir.absolutePath}") + logger.lifecycle("$name: No Swift source files found under ${projectDir.absolutePath}. Skipping.") + // Provide a dummy command so the Exec task doesn't fail with an empty command line error + commandLine("echo", "No Swift source files found.") + } else { + commandLine( + buildList { + add("swiftlint") + if (fix) add("--fix") else add("lint") + add("--config") + add("../.github/config/.swiftlint.yml") + addAll(sourceFiles) + }, + ) } - - commandLine( - buildList { - add("swiftlint") - if (fix) add("--fix") else add("lint") - add("--config") - add("../.github/config/.swiftlint.yml") - addAll(sourceFiles) - }, - ) } } } @@ -671,12 +673,7 @@ tasks { val spmConfigFile = file("$projectDir/config/spm_dependencies.json") val deps = readSpmDependencies(spmConfigFile) - if (deps.isEmpty()) { - println("Warning: No dependencies found for plugin. Skipping SPM update.") - return@doLast - } - - val totalProducts = deps.sumOf { it.products.size } + var totalProducts = deps.sumOf { it.products.size } println("Found $totalProducts SPM ${if (totalProducts == 1) "dependency" else "dependencies"}:") deps.forEach { dep -> dep.products.forEach { println("\t• $it (${dep.url} @ ${dep.version})") } @@ -708,67 +705,85 @@ tasks { val scriptDir = file("$repositoryRootDir/script") // -- Module target: compile-only (--no-link) ---------------------- - // Firebase frameworks must be in packageProductDependencies so the + // Dependency frameworks must be in packageProductDependencies so the // Swift compiler can resolve their modules (Authentication.swift, // AuthProviding.swift etc. import them). However they must NOT be - // linked into FirebasePlugin.a — the consuming Godot app links them + // linked into *Plugin.a — the consuming Godot app links them // independently, and duplicate symbols would cause export failure. val moduleName = "${pluginConfig.pluginModuleName}_plugin" val testTargetName = "${pluginConfig.pluginModuleName}_plugin_tests" - println("Updating Xcode project with SPM dependencies...") - println(" - Module target '$moduleName' (compile-only, not linked):") - deps.forEach { dep -> - dep.products.forEach { product -> - println(" • $product") - execOps.exec { - commandLine( - "ruby", - "$scriptDir/spm_manager.rb", - "-a", - "--target", - moduleName, - "--no-link", - xcodeproj, - dep.url, - dep.version, - product, - ) + if (!deps.isEmpty()) { + println("Updating Xcode project with SPM dependencies...") + println(" - Module target '$moduleName' (compile-only, not linked):") + deps.forEach { dep -> + dep.products.forEach { product -> + println(" • $product") + execOps.exec { + commandLine( + "ruby", + "$scriptDir/spm_manager.rb", + "-a", + "--target", + moduleName, + "--no-link", + xcodeproj, + dep.url, + dep.version, + product, + ) + } } } + + println("SPM update completed for main target.") + } else { + println("Warning: No dependencies found for plugin. Skipping SPM update for main target.") } - // -- Test target: compile + link (normal) -------------------------- - // The test target compiles Swift files directly (not via the .a) and - // must link Firebase frameworks itself. val spmTestConfigFile = file("$projectDir/config/spm_test_dependencies.json") - val testDeps = if (spmTestConfigFile.exists()) readSpmDependencies(spmTestConfigFile) else deps + val testDeps = if (spmTestConfigFile.exists()) readSpmDependencies(spmTestConfigFile) else emptyList() - println(" - Test target '$testTargetName' (compile + link):") + totalProducts = testDeps.sumOf { it.products.size } + println("Found $totalProducts SPM test ${if (totalProducts == 1) "dependency" else "dependencies"}:") testDeps.forEach { dep -> - dep.products.forEach { product -> - println(" • $product") - execOps.exec { - commandLine( - "ruby", - "$scriptDir/spm_manager.rb", - "-a", - "--target", - testTargetName, - xcodeproj, - dep.url, - dep.version, - product, - ) + dep.products.forEach { println("\t• $it (${dep.url} @ ${dep.version})") } + } + println() + + if (!testDeps.isEmpty()) { + // -- Test target: compile + link (normal) -------------------------- + // The test target compiles Swift files directly (not via the .a) and + // must link frameworks itself. + + println(" - Test target '$testTargetName' (compile + link):") + testDeps.forEach { dep -> + dep.products.forEach { product -> + println(" • $product") + execOps.exec { + commandLine( + "ruby", + "$scriptDir/spm_manager.rb", + "-a", + "--target", + testTargetName, + xcodeproj, + dep.url, + dep.version, + product, + ) + } } } - } - println("SPM update completed.") + println("SPM update completed for test target.") + } else { + println("Warning: No test dependencies found for plugin. Skipping SPM update for test target.") + } } } - register("resolveSPMDependencies") { + register("resolveSPMDependencies") { description = "Resolves SPM package dependencies via xcodebuild" group = "setup" @@ -783,19 +798,41 @@ tasks { outputs.file("$xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved") outputs.dir(derivedDataDir.resolve("SourcePackages")) - isIgnoreExitValue = true - - commandLine( - "xcodebuild", - "-resolvePackageDependencies", - "-project", - xcodeproj, - "-scheme", - "${pluginConfig.pluginModuleName}_plugin", - "-derivedDataPath", - derivedDataDir.absolutePath, - "GODOT_DIR=$godotDir", - ) + val execOps = objects.newInstance().execOps + + doLast { + // Resolve main plugin scheme + execOps.exec { + commandLine( + "xcodebuild", + "-resolvePackageDependencies", + "-project", + xcodeproj, + "-scheme", + "${pluginConfig.pluginModuleName}_plugin", + "-derivedDataPath", + derivedDataDir.absolutePath, + "GODOT_DIR=$godotDir", + ) + isIgnoreExitValue = true + } + + // Resolve test scheme + execOps.exec { + commandLine( + "xcodebuild", + "-resolvePackageDependencies", + "-project", + xcodeproj, + "-scheme", + "${pluginConfig.pluginModuleName}_plugin_tests", + "-derivedDataPath", + derivedDataDir.absolutePath, + "GODOT_DIR=$godotDir", + ) + isIgnoreExitValue = true + } + } } register("validateSwiftVersion") { @@ -1294,4 +1331,22 @@ tasks { "Formats Swift source files in-place using swiftlint --fix", fix = true, ) + + register("checkiOSFormat") { + description = "Validates format in all source code" + group = "verification" + dependsOn( + project(":ios").tasks.named("checkObjCFormat"), + project(":ios").tasks.named("checkSwiftFormat"), + ) + } + + register("applyiOSFormat") { + description = "Formats all source code" + group = "formatting" + dependsOn( + project(":ios").tasks.named("formatObjCSource"), + project(":ios").tasks.named("formatSwiftSource"), + ) + } } diff --git a/ios/plugin.xcodeproj/project.pbxproj b/ios/plugin.xcodeproj/project.pbxproj index 1702c65..3378733 100644 --- a/ios/plugin.xcodeproj/project.pbxproj +++ b/ios/plugin.xcodeproj/project.pbxproj @@ -21,16 +21,6 @@ 078A3A4B2F336DA7008B4674 /* PluginTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07C98E082F3348BA002D3480 /* PluginTemplate.swift */; }; /* End PBXBuildFile section */ -/* Begin PBXContainerItemProxy section */ - 0721C6A32F82629000A9BA75 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 90CAAA8C24E71FF10013969F /* Project object */; - proxyType = 1; - remoteGlobalIDString = 90CAAA9324E71FF10013969F; - remoteInfo = plugin_template_plugin; - }; -/* End PBXContainerItemProxy section */ - /* Begin PBXCopyFilesBuildPhase section */ 90CAAA9224E71FF10013969F /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; @@ -162,11 +152,8 @@ buildRules = ( ); dependencies = ( - 0721C6A42F82629000A9BA75 /* PBXTargetDependency */, ); name = plugin_template_plugin_tests; - packageProductDependencies = ( - ); productName = plugin_template_plugin_tests; productReference = 0721C69E2F82629000A9BA75 /* plugin_template_plugin_tests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; @@ -264,14 +251,6 @@ }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXTargetDependency section */ - 0721C6A42F82629000A9BA75 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 90CAAA9324E71FF10013969F /* plugin_template_plugin */; - targetProxy = 0721C6A32F82629000A9BA75 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - /* Begin XCBuildConfiguration section */ 0721C6A52F82629000A9BA75 /* Debug */ = { isa = XCBuildConfiguration; @@ -294,7 +273,7 @@ "$(SRCROOT)/test/fixtures", "$(SRCROOT)/src", ); - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.3; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -306,10 +285,6 @@ ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; - OTHER_LDFLAGS = ( - "$(inherited)", - "$(GODOT_DIR)/bin/libgodot.ios.template_debug.$(CURRENT_ARCH).simulator.a", - ); OTHER_CFLAGS = ( "-g", "-DDEBUG", @@ -358,6 +333,12 @@ "-isystem", "$(GODOT_DIR)/platform/ios", ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-undefined", + "dynamic_lookup", + "$(GODOT_DIR)/bin/libgodot.ios.template_debug.$(CURRENT_ARCH).simulator.a", + ); PRODUCT_BUNDLE_IDENTIFIER = "org.godotengine.plugin.plugin-template-unit-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; @@ -392,7 +373,7 @@ "$(SRCROOT)/test/fixtures", "$(SRCROOT)/src", ); - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.3; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -404,10 +385,6 @@ ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; - OTHER_LDFLAGS = ( - "$(inherited)", - "$(GODOT_DIR)/bin/libgodot.ios.template_debug.$(CURRENT_ARCH).simulator.a", - ); OTHER_CFLAGS = ( "-fmodules", "-fobjc-arc", @@ -447,6 +424,12 @@ "-isystem", "$(GODOT_DIR)/platform/ios", ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-undefined", + "dynamic_lookup", + "$(GODOT_DIR)/bin/libgodot.ios.template_debug.$(CURRENT_ARCH).simulator.a", + ); PRODUCT_BUNDLE_IDENTIFIER = "org.godotengine.plugin.plugin-template-unit-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; @@ -511,7 +494,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.3; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -565,7 +548,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.3; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -585,7 +568,7 @@ "$(inherited)", "$(SRCROOT)/src", ); - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.3; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -668,7 +651,7 @@ "$(inherited)", "$(SRCROOT)/src", ); - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.3; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/ios/test/unit/PluginTemplateTests.swift b/ios/test/unit/PluginTemplateTests.swift index 0f4077b..97cd06c 100644 --- a/ios/test/unit/PluginTemplateTests.swift +++ b/ios/test/unit/PluginTemplateTests.swift @@ -2,8 +2,8 @@ // © 2026-present https://github.com/<> // -import XCTest @testable import plugin_template_plugin +import XCTest // MARK: - PluginTemplate Swift Class Tests diff --git a/script/build.sh b/script/build.sh index 64ea575..32fe9f4 100755 --- a/script/build.sh +++ b/script/build.sh @@ -31,11 +31,12 @@ function display_help() echo_yellow "libraries and configuration." echo "$SCRIPT_DIR"/echocolor.sh -Y "Syntax:" - echo_yellow " $0 [-a|A|c|C|d|D|h|i|I|M|R|t|z|Z]" + echo_yellow " $0 [-a|A|b|c|C|d|D|h|i|I|M|R|t|z|Z]" echo "$SCRIPT_DIR"/echocolor.sh -Y "Options:" echo_yellow " a build plugin for the Android platform" echo_yellow " A build and create Android release archive" + echo_yellow " b build Android and iOS platforms" echo_yellow " c remove existing builds" echo_yellow " C remove existing builds and release archives" echo_yellow " d uninstall plugin from demo app" @@ -126,7 +127,7 @@ function display_warning() function run_android_build() { - local build_arguments="$1" + local build_arguments="${1:-"-b"}" display_step "Running Android build script with opts: $build_arguments" @@ -136,7 +137,7 @@ function run_android_build() function run_ios_build() { - local build_arguments="$1" + local build_arguments="${1:-"-b"}" display_step "Running iOS build script with opts: $build_arguments" @@ -144,7 +145,7 @@ function run_ios_build() } -while getopts "aAcCdDfhiIMRtv" option; do +while getopts "aAbcCdDfhiIMRtv" option; do case $option in h) display_help @@ -155,6 +156,10 @@ while getopts "aAcCdDfhiIMRtv" option; do A) do_android_release=true ;; + b) + do_build_android=true + do_build_ios=true + ;; c) do_clean_build=true ;; diff --git a/script/build_android.sh b/script/build_android.sh index 1b00540..5dd51e0 100755 --- a/script/build_android.sh +++ b/script/build_android.sh @@ -14,6 +14,8 @@ do_create_archive=false do_uninstall=false do_install=false do_run_tests=false +do_check_format=false +do_apply_format=false function display_help() @@ -23,18 +25,19 @@ function display_help() echo_yellow "libraries and configuration." echo "$SCRIPT_DIR"/echocolor.sh -Y "Syntax:" - echo_yellow " $0 [-b|c|d|D|h|r|R|t]" + echo_yellow " $0 [-b|c|d|D|f|h|r|R|t|v]" echo "$SCRIPT_DIR"/echocolor.sh -Y "Options:" echo_yellow " b build plugin for the Android platform" echo_yellow " c remove existing Android build" echo_yellow " d uninstall Android plugin from demo app" echo_yellow " D install Android plugin to demo app" + echo_yellow " f fix source code format issues" echo_yellow " h display usage information" echo_yellow " r build Android plugin with release build variant" echo_yellow " R create Android release archive" echo_yellow " t run Android tests (shows per-suite pass/fail table and code coverage)" - echo + echo_yellow " v verify source code format compliance" "$SCRIPT_DIR"/echocolor.sh -Y "Examples:" echo_yellow " * clean existing build, do a release build for Android, and create archive" echo_yellow " $> $0 -cbrR" @@ -54,6 +57,9 @@ function display_help() echo_yellow " * run all Android tests and display coverage summary" echo_yellow " $> $0 -t" echo + echo_yellow " * verify source code format compliance" + echo_yellow " $> $0 -v" + echo } @@ -100,7 +106,7 @@ function display_warning() } -while getopts "bcdDhrRt" option; do +while getopts "bcdDfhrRtv" option; do case $option in h) display_help @@ -117,6 +123,9 @@ while getopts "bcdDhrRt" option; do D) do_install=true ;; + f) + do_apply_format=true + ;; r) gradle_build_task="buildAndroidRelease" ;; @@ -126,6 +135,9 @@ while getopts "bcdDhrRt" option; do t) do_run_tests=true ;; + v) + do_check_format=true + ;; \?) display_error "Invalid option $option" echo @@ -163,12 +175,6 @@ then "$SCRIPT_DIR"/run_gradle_task.sh "createAndroidArchive" fi -if [[ "$do_install" == true ]] -then - display_status "Installing Android plugin to demo app" - "$SCRIPT_DIR"/run_gradle_task.sh "installToDemoAndroid" -fi - if [[ "$do_run_tests" == true ]] then display_status "Running Android tests" @@ -181,3 +187,21 @@ then # tests failed "$SCRIPT_DIR"/run_gradle_task.sh ":android:printTestSummary" fi + +if [[ "$do_check_format" == true ]] +then + display_status "Verifying source code format compliance" + "$SCRIPT_DIR"/run_gradle_task.sh "checkAndroidFormat" +fi + +if [[ "$do_apply_format" == true ]] +then + display_status "Fixing source code format issues" + "$SCRIPT_DIR"/run_gradle_task.sh "applyAndroidFormat" +fi + +if [[ "$do_install" == true ]] +then + display_status "Installing Android plugin to demo app" + "$SCRIPT_DIR"/run_gradle_task.sh "installToDemoAndroid" +fi diff --git a/script/build_ios.sh b/script/build_ios.sh index 1369513..e2e1f49 100755 --- a/script/build_ios.sh +++ b/script/build_ios.sh @@ -20,6 +20,8 @@ do_create_archive=false do_uninstall=false do_install=false do_run_tests=false +do_check_format=false +do_apply_format=false function display_help() @@ -31,7 +33,7 @@ function display_help() echo_yellow "If plugin version is not set with the -z option, then Godot version will be used." echo "$SCRIPT_DIR"/echocolor.sh -Y "Syntax:" - echo_yellow " $0 [-a|A|b|B|c|d|D|g|G|h|p|P|r|R|s|t]" + echo_yellow " $0 [-a|A|b|B|c|d|D|f|g|G|h|p|P|r|R|s|t|v]" echo "$SCRIPT_DIR"/echocolor.sh -Y "Options:" echo_yellow " a update SPM and build both variants of plugin" @@ -42,6 +44,7 @@ function display_help() echo_yellow " c remove any existing plugin build" echo_yellow " d uninstall iOS plugin from demo app" echo_yellow " D install iOS plugin to demo app" + echo_yellow " f fix source code format issues" echo_yellow " g remove directory with godot header files" echo_yellow " G download the configured godot headers version into godot directory" echo_yellow " h display usage information" @@ -51,6 +54,7 @@ function display_help() echo_yellow " R create iOS release archive" echo_yellow " s simulator build; use with -b for simulator debug, -B for simulator release" echo_yellow " t run iOS tests (shows per-suite pass/fail table and code coverage)" + echo_yellow " v verify source code format compliance" echo "$SCRIPT_DIR"/echocolor.sh -Y "Examples:" echo_yellow " * clean existing build, remove godot, and rebuild all" @@ -117,7 +121,7 @@ function display_error() } -while getopts "aAbBcdDgGhpPrRst" option; do +while getopts "aAbBcdDfgGhpPrRstv" option; do case $option in h) display_help @@ -148,6 +152,9 @@ while getopts "aAbBcdDgGhpPrRst" option; do D) do_install=true ;; + f) + do_apply_format=true + ;; g) do_remove_godot=true ;; @@ -172,6 +179,9 @@ while getopts "aAbBcdDgGhpPrRst" option; do t) do_run_tests=true ;; + v) + do_check_format=true + ;; \?) display_error "invalid option" echo @@ -241,14 +251,26 @@ then "$SCRIPT_DIR"/run_gradle_task.sh "createiOSArchive" fi -if [[ "$do_install" == true ]] -then - display_status "Installing iOS plugin to demo app" - "$SCRIPT_DIR"/run_gradle_task.sh "installToDemoiOS" -fi - if [[ "$do_run_tests" == true ]] then display_status "Running iOS tests" "$SCRIPT_DIR"/run_gradle_task.sh ":ios:testiOS" fi + +if [[ "$do_check_format" == true ]] +then + display_status "Verifying source code format compliance" + "$SCRIPT_DIR"/run_gradle_task.sh "checkiOSFormat" +fi + +if [[ "$do_apply_format" == true ]] +then + display_status "Fixing source code format issues" + "$SCRIPT_DIR"/run_gradle_task.sh "applyiOSFormat" +fi + +if [[ "$do_install" == true ]] +then + display_status "Installing iOS plugin to demo app" + "$SCRIPT_DIR"/run_gradle_task.sh "installToDemoiOS" +fi diff --git a/script/spm_manager.rb b/script/spm_manager.rb index 8e307df..bb537e8 100644 --- a/script/spm_manager.rb +++ b/script/spm_manager.rb @@ -156,7 +156,10 @@ def print_usage "[compile-only, not linked] in #{File.basename(project_path)}\n\n" else frameworks_phase = target.frameworks_build_phase - frameworks_phase.add_file_reference(ref) + # Create a new PBXBuildFile that points to the product dependency + build_file = project.new(Xcodeproj::Project::Object::PBXBuildFile) + build_file.product_ref = ref + frameworks_phase.files << build_file puts "Successfully added SPM dependency '#{product_name}' " \ "(#{url} @ #{version}) to target '#{target.name}' " \ "[compile + link] in #{File.basename(project_path)}\n\n"