From f060224cbe1a21d8a41d27552c7838f35092cabc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Pe=C3=B1a?= Date: Thu, 16 Jul 2026 19:06:02 +0800 Subject: [PATCH 1/3] feat: add option to extract downloaded binaries directly in versions folder. Also adapted the local editor deletion flow to allow only deleting the editor binary instead of the parent folder (current behaviour). --- .../editors/local/editor_item/editor_item.gd | 25 ++++++++----- .../local/editors_list/editors_list.gd | 4 +-- src/components/editors/local/local_editors.gd | 36 ++++++++++++++----- .../editors/remote/remote_editors.gd | 18 +++++++--- src/components/settings/settings_window.gd | 6 ++++ src/config.gd | 15 ++++++-- .../editors/resolve_removal_target_test.gd | 32 +++++++++++++++++ .../resolve_removal_target_test.gd.uid | 1 + tests/cases/editors/resolve_unzip_dir_test.gd | 13 +++++++ .../editors/resolve_unzip_dir_test.gd.uid | 1 + 10 files changed, 124 insertions(+), 27 deletions(-) create mode 100644 tests/cases/editors/resolve_removal_target_test.gd create mode 100644 tests/cases/editors/resolve_removal_target_test.gd.uid create mode 100644 tests/cases/editors/resolve_unzip_dir_test.gd create mode 100644 tests/cases/editors/resolve_unzip_dir_test.gd.uid diff --git a/src/components/editors/local/editor_item/editor_item.gd b/src/components/editors/local/editor_item/editor_item.gd index 959f5b98..f42134f2 100644 --- a/src/components/editors/local/editor_item/editor_item.gd +++ b/src/components/editors/local/editor_item/editor_item.gd @@ -2,7 +2,7 @@ class_name EditorListItemControl extends HBoxListItem signal edited -signal removed(remove_dir: bool) +signal removed(remove_dir: bool, remove_binary: bool) signal manage_tags_requested signal tag_clicked(tag: String) @@ -299,23 +299,30 @@ func _on_remove(item: LocalEditors.Item) -> void: warning.self_modulate = get_theme_color("warning_color", "Editor") warning.hide() - var checkbox := CheckBox.new() - checkbox.text = tr("remove also from the file system") - checkbox.toggled.connect(func(toggled: bool) -> void: + var remove_binary_checkbox := CheckBox.new() + remove_binary_checkbox.text = tr("delete binary from filesystem") + + var remove_dir_checkbox := CheckBox.new() + remove_dir_checkbox.text = tr("delete parent folder in filesystem") + remove_dir_checkbox.toggled.connect(func(toggled: bool) -> void: warning.visible = toggled + if toggled: + remove_binary_checkbox.button_pressed = false + remove_binary_checkbox.disabled = toggled ) - + var vb := VBoxContainer.new() vb.add_child(label) - vb.add_child(checkbox) + vb.add_child(remove_binary_checkbox) + vb.add_child(remove_dir_checkbox) vb.add_child(warning) vb.add_spacer(false) - + confirmation_dialog.add_child(vb) - + confirmation_dialog.confirmed.connect(func() -> void: queue_free() - removed.emit(checkbox.button_pressed) + removed.emit(remove_dir_checkbox.button_pressed, remove_binary_checkbox.button_pressed) ) add_child(confirmation_dialog) confirmation_dialog.popup_centered() diff --git a/src/components/editors/local/editors_list/editors_list.gd b/src/components/editors/local/editors_list/editors_list.gd index 8678d8a7..b521cfde 100644 --- a/src/components/editors/local/editors_list/editors_list.gd +++ b/src/components/editors/local/editors_list/editors_list.gd @@ -1,7 +1,7 @@ class_name EditorsVBoxList extends VBoxList -signal item_removed(item_data: LocalEditors.Item, remove_dir: bool) +signal item_removed(item_data: LocalEditors.Item, remove_dir: bool, remove_binary: bool) signal item_edited(item_data: LocalEditors.Item) signal item_manage_tags_requested(item_data: LocalEditors.Item) @@ -10,7 +10,7 @@ func _post_add(raw_item_data: Object, raw_item_control: Control) -> void: var item_data := raw_item_data as LocalEditors.Item var item_control := raw_item_control as EditorListItemControl item_control.removed.connect( - func(remove_dir: bool) -> void: item_removed.emit(item_data, remove_dir) + func(remove_dir: bool, remove_binary: bool) -> void: item_removed.emit(item_data, remove_dir, remove_binary) ) item_control.edited.connect( func() -> void: item_edited.emit(item_data) diff --git a/src/components/editors/local/local_editors.gd b/src/components/editors/local/local_editors.gd index 2a8208fa..ad407de4 100644 --- a/src/components/editors/local/local_editors.gd +++ b/src/components/editors/local/local_editors.gd @@ -197,17 +197,17 @@ func _on_editors_list_item_selected(item: EditorListItemControl) -> void: _sidebar.refresh_actions(item.get_actions()) -func _on_editors_list_item_removed(item_data: LocalEditors.Item, remove_dir: bool) -> void: +func _on_editors_list_item_removed(item_data: LocalEditors.Item, remove_dir: bool, remove_binary: bool) -> void: + var target := _resolve_removal_target( + item_data.path, Config.VERSIONS_PATH.ret() as String, remove_dir, remove_binary + ) if remove_dir: - var base_dir := ProjectSettings.globalize_path(item_data.path.get_base_dir()) - var versions_dir := ProjectSettings.globalize_path(Config.VERSIONS_PATH.ret() as String) - if not OS.has_feature("linux"): - base_dir = base_dir.to_lower() - versions_dir = versions_dir.to_lower() - if base_dir != versions_dir and base_dir.begins_with(versions_dir): - edir.remove_recursive(base_dir) + if target != "": + edir.remove_recursive(target) else: - Output.push("skipping removing path {%s}" % base_dir) + Output.push("skipping removing path {%s}" % ProjectSettings.globalize_path(item_data.path.get_base_dir())) + elif remove_binary: + DirAccess.remove_absolute(target) if _local_editors.has(item_data.path): _local_editors.erase(item_data.path) _local_editors.save() @@ -215,6 +215,24 @@ func _on_editors_list_item_removed(item_data: LocalEditors.Item, remove_dir: boo _update_remove_missing_disabled() +# Returns the absolute path to delete from the filesystem, or "" if nothing should be +# removed. The parent folder is only removable when it is a sub-folder of the versions +# dir (never the versions dir itself, e.g. for "extract to same folder" installs). +static func _resolve_removal_target(editor_path: String, versions_path: String, remove_dir: bool, remove_binary: bool) -> String: + if remove_dir: + var base_dir := ProjectSettings.globalize_path(editor_path.get_base_dir()) + var versions_dir := ProjectSettings.globalize_path(versions_path) + if not OS.has_feature("linux"): + base_dir = base_dir.to_lower() + versions_dir = versions_dir.to_lower() + if base_dir != versions_dir and base_dir.begins_with(versions_dir): + return base_dir + return "" + if remove_binary: + return ProjectSettings.globalize_path(editor_path) + return "" + + func _on_editors_list_item_edited(item_data: Variant) -> void: _local_editors.save() _editors_list.sort_items() diff --git a/src/components/editors/remote/remote_editors.gd b/src/components/editors/remote/remote_editors.gd index 7b2c4711..8bea2e69 100644 --- a/src/components/editors/remote/remote_editors.gd +++ b/src/components/editors/remote/remote_editors.gd @@ -120,9 +120,19 @@ func install_zip(zip_abs_path: String, root_unzip_folder_name: String, possible_ func _unzip_downloaded(downloaded_abs_path: String, root_unzip_folder_name: String) -> String: - var zip_content_dir := "%s/%s" % [Config.VERSIONS_PATH.ret(), root_unzip_folder_name] - if DirAccess.dir_exists_absolute(ProjectSettings.globalize_path(zip_content_dir)): - zip_content_dir += "-%s" % uuid.v4().substr(0, 8) - zip_content_dir += "/" + var zip_content_dir := _resolve_unzip_dir( + Config.VERSIONS_PATH.ret() as String, + root_unzip_folder_name, + Config.EXTRACT_TO_SAME_FOLDER.ret() as bool + ) zip.unzip(downloaded_abs_path, zip_content_dir) return zip_content_dir + + +static func _resolve_unzip_dir(versions_path: String, root_unzip_folder_name: String, extract_to_same_folder: bool) -> String: + if extract_to_same_folder: + return versions_path + "/" + var zip_content_dir := "%s/%s" % [versions_path, root_unzip_folder_name] + if DirAccess.dir_exists_absolute(ProjectSettings.globalize_path(zip_content_dir)): + zip_content_dir += "-%s" % uuid.v4().substr(0, 8) + return zip_content_dir + "/" diff --git a/src/components/settings/settings_window.gd b/src/components/settings/settings_window.gd index 07df753d..c8ba99eb 100644 --- a/src/components/settings/settings_window.gd +++ b/src/components/settings/settings_window.gd @@ -138,6 +138,12 @@ func _prepare_settings() -> Array: SettingCheckbox, tr("Will check only stable Godots releases.") )), + SettingChangeObserved(SettingCfg( + "application/advanced/extract_to_same_folder", + Config.EXTRACT_TO_SAME_FOLDER, + SettingCheckbox, + tr("Extract downloaded editors directly into the versions dir instead of creating a subfolder for each one.") + )), SettingChangeObserved(SettingCfg( "network/http_proxy/host", Config.HTTP_PROXY_HOST, diff --git a/src/config.gd b/src/config.gd index eeb7c895..2e3debb2 100644 --- a/src/config.gd +++ b/src/config.gd @@ -181,11 +181,20 @@ var ALLOW_INSTALL_TO_NOT_EMPTY_DIR := ConfigFileValue.new( var ONLY_STABLE_UPDATES := ConfigFileValue.new( - _cfg_auto_save.as_config_like(), - "app", + _cfg_auto_save.as_config_like(), + "app", "only_stable_updates", true -): +): + set(_v): _readonly() + + +var EXTRACT_TO_SAME_FOLDER := ConfigFileValue.new( + _cfg_auto_save.as_config_like(), + "app", + "extract_to_same_folder", + false +): set(_v): _readonly() diff --git a/tests/cases/editors/resolve_removal_target_test.gd b/tests/cases/editors/resolve_removal_target_test.gd new file mode 100644 index 00000000..b4dc4899 --- /dev/null +++ b/tests/cases/editors/resolve_removal_target_test.gd @@ -0,0 +1,32 @@ +extends GdUnitTestSuite + + +func test_delete_parent_folder_returns_the_editor_subfolder() -> void: + assert_str( + LocalEditorsControl._resolve_removal_target("/versions/Godot_v4.5-stable/godot", "/versions", true, false) + ).is_equal("/versions/Godot_v4.5-stable") + + +func test_delete_parent_folder_is_skipped_for_same_folder_installs() -> void: + # The binary lives directly in the versions dir, so the guard refuses to remove it. + assert_str( + LocalEditorsControl._resolve_removal_target("/versions/godot", "/versions", true, false) + ).is_equal("") + + +func test_delete_parent_folder_is_skipped_outside_the_versions_dir() -> void: + assert_str( + LocalEditorsControl._resolve_removal_target("/elsewhere/godot", "/versions", true, false) + ).is_equal("") + + +func test_delete_binary_returns_the_binary_path() -> void: + assert_str( + LocalEditorsControl._resolve_removal_target("/versions/godot", "/versions", false, true) + ).is_equal("/versions/godot") + + +func test_no_option_selected_removes_nothing() -> void: + assert_str( + LocalEditorsControl._resolve_removal_target("/versions/Godot_v4.5-stable/godot", "/versions", false, false) + ).is_equal("") diff --git a/tests/cases/editors/resolve_removal_target_test.gd.uid b/tests/cases/editors/resolve_removal_target_test.gd.uid new file mode 100644 index 00000000..90c4e67f --- /dev/null +++ b/tests/cases/editors/resolve_removal_target_test.gd.uid @@ -0,0 +1 @@ +uid://bbkjp7dksyl5s diff --git a/tests/cases/editors/resolve_unzip_dir_test.gd b/tests/cases/editors/resolve_unzip_dir_test.gd new file mode 100644 index 00000000..665c17dd --- /dev/null +++ b/tests/cases/editors/resolve_unzip_dir_test.gd @@ -0,0 +1,13 @@ +extends GdUnitTestSuite + + +func test_extract_to_same_folder_uses_versions_path_directly() -> void: + assert_str( + RemoteEditorsControl._resolve_unzip_dir("user://__test_versions__", "Godot_v4.5-stable", true) + ).is_equal("user://__test_versions__/") + + +func test_extract_creates_subfolder_named_after_the_release() -> void: + assert_str( + RemoteEditorsControl._resolve_unzip_dir("user://__test_versions__", "Godot_v4.5-stable", false) + ).is_equal("user://__test_versions__/Godot_v4.5-stable/") diff --git a/tests/cases/editors/resolve_unzip_dir_test.gd.uid b/tests/cases/editors/resolve_unzip_dir_test.gd.uid new file mode 100644 index 00000000..37022d6f --- /dev/null +++ b/tests/cases/editors/resolve_unzip_dir_test.gd.uid @@ -0,0 +1 @@ +uid://d2cqe4bdjy68i From 1e3e7b31fb8c882af2ca1cba8be0239a6a97498c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Pe=C3=B1a?= Date: Tue, 28 Jul 2026 11:48:54 +0800 Subject: [PATCH 2/3] fix(ci): rewrite release workflow as self-contained build The workflow delegated the build to firebelley/godot-export and was broken: it installed `wine64`, a package name Ubuntu 24.04 dropped, so `which wine64` resolved to nothing on ubuntu-latest. Wine is only needed so Godot can run rcedit to stamp the icon into the Windows .exe (the preset sets application/modify_resources=true). Godot logs a missing rcedit as an *info* message rather than an error, so the broken setup exported "successfully" and shipped an iconless binary on a green build. Replace the action with plain shell steps that download Godot and its export templates, import, export both presets, and create-or-update a draft release via the preinstalled gh CLI. Notable details: - Editor settings must be written to editor_settings-4.5.tres; Godot 4.x keys them by major.minor and silently ignores editor_settings-4.tres, which is what most CI examples write. - The export template directory is derived from templates/version.txt rather than hardcoded, keeping editor and templates in lockstep (4.5.2 looks for "4.5.2.stable", not "4.5.stable"). - Fail loudly when no wine binary is found, and verify export outputs are non-empty, so neither failure mode can ship silently. - Pin ubuntu-24.04, narrow permissions to contents:write, and add workflow_dispatch for building without cutting a tag. Co-Authored-By: Claude Opus 5 --- .github/workflows/create-release.yml | 196 +++++++++++++++++++++------ 1 file changed, 154 insertions(+), 42 deletions(-) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 17dc2d0b..731b5279 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -1,66 +1,178 @@ -# Whenever a tag push matching pattern "v*" then run the job +name: Create Release + +# Runs on a "v*" tag push (builds + publishes a draft release), or manually +# (builds only, so the pipeline can be tested without cutting a tag). on: push: tags: - "v*" + workflow_dispatch: env: - GODOT_VERSION: 4.5 + GODOT_VERSION: "4.5" GODOT_RELEASE_TYPE: stable + RCEDIT_VERSION: v2.0.0 + +permissions: + contents: write jobs: - # job id, can be anything export_game: - # Always use ubuntu-latest for this action - runs-on: ubuntu-latest - # Add permission for release creation. Can be made narrower according to your needs - permissions: write-all - # Job name, can be anything name: Export Game + # Pinned rather than ubuntu-latest: an image roll silently changed the + # available wine packages once already. + runs-on: ubuntu-24.04 steps: - # Always include the checkout step so that - # your project is available for Godot to export - - name: checkout - uses: actions/checkout@v3.3.0 + - name: Checkout + uses: actions/checkout@v4 with: - submodules: "recursive" + submodules: recursive + # Godot shells out to rcedit (through wine) to stamp the icon and version + # metadata into the Windows .exe, because the "Windows Desktop" preset sets + # application/modify_resources=true. Godot only logs an *info* message when + # rcedit is missing, so a broken wine setup ships an iconless .exe on a green + # build -- hence the hard failure below. - name: Install Wine - id: wine_install + id: wine run: | + set -euo pipefail sudo apt-get update - sudo apt-get install -y --no-install-recommends wine64 - echo "WINE_PATH=$(which wine64)" >> $GITHUB_OUTPUT + sudo apt-get install -y --no-install-recommends wine - - name: export game - id: export - # Use latest version (see releases for all versions) - uses: firebelley/godot-export@v5.2.1 - with: - # Defining all the required inputs - godot_executable_download_url: https://github.com/godotengine/godot-builds/releases/download/${{ env.GODOT_VERSION }}-${{ env.GODOT_RELEASE_TYPE }}/Godot_v${{ env.GODOT_VERSION }}-${{ env.GODOT_RELEASE_TYPE }}_linux.x86_64.zip - godot_export_templates_download_url: https://github.com/godotengine/godot-builds/releases/download/${{ env.GODOT_VERSION }}-${{ env.GODOT_RELEASE_TYPE }}/Godot_v${{ env.GODOT_VERSION }}-${{ env.GODOT_RELEASE_TYPE }}_export_templates.tpz - relative_project_path: . - archive_output: true - wine_path: ${{ steps.wine_install.outputs.WINE_PATH }} - - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Ubuntu 24.04 dropped the "wine64" binary name in favour of "wine". + WINE_PATH="$(command -v wine64 || command -v wine || true)" + if [ -z "$WINE_PATH" ]; then + echo "::error::Neither 'wine64' nor 'wine' is on PATH after install; rcedit cannot run." + exit 1 + fi + echo "Using wine at: $WINE_PATH" + echo "wine_path=$WINE_PATH" >> "$GITHUB_OUTPUT" + + # Create the wine prefix up front so the first rcedit call doesn't race it. + # This is an optimisation, not a requirement -- wine builds the prefix on + # first use anyway, so don't fail the build over it. + if command -v wineboot >/dev/null 2>&1; then + WINEDEBUG=-all wineboot --init \ + || echo "::warning::wineboot --init failed; wine will create its prefix on first use." + fi + + - name: Download rcedit + run: | + set -euo pipefail + curl -fsSL -o "$HOME/rcedit-x64.exe" \ + "https://github.com/electron/rcedit/releases/download/${RCEDIT_VERSION}/rcedit-x64.exe" - - name: generate SHA512-SUMS.txt + # Unpacked outside the workspace: stray files inside the project directory + # would be picked up by Godot's filesystem scan. + - name: Install Godot and export templates run: | - cd "${{ steps.export.outputs.archive_directory }}" - sha512sum * > SHA512-SUMS.txt + set -euo pipefail + BASE="https://github.com/godotengine/godot-builds/releases/download/${GODOT_VERSION}-${GODOT_RELEASE_TYPE}" + NAME="Godot_v${GODOT_VERSION}-${GODOT_RELEASE_TYPE}" + + WORK="$RUNNER_TEMP/godot-setup" + mkdir -p "$WORK" "$HOME/bin" + cd "$WORK" + + curl -fsSL -o godot.zip "${BASE}/${NAME}_linux.x86_64.zip" + curl -fsSL -o templates.tpz "${BASE}/${NAME}_export_templates.tpz" + + unzip -q godot.zip + mv "${NAME}_linux.x86_64" "$HOME/bin/godot" + chmod +x "$HOME/bin/godot" + echo "$HOME/bin" >> "$GITHUB_PATH" + "$HOME/bin/godot" --version + + # The .tpz expands to a top-level "templates/" dir; version.txt inside it + # holds the exact directory name Godot looks for (e.g. "4.5.stable"). + unzip -q templates.tpz + TEMPLATE_VERSION="$(cat templates/version.txt)" + echo "Export templates version: ${TEMPLATE_VERSION}" + mkdir -p "$HOME/.local/share/godot/export_templates" + rm -rf "$HOME/.local/share/godot/export_templates/${TEMPLATE_VERSION}" + mv templates "$HOME/.local/share/godot/export_templates/${TEMPLATE_VERSION}" + + # rcedit/wine paths live in the editor settings, not in export_presets.cfg. + # Godot 4.5 reads editor_settings-4.5.tres (major.minor) -- the widely copied + # editor_settings-4.tres is silently ignored. + - name: Configure Godot editor settings + run: | + set -euo pipefail + mkdir -p "$HOME/.config/godot" + SETTINGS="$HOME/.config/godot/editor_settings-${GODOT_VERSION}.tres" + cat > "$SETTINGS" < SHA512-SUMS.txt echo "Generated SHA512-SUMS.txt:" cat SHA512-SUMS.txt - # This release action has worked well for me. However, you can most likely use any release action of your choosing. - # https://github.com/ncipollo/release-action - - name: create release - uses: ncipollo/release-action@v1.12.0 + - name: Upload build artifacts + uses: actions/upload-artifact@v4 with: - draft: true - tag: ${{ github.ref_name }} - artifacts: | - ${{ steps.export.outputs.archive_directory }}/* - ${{ steps.export.outputs.archive_directory }}/SHA512-SUMS.txt + name: godots-${{ github.sha }} + path: dist/ + if-no-files-found: error + + - name: Create or update draft release + if: startsWith(github.ref, 'refs/tags/') + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + if gh release view "$TAG" >/dev/null 2>&1; then + echo "Release $TAG already exists; replacing its assets." + gh release upload "$TAG" dist/* --clobber + else + echo "Creating draft release $TAG." + gh release create "$TAG" dist/* --draft --title "$TAG" --generate-notes + fi From c2bb2622c2df1eea78a59c0ca63cfcb9a9426ba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Pe=C3=B1a?= Date: Tue, 28 Jul 2026 12:17:30 +0800 Subject: [PATCH 3/3] fix(ci): don't expect a console wrapper from the release export The export step asserted Godots.console.exe exists, and the packaging step zipped it. The Windows preset sets debug/export_console_wrapper=1, which is the "Debug Only" enum value, so --export-release produces the GUI exe alone and the assertion failed on an otherwise clean build. Drop the file from both steps and record why it is absent, so shipping it stays a deliberate preset change (value 2) rather than something re-added to CI on the assumption Godot emits it. Co-Authored-By: Claude Opus 5 --- .github/workflows/create-release.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 731b5279..7151fa31 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -128,11 +128,13 @@ jobs: godot --headless --export-release "Linux/X11" "$BUILD/linux/Godots.x86_64" godot --headless --export-release "Windows Desktop" "$BUILD/windows/Godots.exe" + # No Godots.console.exe here: the preset sets debug/export_console_wrapper=1 + # ("Debug Only"), so a release export deliberately produces the GUI exe alone. + # # Godot can exit 0 having produced nothing; never publish an empty release. for f in \ "$BUILD/linux/Godots.x86_64" \ - "$BUILD/windows/Godots.exe" \ - "$BUILD/windows/Godots.console.exe"; do + "$BUILD/windows/Godots.exe"; do if [ ! -s "$f" ]; then echo "::error::Expected export output '$f' is missing or empty." exit 1 @@ -148,7 +150,7 @@ jobs: chmod +x "$BUILD/linux/Godots.x86_64" (cd "$BUILD/linux" && zip -q -X "$GITHUB_WORKSPACE/dist/Godots-linux-x86_64.zip" Godots.x86_64) - (cd "$BUILD/windows" && zip -q -X "$GITHUB_WORKSPACE/dist/Godots-windows-x86_64.zip" Godots.exe Godots.console.exe) + (cd "$BUILD/windows" && zip -q -X "$GITHUB_WORKSPACE/dist/Godots-windows-x86_64.zip" Godots.exe) cd dist sha512sum *.zip > SHA512-SUMS.txt