diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 14ba83d..5756dc3 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -16,15 +16,17 @@ jobs: - name: Install dependencies run: | - apt-get update -qq - apt-get install -y -qq make git wget unzip + apt-get update + apt-get install -y -qq make git wget - name: Install Godot run: | wget -q https://github.com/godotengine/godot/releases/download/4.4.1-stable/Godot_v4.4.1-stable_linux.x86_64.zip - unzip -q Godot_v4.4.1-stable_linux.x86_64.zip + # python3 instead of unzip: the runner's apt index goes stale and the + # pinned unzip deb 404s in the security pool. + python3 -m zipfile -e Godot_v4.4.1-stable_linux.x86_64.zip godot-dir mkdir -p "$HOME/.local/bin" - mv Godot_v4.4.1-stable_linux.x86_64 "$HOME/.local/bin/godot" + mv godot-dir/Godot_v4.4.1-stable_linux.x86_64 "$HOME/.local/bin/godot" chmod +x "$HOME/.local/bin/godot" echo "$HOME/.local/bin" >> "$GITHUB_PATH" @@ -41,21 +43,32 @@ jobs: TAG="${GITHUB_REF_NAME}" API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}" - # Create the release - curl -sf -X POST "${API}/releases" \ + # Create the release. `curl -sf` hid the reason a tag build could not + # publish, which left devices on the previous release with no clue, + # so report the status code and the API answer. + HTTP=$(curl -sS -o /tmp/release.json -w "%{http_code}" -X POST "${API}/releases" \ -H "Authorization: token ${GITHUB_TOKEN}" \ -H "Content-Type: application/json" \ - -d "{\"tag_name\":\"${TAG}\",\"name\":\"${TAG}\"}" \ - -o /tmp/release.json + -d "{\"tag_name\":\"${TAG}\",\"name\":\"${TAG}\"}") + if [ "${HTTP}" != "201" ] && [ "${HTTP}" != "200" ]; then + echo "Release creation for ${TAG} failed with HTTP ${HTTP}:" + cat /tmp/release.json + exit 1 + fi RELEASE_ID=$(python3 -c "import json; print(json.load(open('/tmp/release.json'))['id'])") # Upload assets for FILE in dist/itch.zip dist/itch.zip.sha256.txt; do NAME=$(basename "$FILE") - curl -sf -X POST \ + HTTP=$(curl -sS -o /tmp/asset.json -w "%{http_code}" -X POST \ "${API}/releases/${RELEASE_ID}/assets?name=${NAME}" \ -H "Authorization: token ${GITHUB_TOKEN}" \ -H "Content-Type: application/octet-stream" \ - --data-binary "@${FILE}" + --data-binary "@${FILE}") + if [ "${HTTP}" != "201" ] && [ "${HTTP}" != "200" ]; then + echo "Asset upload of ${NAME} failed with HTTP ${HTTP}:" + cat /tmp/asset.json + exit 1 + fi done diff --git a/core/itch_client.gd b/core/itch_client.gd index 39490df..f9d92e2 100644 --- a/core/itch_client.gd +++ b/core/itch_client.gd @@ -83,7 +83,8 @@ func _ready() -> void: bootstrap() -## Bootstraps the butler binary if it isn't present, then spawns butlerd. +## Bootstraps the butler binary (downloading it when missing or outdated), then +## spawns butlerd. func bootstrap() -> void: var butler_dir_global := ProjectSettings.globalize_path(butler_dir) _migrate_butler(butler_dir_global) @@ -96,6 +97,12 @@ func bootstrap() -> void: bootstrap_finished.emit() return logger.info("Successfully installed butler") + elif await _butler_is_outdated(butler_bin): + logger.info("Updating the butler binary to " + str(await _fetch_latest_butler_version()) + ".") + var success := await _install_butler() + if not success: + # Not fatal: the installed butler still works, it is just behind. + logger.warn("Unable to update butler, keeping the installed one.") var dbpath := "/".join([butler_dir_global, "butler.db"]) var args := [ @@ -139,9 +146,8 @@ func _migrate_butler(dest_dir: String) -> void: logger.info("Migrated butler from " + old_dir + " to " + dest_dir) -## Downloads a butler binary for this platform from itch's broth distribution -## channel (the same one the official itch.io app uses to self-update). -func _install_butler() -> bool: +## broth's slug for the platform we are running on, e.g. "linux-amd64". +func _butler_platform_slug() -> String: var goos := "linux" var goarch := "amd64" if OS.get_name() == "Windows": @@ -155,10 +161,13 @@ func _install_butler() -> bool: var arch: String = Engine.get_architecture_name() if "arm64" in arch or "aarch64" in arch: goarch = "arm64" + return goos + "-" + goarch - var platform_slug := goos + "-" + goarch - var latest_url := "/".join([broth_base, platform_slug, "LATEST"]) +## Asks itch's broth channel which butler version is current. Returns "" when +## the channel can't be reached, so callers can tell "unknown" from "current". +func _fetch_latest_butler_version() -> String: + var latest_url := "/".join([broth_base, _butler_platform_slug(), "LATEST"]) var http := HTTPRequest.new() add_child.call_deferred(http) await http.ready @@ -166,28 +175,65 @@ func _install_butler() -> bool: logger.error("Error fetching latest butler version: " + latest_url) remove_child(http) http.queue_free() - return false + return "" var args: Array = await http.request_completed var result: int = args[0] var response_code: int = args[1] var body: PackedByteArray = args[3] + remove_child(http) + http.queue_free() if result != HTTPRequest.RESULT_SUCCESS or response_code != 200: - logger.error("Unable to determine latest butler version for " + platform_slug) - remove_child(http) - http.queue_free() - return false - var version := body.get_string_from_utf8().strip_edges() + logger.error("Unable to determine the latest butler version") + return "" + return body.get_string_from_utf8().strip_edges() + +## Reads the version of an installed butler binary, e.g. "15.30.0" out of +## "v15.30.0, built on Jul 27 2026". Returns "" when the binary can't be run. +func _butler_installed_version(butler_bin: String) -> String: + var out := [] + if OS.execute(butler_bin, ["version"], out) != OK or out.is_empty(): + return "" + var reported: String = (out[0] as String).strip_edges().get_slice(",", 0) + return reported.trim_prefix("v").strip_edges() + + +## True when the installed butler is behind broth's current version. The +## binary used to be downloaded only when missing, so it silently rotted for +## months and missed fixes. Unknown version (channel unreachable, binary not +## runnable) counts as up to date: never replace a working butler on a guess. +func _butler_is_outdated(butler_bin: String) -> bool: + var latest := await _fetch_latest_butler_version() + if latest == "": + return false + var installed := _butler_installed_version(butler_bin) + if installed == "" or installed == latest: + return false + logger.info("butler " + installed + " is behind " + latest + ". Updating it.") + return true + + +## Downloads a butler binary for this platform from itch's broth distribution +## channel (the same one the official itch.io app uses to self-update). +func _install_butler() -> bool: + var platform_slug := _butler_platform_slug() + var version := await _fetch_latest_butler_version() + if version == "": + return false + + var http := HTTPRequest.new() + add_child.call_deferred(http) + await http.ready var archive_url := "/".join([broth_base, platform_slug, version, "archive", "default"]) if http.request(archive_url) != OK: logger.error("Error downloading butler: " + archive_url) remove_child(http) http.queue_free() return false - args = await http.request_completed - result = args[0] - response_code = args[1] - body = args[3] + var args: Array = await http.request_completed + var result: int = args[0] + var response_code: int = args[1] + var body: PackedByteArray = args[3] remove_child(http) http.queue_free() if result != HTTPRequest.RESULT_SUCCESS or response_code != 200: diff --git a/plugin.json b/plugin.json index 3826d28..a9793d1 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "plugin.id": "itch", "plugin.name": "itch.io", - "plugin.version": "0.1.41", + "plugin.version": "0.1.43", "plugin.min-api-version": "1.1.0", "plugin.link": "https://forge.thergic.ar/jose/itchio-opengamepadui-plugin", "plugin.source": "https://forge.thergic.ar/jose/itchio-opengamepadui-plugin",