Skip to content

Instantly share code, notes, and snippets.

@DragonAxe
Created July 15, 2025 22:12
Show Gist options
  • Save DragonAxe/d0334995f2d2b30e553ee75eb78457ae to your computer and use it in GitHub Desktop.
Save DragonAxe/d0334995f2d2b30e553ee75eb78457ae to your computer and use it in GitHub Desktop.
Deploy Godot Game with Rust GDExtension
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