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..3e013b2 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.42", "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",