Files
agame/addons/GDQuest_GDScript_formatter/install_and_update.gd
T
2026-09-12 02:07:17 +08:00

241 lines
7.2 KiB
GDScript

@tool
extends Node
enum HttpRequestState {
IDLE,
FETCHING_RELEASE_INFO,
DOWNLOADING_BINARY,
}
const URL_GITHUB_API_LATEST_RELEASE = "https://api.github.com/repos/gdquest/GDScript-formatter/releases/latest"
signal installation_completed(binary_path: String)
signal installation_failed(error_message: String)
var http_request_state := HttpRequestState.IDLE
var http_request: HTTPRequest = HTTPRequest.new()
var formatter_cache_dir: String
func _init(cache_dir: String) -> void:
formatter_cache_dir = cache_dir
func _ready() -> void:
add_child(http_request)
http_request.request_completed.connect(_on_request_completed)
func install_or_update_formatter() -> void:
print("Starting GDScript formatter installation...")
http_request_state = HttpRequestState.FETCHING_RELEASE_INFO
http_request.request(URL_GITHUB_API_LATEST_RELEASE)
func _on_request_completed(
_http_result: int,
response_code: int,
_http_headers: PackedStringArray,
body: PackedByteArray,
) -> void:
if response_code != 200:
var error_message := "HTTP request failed. Response code: " + str(response_code)
push_error(error_message)
http_request_state = HttpRequestState.IDLE
installation_failed.emit(error_message)
return
match http_request_state:
HttpRequestState.FETCHING_RELEASE_INFO:
_process_response_latest_release(body)
HttpRequestState.DOWNLOADING_BINARY:
_process_response_download_file(body)
_:
var error_message := "Unexpected HTTP request state: " + str(http_request_state)
push_error(error_message)
http_request_state = HttpRequestState.IDLE
installation_failed.emit(error_message)
func _process_response_latest_release(body: PackedByteArray) -> void:
var json = JSON.parse_string(body.get_string_from_utf8())
if not json or not json.has("assets"):
var error_message := "Failed to parse release information from GitHub API"
push_error(error_message)
http_request_state = HttpRequestState.IDLE
installation_failed.emit(error_message)
return
print("GDScript Formatter release information loaded successfully")
var assets = json["assets"]
var tag = json["tag_name"]
var download_url := _find_matching_asset(assets, tag)
if download_url.is_empty():
var error_message := "No matching binary found for current platform"
push_error(error_message)
http_request_state = HttpRequestState.IDLE
installation_failed.emit(error_message)
return
print("Found compatible release, starting download...\n", "Download URL: ", download_url)
http_request_state = HttpRequestState.DOWNLOADING_BINARY
http_request.request(download_url)
func _process_response_download_file(body: PackedByteArray) -> void:
http_request_state = HttpRequestState.IDLE
print("Download completed successfully (", body.size(), " bytes)")
var asset_info := _get_platform_info()
if asset_info.is_empty():
var error_message := "Failed to determine platform information"
push_error(error_message)
installation_failed.emit(error_message)
return
print("Extracting and installing binary...")
var binary_path := _download_and_install_binary(body, asset_info)
if binary_path.is_empty():
var error_message := "Installation failed"
push_error(error_message)
installation_failed.emit(error_message)
return
print(
"\n".join(["GDScript formatter installed successfully!", "Binary location: " + binary_path]),
)
installation_completed.emit(binary_path)
func _get_platform_info() -> Dictionary:
var os_name := OS.get_name().to_lower()
var processor_name := OS.get_processor_name().to_lower()
var architecture := "x86_64"
if (
processor_name.contains("aarch64") or processor_name.contains("arm64")
or processor_name.contains("apple")
):
architecture = "aarch64"
elif processor_name.contains("x86_64") or processor_name.contains("amd64"):
architecture = "x86_64"
var binary_name := "gdscript-formatter"
if os_name.contains("windows"):
binary_name = "gdscript-formatter.exe"
var platform_info := { "architecture": architecture, "binary_name": binary_name }
if os_name.contains("windows"):
platform_info["os"] = "windows"
elif os_name.contains("linux"):
platform_info["os"] = "linux"
elif os_name.contains("macos") or os_name.contains("osx"):
platform_info["os"] = "macos"
return platform_info
func _find_matching_asset(assets: Array, tag: String) -> String:
var platform_info := _get_platform_info()
if platform_info.is_empty():
return ""
var expected_pattern := "gdscript-formatter-%s-%s-%s" % [
tag,
platform_info["os"],
platform_info["architecture"],
]
if platform_info["os"] == "windows":
expected_pattern += ".exe"
expected_pattern += ".zip"
for asset in assets:
var asset_name: String = asset["name"]
if asset_name == expected_pattern:
return asset["browser_download_url"]
return ""
func _download_and_install_binary(zip_data: PackedByteArray, platform_info: Dictionary) -> String:
var binary_name: String = platform_info["binary_name"]
# We get the contents of the zip file as a byte array. we need to write
# it to a file to be able to read it. We use a temporary file for that.
var temp_archive_path := formatter_cache_dir.path_join("temp_formatter_archive.zip")
print("Installing to cache directory: ", formatter_cache_dir)
# Create the gdquest cache directory if it doesn't exist
if not DirAccess.dir_exists_absolute(formatter_cache_dir):
var dir_result := DirAccess.make_dir_recursive_absolute(formatter_cache_dir)
if dir_result != OK:
push_error("Failed to create cache directory: ", formatter_cache_dir)
return ""
var temp_file := FileAccess.open(temp_archive_path, FileAccess.WRITE)
if not temp_file:
push_error("Failed to create temporary archive file")
return ""
temp_file.store_buffer(zip_data)
temp_file.close()
var zip_reader := ZIPReader.new()
var err := zip_reader.open(temp_archive_path)
if err != OK:
push_error("Failed to open ZIP archive from temporary file")
zip_reader.close()
DirAccess.remove_absolute(temp_archive_path)
return ""
var files := zip_reader.get_files()
var binary_data: PackedByteArray
var found_binary := false
var actual_binary_name := ""
for file_path in files:
if not file_path.ends_with("/"):
binary_data = zip_reader.read_file(file_path)
actual_binary_name = file_path.get_file()
found_binary = true
break
zip_reader.close()
DirAccess.remove_absolute(temp_archive_path)
if not found_binary:
push_error("No executable found in ZIP archive")
return ""
var binary_path := formatter_cache_dir.path_join(binary_name)
var file := FileAccess.open(binary_path, FileAccess.WRITE)
if not file:
push_error("Failed to create binary file at: ", binary_path)
return ""
file.store_buffer(binary_data)
file.close()
if not OS.get_name().to_lower().contains("windows"):
# This should be equivalent to setting the binary to permissions 755
const UNIX_EXECUTABLE_PERMISSIONS = (
FileAccess.UNIX_READ_OWNER | FileAccess.UNIX_WRITE_OWNER
| FileAccess.UNIX_EXECUTE_OWNER | FileAccess.UNIX_READ_GROUP
| FileAccess.UNIX_EXECUTE_GROUP | FileAccess.UNIX_READ_OTHER | FileAccess.UNIX_EXECUTE_OTHER
)
var permissions_error := FileAccess.set_unix_permissions(
binary_path,
UNIX_EXECUTABLE_PERMISSIONS,
)
if permissions_error != OK:
push_error("Failed to make formatter executable: ", binary_path)
DirAccess.remove_absolute(binary_path)
return ""
return binary_path