Created
July 15, 2025 22:12
-
-
Save DragonAxe/d0334995f2d2b30e553ee75eb78457ae to your computer and use it in GitHub Desktop.
Deploy Godot Game with Rust GDExtension
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os | |
from os.path import dirname, abspath | |
import subprocess | |
import sys | |
import argparse | |
from pathlib import Path | |
from datetime import datetime | |
from contextlib import chdir | |
import glob | |
import re | |
import shutil | |
# | |
# Assumed directory structure: | |
# | |
# project_root | |
# - godot # Godot project directory | |
# - project.godot | |
# - godot_export # Contains export zips | |
# - windows # Contains latest export | |
# - linux # Contains latest export | |
# - Cargo.toml # Rust project root | |
# - deploy.py # This script | |
# | |
GAME_NAME = "my_game" # Used for zip file names | |
ITCH_GAME_NAME = "my_game_on_itch" | |
ITCH_USERNAME = "my_itch_username" | |
def print_announcement(message: str) -> None: | |
border = "═" * (len(message) + 4) | |
print(f"\n╔{border}╗") | |
print(f"║ {message} ║") | |
print(f"╚{border}╝\n") | |
def get_next_zip_name(platform: str) -> str: | |
today = datetime.now().strftime("%Y-%m-%d") | |
counter = 1 | |
while True: | |
filename = f"{today}_{counter:02d}_{platform}_{GAME_NAME}.zip" | |
if not os.path.exists(filename): | |
return filename | |
counter += 1 | |
def get_latest_zip_name(platform: str) -> tuple[str | None, str | None]: | |
pattern = f"*_{platform}_{GAME_NAME}.zip" | |
files = glob.glob(pattern) | |
if not files: | |
return None, None | |
def parse_date_counter(filename): | |
file_match = re.match(r"(\d{4}-\d{2}-\d{2})_(\d+)_", filename) | |
if file_match: | |
date_str, counter = file_match.groups() | |
return datetime.strptime(date_str, "%Y-%m-%d"), int(counter) | |
return datetime.min, 0 | |
latest_file = max(files, key=parse_date_counter) | |
version_match = re.match(r"(\d{4}-\d{2}-\d{2})_(\d+)_", latest_file) | |
version = f"{version_match.group(1)}.{version_match.group(2)}" if version_match else None | |
return latest_file, version | |
def step_setup_rust_targets() -> None: | |
print_announcement("Setting up Rust targets") | |
subprocess.run(["rustup", "target", "add", "x86_64-pc-windows-gnu"], check=True) | |
subprocess.run(["rustup", "target", "add", "x86_64-unknown-linux-gnu"], check=True) | |
# subprocess.run(["sudo", "pacman", "-S", "mingw-w64-gcc"], check=True) | |
def step_build_rust_targets() -> None: | |
print_announcement("Building Rust targets") | |
subprocess.run(["cargo", "build", "--release", "--target", "x86_64-pc-windows-gnu"], check=True) | |
subprocess.run(["cargo", "build", "--release", "--target", "x86_64-unknown-linux-gnu"], check=True) | |
def step_prepare_export_dirs() -> None: | |
print_announcement("Creating export directories") | |
if os.path.exists("godot_export/windows"): | |
shutil.rmtree("godot_export/windows") | |
if os.path.exists("godot_export/linux"): | |
shutil.rmtree("godot_export/linux") | |
Path("godot_export/windows").mkdir(parents=True, exist_ok=True) | |
Path("godot_export/linux").mkdir(parents=True, exist_ok=True) | |
def step_export_godot_project() -> None: | |
print_announcement("Exporting Godot project") | |
with chdir("godot"): | |
subprocess.run([ | |
"godot", "--path", ".", "--import", "--headless", "--quit", | |
"--export-release", "windows_desktop", | |
f"../godot_export/windows/{GAME_NAME}.exe" | |
], check=True) | |
subprocess.run([ | |
"godot", "--path", ".", "--import", "--headless", "--quit", | |
"--export-release", "linux", | |
f"../godot_export/linux/{GAME_NAME}.x86_64" | |
], check=True) | |
def step_zip_exports() -> None: | |
print_announcement("Zipping exports") | |
with chdir("godot_export"): | |
if os.path.exists("windows"): | |
zip_name = get_next_zip_name("windows") | |
subprocess.run(["zip", "-r", zip_name, "windows"], check=True) | |
if os.path.exists("linux"): | |
zip_name = get_next_zip_name("linux") | |
subprocess.run(["zip", "-r", zip_name, "linux"], check=True) | |
def download_butler() -> Path: | |
print_announcement("Checking butler installation") | |
butler_dir = Path(".butler").resolve() | |
butler_dir.mkdir(exist_ok=True) | |
platform = sys.platform | |
if platform == "win32": | |
butler_path = butler_dir / "butler.exe" | |
download_url = "https://broth.itch.ovh/butler/windows-amd64/LATEST/archive/default" | |
elif platform == "linux": | |
butler_path = butler_dir / "butler" | |
download_url = "https://broth.itch.ovh/butler/linux-amd64/LATEST/archive/default" | |
else: | |
raise RuntimeError(f"Unsupported platform: {platform}") | |
print(f"Butler path: {butler_path}") | |
print(f"Butler path exists: {butler_path.exists()}") | |
if not butler_path.exists(): | |
print("Downloading butler...") | |
subprocess.run(["curl", "-L", download_url, "-o", "butler.zip"], check=True) | |
subprocess.run(["unzip", "butler.zip", "-d", str(butler_dir)], check=True) | |
os.remove("butler.zip") | |
if platform == "linux": | |
butler_path.chmod(0o755) | |
if not butler_path.exists(): | |
raise FileNotFoundError(f"Butler executable not found at {butler_path}") | |
try: | |
result = subprocess.run([str(butler_path), "--version"], check=True, capture_output=True, text=True) | |
print(f"Butler version: {result.stdout.strip()}") | |
except (subprocess.CalledProcessError, FileNotFoundError) as e: | |
raise RuntimeError(f"Error checking butler version: {e}") | |
return butler_path.resolve() | |
def step_upload_exports_to_itch_io() -> None: | |
try: | |
butler_path = download_butler() | |
except (subprocess.CalledProcessError, FileNotFoundError, RuntimeError) as e: | |
print(f"Error setting up butler: {e}") | |
return | |
print_announcement("Uploading to itch.io") | |
with chdir("godot_export"): | |
try: | |
for platform in ["windows", "linux"]: | |
zip_name, version = get_latest_zip_name(platform) | |
if zip_name and version: | |
print(f"Uploading {platform} zip: {zip_name} (version {version})") | |
subprocess.run([ | |
butler_path, "push", zip_name, | |
f"{ITCH_USERNAME}/{ITCH_GAME_NAME}:{platform}-dev", "--userversion", version | |
], check=True) | |
else: | |
print(f"No {platform} zip found to upload") | |
except subprocess.CalledProcessError as e: | |
print(f"Failed to upload: {e}") | |
def main() -> None: | |
parser = argparse.ArgumentParser(description=f'Deploy {GAME_NAME.replace("_", " ").title()}') | |
parser.add_argument('--setup-rust', action='store_true', help='Setup Rust targets') | |
parser.add_argument('--build-rust', action='store_true', help='Build Rust targets') | |
parser.add_argument('--create-dirs', action='store_true', help='Create export directories') | |
parser.add_argument('--export-godot', action='store_true', help='Export Godot project') | |
parser.add_argument('--zip', action='store_true', help='Zip exports') | |
parser.add_argument('--upload', action='store_true', help='Upload to itch.io using butler') | |
parser.add_argument('--all', action='store_true', help='Run all deployment steps') | |
args = parser.parse_args() | |
with chdir(dirname(abspath(__file__))): | |
try: | |
if args.setup_rust or args.all: | |
step_setup_rust_targets() | |
if args.build_rust or args.all: | |
step_build_rust_targets() | |
if args.create_dirs or args.all: | |
step_prepare_export_dirs() | |
if args.export_godot or args.all: | |
step_export_godot_project() | |
if args.zip or args.all: | |
step_zip_exports() | |
if args.upload or args.all: | |
step_upload_exports_to_itch_io() | |
if not any(vars(args).values()): | |
parser.print_help() | |
except subprocess.CalledProcessError as e: | |
print(f"Error executing command: {e}", file=sys.stderr) | |
sys.exit(1) | |
except Exception as e: | |
print(f"Error: {e}", file=sys.stderr) | |
sys.exit(1) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment