Keep the butler binary up to date with itch's broth channel
All checks were successful
Build plugin / build (push) Successful in 1m10s

butler was only downloaded when the binary was missing, so a device that
installed it once kept that build forever (brutha was still on v15.30.0
from July while broth ships 15.32.0). bootstrap now compares the installed
version against broth's LATEST and refreshes it when behind, keeping the
installed binary when the channel can't be reached or the version can't be
read, so a network hiccup never replaces a working butler.
This commit is contained in:
Jose Falanga 2026-10-15 11:41:09 -03:00
parent c6fd78e76f
commit bb4ddb0ea5
2 changed files with 63 additions and 17 deletions

View file

@ -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]
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()
if result != HTTPRequest.RESULT_SUCCESS or response_code != 200:
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:

View file

@ -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",