diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 17dc2d0b..7151fa31 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -1,66 +1,180 @@ -# 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 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