GamesDows/decky_builder.py
2024-12-25 15:18:14 -08:00

831 lines
34 KiB
Python

import os
import subprocess
import shutil
import argparse
from pathlib import Path
import sys
import time
import PyInstaller
import atexit
import requests
import psutil
import re
class DeckyBuilder:
def __init__(self, release: str = None):
self.release = release or self.prompt_for_version()
self.root_dir = Path(__file__).resolve().parent
self.app_dir = self.root_dir / "app"
self.src_dir = self.root_dir / "src"
self.dist_dir = self.root_dir / "dist"
self.homebrew_dir = self.dist_dir / "homebrew"
self.temp_files = [] # Track temporary files for cleanup
atexit.register(self.cleanup) # Register cleanup on exit
# Setup user homebrew directory
self.user_home = Path.home()
self.user_homebrew_dir = self.user_home / "homebrew"
self.homebrew_folders = [
"data",
"logs",
"plugins",
"services",
"settings",
"themes"
]
def cleanup(self):
"""Clean up temporary files and directories"""
try:
# Clean up any temporary files we created
for temp_file in self.temp_files:
if os.path.exists(temp_file):
try:
if os.path.isfile(temp_file):
os.remove(temp_file)
elif os.path.isdir(temp_file):
shutil.rmtree(temp_file, ignore_errors=True)
except Exception as e:
print(f"Warning: Failed to remove temporary file {temp_file}: {e}")
# Clean up PyInstaller temp files
for dir_name in ['build', 'dist']:
dir_path = self.root_dir / dir_name
if dir_path.exists():
try:
shutil.rmtree(dir_path, ignore_errors=True)
except Exception as e:
print(f"Warning: Failed to remove {dir_name} directory: {e}")
# Clean up PyInstaller spec files
for spec_file in self.root_dir.glob("*.spec"):
try:
os.remove(spec_file)
except Exception as e:
print(f"Warning: Failed to remove spec file {spec_file}: {e}")
except Exception as e:
print(f"Warning: Error during cleanup: {e}")
def safe_remove_directory(self, path):
"""Safely remove a directory with retries for Windows"""
max_retries = 3
retry_delay = 1 # seconds
for attempt in range(max_retries):
try:
if path.exists():
# On Windows, sometimes we need to remove .git directory separately
git_dir = path / '.git'
if git_dir.exists():
for item in git_dir.glob('**/*'):
if item.is_file():
try:
item.chmod(0o777) # Give full permissions
item.unlink()
except:
pass
shutil.rmtree(path, ignore_errors=True)
return
except Exception as e:
print(f"Attempt {attempt + 1} failed to remove {path}: {str(e)}")
if attempt < max_retries - 1:
time.sleep(retry_delay)
continue
else:
print(f"Warning: Could not fully remove {path}. Continuing anyway...")
def setup_directories(self):
"""Setup directory structure"""
print("Setting up directories...")
# Clean up any existing directories
if self.app_dir.exists():
self.safe_remove_directory(self.app_dir)
if self.src_dir.exists():
self.safe_remove_directory(self.src_dir)
if self.homebrew_dir.exists():
self.safe_remove_directory(self.homebrew_dir)
# Create fresh directories
self.src_dir.mkdir(parents=True, exist_ok=True)
self.homebrew_dir.mkdir(parents=True, exist_ok=True)
def setup_homebrew(self):
"""Setup homebrew directory structure"""
print("Setting up homebrew directory structure...")
# Create dist directory
(self.homebrew_dir / "dist").mkdir(parents=True, exist_ok=True)
# Setup homebrew directory structure for both temp and user directories
print("Setting up homebrew directory structure...")
for directory in [self.homebrew_dir, self.user_homebrew_dir]:
if not directory.exists():
directory.mkdir(parents=True)
for folder in self.homebrew_folders:
folder_path = directory / folder
if not folder_path.exists():
folder_path.mkdir(parents=True)
def clone_repository(self):
"""Clone Decky Loader repository and checkout specific version"""
print(f"\nCloning Decky Loader repository version: {self.release}")
# Clean up existing directory
if os.path.exists(self.app_dir):
print("Removing existing repository...")
self.safe_remove_directory(self.app_dir)
try:
# Clone the repository
subprocess.run([
'git', 'clone', '--no-checkout', # Don't checkout anything yet
'https://github.com/SteamDeckHomebrew/decky-loader.git',
str(self.app_dir)
], check=True)
os.chdir(self.app_dir)
# Fetch all refs
subprocess.run(['git', 'fetch', '--all', '--tags'], check=True)
# Try to checkout the exact version first
try:
subprocess.run(['git', 'checkout', self.release], check=True)
except subprocess.CalledProcessError:
# If exact version fails, try to find the commit for pre-releases
if '-pre' in self.release:
# Get all tags and their commit hashes
result = subprocess.run(
['git', 'ls-remote', '--tags', 'origin'],
capture_output=True, text=True, check=True
)
# Find the commit hash for our version
for line in result.stdout.splitlines():
commit_hash, ref = line.split('\t')
ref = ref.replace('refs/tags/', '')
ref = ref.replace('^{}', '') # Remove annotated tag suffix
if ref == self.release:
print(f"Found commit {commit_hash} for version {self.release}")
subprocess.run(['git', 'checkout', commit_hash], check=True)
break
else:
raise Exception(f"Could not find commit for version {self.release}")
else:
raise
print(f"Successfully checked out version: {self.release}")
# Create version files in key locations with the requested version
version_files = [
'.loader.version',
'frontend/.loader.version',
'backend/.loader.version',
'backend/decky_loader/.loader.version'
]
for version_file in version_files:
file_path = os.path.join(self.app_dir, version_file)
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, 'w') as f:
f.write(self.release) # Use the requested version
except subprocess.CalledProcessError as e:
raise Exception(f"Failed to clone/checkout repository: {str(e)}")
finally:
os.chdir(self.root_dir)
def build_frontend(self):
"""Build frontend files"""
print("Building frontend...")
batch_file = None
original_dir = os.getcwd()
try:
frontend_dir = self.app_dir / "frontend"
if not frontend_dir.exists():
raise Exception(f"Frontend directory not found at {frontend_dir}")
print(f"Changing to frontend directory: {frontend_dir}")
os.chdir(frontend_dir)
# Create .loader.version file with the release tag
version_file = frontend_dir / ".loader.version"
with open(version_file, "w") as f:
f.write(self.release)
self.temp_files.append(str(version_file))
# Create a batch file to run the commands
batch_file = frontend_dir / "build_frontend.bat"
with open(batch_file, "w") as f:
f.write("@echo off\n")
f.write("call pnpm install\n")
f.write("if %errorlevel% neq 0 exit /b %errorlevel%\n")
f.write("call pnpm run build\n")
f.write("if %errorlevel% neq 0 exit /b %errorlevel%\n")
self.temp_files.append(str(batch_file))
print("Running build commands...")
result = subprocess.run([str(batch_file)], check=True, capture_output=True, text=True, shell=True)
print(result.stdout)
except subprocess.CalledProcessError as e:
print(f"Command failed: {e.cmd}")
print(f"Output: {e.output}")
print(f"Error: {e.stderr}")
raise Exception(f"Error building frontend: Command failed - {str(e)}")
except Exception as e:
print(f"Error building frontend: {str(e)}")
raise
finally:
# Always return to original directory
os.chdir(original_dir)
def prepare_backend(self):
"""Prepare backend files for building."""
print("Preparing backend files...")
print("Copying files according to Dockerfile structure...")
# Create src directory if it doesn't exist
os.makedirs(self.src_dir, exist_ok=True)
# Copy backend files from app/backend/decky_loader to src/decky_loader
print("Copying backend files...")
shutil.copytree(os.path.join(self.app_dir, "backend", "decky_loader"),
os.path.join(self.src_dir, "decky_loader"),
dirs_exist_ok=True)
# Copy static, locales, and plugin directories to maintain decky_loader structure
os.makedirs(os.path.join(self.src_dir, "decky_loader"), exist_ok=True)
shutil.copytree(os.path.join(self.app_dir, "backend", "decky_loader", "static"),
os.path.join(self.src_dir, "decky_loader", "static"),
dirs_exist_ok=True)
shutil.copytree(os.path.join(self.app_dir, "backend", "decky_loader", "locales"),
os.path.join(self.src_dir, "decky_loader", "locales"),
dirs_exist_ok=True)
shutil.copytree(os.path.join(self.app_dir, "backend", "decky_loader", "plugin"),
os.path.join(self.src_dir, "decky_loader", "plugin"),
dirs_exist_ok=True)
# Create legacy directory
os.makedirs(os.path.join(self.src_dir, "src", "legacy"), exist_ok=True)
# Copy main.py to src directory
shutil.copy2(os.path.join(self.app_dir, "backend", "main.py"),
os.path.join(self.src_dir, "main.py"))
# Create version file in the src directory
version_file = os.path.join(self.src_dir, ".loader.version")
with open(version_file, "w") as f:
f.write(self.release)
print("Backend preparation completed successfully!")
return True
def install_requirements(self):
"""Install Python requirements"""
print("Installing Python requirements...")
try:
# Try both requirements.txt and pyproject.toml
requirements_file = self.app_dir / "backend" / "requirements.txt"
pyproject_file = self.app_dir / "backend" / "pyproject.toml"
if requirements_file.exists():
subprocess.run([
sys.executable, "-m", "pip", "install", "--user", "-r", str(requirements_file)
], check=True)
elif pyproject_file.exists():
# Install core dependencies directly instead of using poetry
dependencies = [
"aiohttp>=3.8.1",
"psutil>=5.9.0",
"fastapi>=0.78.0",
"uvicorn>=0.17.6",
"python-multipart>=0.0.5",
"watchdog>=2.1.7",
"requests>=2.27.1",
"setuptools>=60.0.0",
"wheel>=0.37.1",
"winregistry>=1.1.1; platform_system == 'Windows'",
"pywin32>=303; platform_system == 'Windows'"
]
# Install each dependency
for dep in dependencies:
try:
subprocess.run([
sys.executable, "-m", "pip", "install", "--user", dep
], check=True)
except subprocess.CalledProcessError as e:
print(f"Warning: Failed to install {dep}: {str(e)}")
continue
else:
print("Warning: No requirements.txt or pyproject.toml found")
except Exception as e:
print(f"Error installing requirements: {str(e)}")
raise
def add_defender_exclusion(self, path):
"""Add Windows Defender exclusion for a path"""
try:
subprocess.run([
"powershell",
"-Command",
f"Add-MpPreference -ExclusionPath '{path}'"
], check=True, capture_output=True)
return True
except:
print("Warning: Could not add Windows Defender exclusion. You may need to run as administrator or manually add an exclusion.")
return False
def remove_defender_exclusion(self, path):
"""Remove Windows Defender exclusion for a path"""
try:
subprocess.run([
"powershell",
"-Command",
f"Remove-MpPreference -ExclusionPath '{path}'"
], check=True, capture_output=True)
except:
print("Warning: Could not remove Windows Defender exclusion.")
def build_executables(self):
"""Build executables using PyInstaller"""
print("\nBuilding executables...")
# Read version from .loader.version
version_file = os.path.join(self.app_dir, '.loader.version')
if not os.path.exists(version_file):
raise Exception("Version file not found. Run clone_repository first.")
with open(version_file, 'r') as f:
version = f.read().strip()
# Normalize version for Python packaging
# Convert v3.0.5-pre1 to 3.0.5rc1
py_version = version.lstrip('v') # Remove v prefix
if '-pre' in py_version:
py_version = py_version.replace('-pre', 'rc')
print(f"Building version: {version} (Python package version: {py_version})")
original_dir = os.getcwd()
backend_dir = os.path.join(self.app_dir, "backend")
dist_dir = os.path.join(backend_dir, "dist")
# Add Windows Defender exclusion for build directories
added_exclusion = self.add_defender_exclusion(backend_dir)
try:
os.chdir(backend_dir)
# Create setup.py with the correct version
setup_py = """
from setuptools import setup, find_packages
setup(
name="decky_loader",
version="%s",
packages=find_packages(),
package_data={
'decky_loader': [
'locales/*',
'static/*',
'.loader.version'
],
},
install_requires=[
'aiohttp>=3.8.1',
'certifi>=2022.6.15',
'packaging>=21.3',
'psutil>=5.9.1',
'requests>=2.28.1',
],
)
""" % py_version
with open("setup.py", "w") as f:
f.write(setup_py)
# Install the package in development mode
subprocess.run([sys.executable, "-m", "pip", "install", "-e", "."], check=True)
# Common PyInstaller arguments
pyinstaller_args = [
sys.executable,
"-m",
"PyInstaller",
"--clean",
"--noconfirm",
"pyinstaller.spec"
]
# First build console version
print("Building PluginLoader.exe (console version)...")
os.environ.pop('DECKY_NOCONSOLE', None) # Ensure env var is not set
subprocess.run(pyinstaller_args, check=True)
# Then build no-console version
print("Building PluginLoader_noconsole.exe...")
os.environ['DECKY_NOCONSOLE'] = '1'
subprocess.run(pyinstaller_args, check=True)
# Clean up environment
os.environ.pop('DECKY_NOCONSOLE', None)
# Copy the built executables to dist
os.makedirs(os.path.join(self.root_dir, "dist"), exist_ok=True)
if os.path.exists(os.path.join("dist", "PluginLoader.exe")):
shutil.copy2(
os.path.join("dist", "PluginLoader.exe"),
os.path.join(self.root_dir, "dist", "PluginLoader.exe")
)
else:
raise Exception("PluginLoader.exe not found after build")
if os.path.exists(os.path.join("dist", "PluginLoader_noconsole.exe")):
shutil.copy2(
os.path.join("dist", "PluginLoader_noconsole.exe"),
os.path.join(self.root_dir, "dist", "PluginLoader_noconsole.exe")
)
else:
raise Exception("PluginLoader_noconsole.exe not found after build")
print("Successfully built executables")
except subprocess.CalledProcessError as e:
raise Exception(f"Failed to build executables: {str(e)}")
finally:
if added_exclusion:
self.remove_defender_exclusion(backend_dir)
os.chdir(original_dir)
def install_files(self):
"""Install files to homebrew directory"""
print("\nInstalling files to homebrew directory...")
# Create homebrew directory if it doesn't exist
homebrew_dir = os.path.join(os.path.expanduser("~"), "homebrew")
services_dir = os.path.join(homebrew_dir, "services")
os.makedirs(services_dir, exist_ok=True)
try:
# Copy PluginLoader.exe and PluginLoader_noconsole.exe
for exe_name in ["PluginLoader.exe", "PluginLoader_noconsole.exe"]:
exe_source = os.path.join(self.root_dir, "dist", exe_name)
exe_dest = os.path.join(services_dir, exe_name)
if not os.path.exists(exe_source):
raise Exception(f"{exe_name} not found at {exe_source}")
shutil.copy2(exe_source, exe_dest)
# Create .loader.version file
version_file = os.path.join(services_dir, ".loader.version")
with open(version_file, "w") as f:
f.write(self.release)
print("Successfully installed files")
except Exception as e:
raise Exception(f"Failed to copy files to homebrew: {str(e)}")
def install_nodejs(self):
"""Install Node.js v18.18.0 with npm"""
print("Installing Node.js v18.18.0...")
try:
# First check if Node.js v18.18.0 is already installed in common locations
nodejs_paths = [
r"C:\Program Files\nodejs\node.exe",
r"C:\Program Files (x86)\nodejs\node.exe",
os.path.expandvars(r"%APPDATA%\Local\Programs\nodejs\node.exe")
]
# Try to use existing Node.js 18.18.0 first
for node_path in nodejs_paths:
if os.path.exists(node_path):
try:
version = subprocess.run([node_path, "--version"], capture_output=True, text=True).stdout.strip()
if version.startswith("v18.18.0"):
print(f"Found Node.js {version} at {node_path}")
node_dir = os.path.dirname(node_path)
if node_dir not in os.environ["PATH"]:
os.environ["PATH"] = node_dir + os.pathsep + os.environ["PATH"]
return True
except:
continue
# If we get here, we need to install Node.js 18.18.0
print("Installing Node.js v18.18.0...")
# Create temp directory for downloads
temp_dir = self.root_dir / "temp"
temp_dir.mkdir(exist_ok=True)
# Download Node.js installer
node_installer = temp_dir / "node-v18.18.0-x64.msi"
if not node_installer.exists():
print("Downloading Node.js installer...")
try:
import urllib.request
urllib.request.urlretrieve(
"https://nodejs.org/dist/v18.18.0/node-v18.18.0-x64.msi",
node_installer
)
except Exception as e:
print(f"Error downloading Node.js installer: {str(e)}")
raise
# Install Node.js silently
print("Installing Node.js (this may take a few minutes)...")
try:
# First try to uninstall any existing Node.js using PowerShell
uninstall_cmd = 'Get-WmiObject -Class Win32_Product | Where-Object { $_.Name -like "*Node.js*" } | ForEach-Object { $_.Uninstall() }'
subprocess.run(["powershell", "-Command", uninstall_cmd], capture_output=True, timeout=60)
# Wait a bit for uninstallation to complete
time.sleep(5)
# Now install Node.js 18.18.0
subprocess.run(
["msiexec", "/i", str(node_installer), "/qn", "ADDLOCAL=ALL"],
check=True,
timeout=300 # 5 minute timeout
)
print("Waiting for Node.js installation to complete...")
time.sleep(10)
# Add to PATH
nodejs_path = r"C:\Program Files\nodejs"
npm_path = os.path.join(os.environ["APPDATA"], "npm")
# Update PATH for current process
if nodejs_path not in os.environ["PATH"]:
os.environ["PATH"] = nodejs_path + os.pathsep + os.environ["PATH"]
if npm_path not in os.environ["PATH"]:
os.environ["PATH"] = npm_path + os.pathsep + os.environ["PATH"]
# Verify installation
node_version = subprocess.run(["node", "--version"], capture_output=True, text=True, check=True).stdout.strip()
if not node_version.startswith("v18.18.0"):
raise Exception(f"Wrong Node.js version installed: {node_version}")
npm_version = subprocess.run(["npm", "--version"], capture_output=True, text=True, check=True).stdout.strip()
print(f"Successfully installed Node.js {node_version} with npm {npm_version}")
# Clean up
self.safe_remove_directory(temp_dir)
return True
except subprocess.TimeoutExpired:
print("Installation timed out. Please try installing Node.js v18.18.0 manually.")
raise
except Exception as e:
print(f"Installation failed: {str(e)}")
raise
except Exception as e:
print(f"Error installing Node.js: {str(e)}")
raise
def setup_steam_config(self):
"""Configure Steam for Decky Loader"""
print("Configuring Steam...")
try:
# Add -dev argument to Steam shortcut
import winreg
steam_path = None
# Try to find Steam installation path from registry
try:
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\Valve\Steam") as key:
steam_path = winreg.QueryValueEx(key, "InstallPath")[0]
except:
try:
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Valve\Steam") as key:
steam_path = winreg.QueryValueEx(key, "InstallPath")[0]
except:
print("Steam installation not found in registry")
if steam_path:
steam_exe = Path(steam_path) / "steam.exe"
if steam_exe.exists():
# Create .cef-enable-remote-debugging file
debug_file = Path(steam_path) / ".cef-enable-remote-debugging"
debug_file.touch()
print("Created .cef-enable-remote-debugging file")
# Create/modify Steam shortcut
desktop = Path.home() / "Desktop"
shortcut_path = desktop / "Steam.lnk"
import pythoncom
from win32com.client import Dispatch
shell = Dispatch("WScript.Shell")
shortcut = shell.CreateShortCut(str(shortcut_path))
shortcut.Targetpath = str(steam_exe)
shortcut.Arguments = "-dev"
shortcut.save()
print("Created Steam shortcut with -dev argument")
except Exception as e:
print(f"Error configuring Steam: {str(e)}")
raise
def setup_autostart(self):
"""Setup PluginLoader to run at startup"""
print("Setting up autostart...")
try:
# Get the path to the no-console executable
services_dir = os.path.join(os.path.expanduser("~"), "homebrew", "services")
plugin_loader = os.path.join(services_dir, "PluginLoader_noconsole.exe")
# Get the Windows Startup folder path
startup_folder = os.path.join(os.environ["APPDATA"], "Microsoft", "Windows", "Start Menu", "Programs", "Startup")
# Create a batch file in the startup folder
startup_bat = os.path.join(startup_folder, "start_decky.bat")
# Write the batch file with proper path escaping
with open(startup_bat, "w") as f:
f.write(f'@echo off\n"{plugin_loader}"')
print(f"Created startup script at: {startup_bat}")
return True
except Exception as e:
print(f"Error setting up autostart: {str(e)}")
return False
def check_python_version(self):
"""Check if correct Python version is being used"""
print("Checking Python version...")
if sys.version_info.major != 3 or sys.version_info.minor != 11:
raise Exception("This script requires Python 3.11. Please run using decky_builder.bat")
def check_dependencies(self):
"""Check and install required dependencies"""
print("Checking dependencies...")
try:
# Check Node.js and npm first
try:
# Use shell=True to find node in PATH
node_version = subprocess.run("node --version", shell=True, check=True, capture_output=True, text=True).stdout.strip()
npm_version = subprocess.run("npm --version", shell=True, check=True, capture_output=True, text=True).stdout.strip()
# Check if version meets requirements
if not node_version.startswith("v18."):
print(f"Node.js {node_version} found, but v18.18.0 is required")
self.install_nodejs()
else:
print(f"Node.js {node_version} with npm {npm_version} is installed")
except Exception as e:
print(f"Node.js/npm not found or error: {str(e)}")
self.install_nodejs()
# Install pnpm globally if not present
try:
pnpm_version = subprocess.run("pnpm --version", shell=True, check=True, capture_output=True, text=True).stdout.strip()
print(f"pnpm version {pnpm_version} is installed")
except:
print("Installing pnpm globally...")
subprocess.run("npm i -g pnpm", shell=True, check=True)
pnpm_version = subprocess.run("pnpm --version", shell=True, check=True, capture_output=True, text=True).stdout.strip()
print(f"Installed pnpm version {pnpm_version}")
# Check git
try:
git_version = subprocess.run("git --version", shell=True, check=True, capture_output=True, text=True).stdout.strip()
print(f"{git_version} is installed")
except:
raise Exception("git is not installed. Please install git from https://git-scm.com/downloads")
print("All dependencies are satisfied")
except Exception as e:
print(f"Error checking dependencies: {str(e)}")
raise
def get_release_versions(self):
"""Get list of available release versions"""
print("Fetching available versions...")
try:
response = requests.get(
"https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases"
)
response.raise_for_status()
releases = response.json()
# Split releases into stable and pre-release
stable_releases = []
pre_releases = []
for release in releases:
version = release['tag_name']
if release['prerelease']:
pre_releases.append(version)
else:
stable_releases.append(version)
# Sort versions and take only the latest 3 of each
stable_releases.sort(reverse=True)
pre_releases.sort(reverse=True)
stable_releases = stable_releases[:3]
pre_releases = pre_releases[:3]
# Combine and sort all versions
all_versions = stable_releases + pre_releases
all_versions.sort(reverse=True)
return all_versions
except requests.RequestException as e:
raise Exception(f"Failed to fetch release versions: {str(e)}")
def prompt_for_version(self):
"""Prompt the user to select a version to install."""
versions = self.get_release_versions()
print("\nAvailable versions:")
print("Stable versions:")
stable_count = 0
for i, version in enumerate(versions):
if '-pre' not in version:
print(f"{i+1}. {version}")
stable_count += 1
print("\nPre-release versions:")
for i, version in enumerate(versions):
if '-pre' in version:
print(f"{i+1}. {version}")
while True:
try:
choice = input("\nSelect a version (1-{}): ".format(len(versions)))
index = int(choice) - 1
if 0 <= index < len(versions):
return versions[index]
print("Invalid selection, please try again.")
except ValueError:
print("Invalid input, please enter a number.")
def terminate_processes(self):
"""Terminate running instances of executables that may interfere with the build."""
for proc in psutil.process_iter(['pid', 'name', 'exe']):
if proc.info['name'] in ['PluginLoader.exe', 'PluginLoader_noconsole.exe']:
try:
proc.terminate()
proc.wait()
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
pass
def run(self):
"""Run the build and installation process."""
# Terminate interfering processes
self.terminate_processes()
try:
print("Starting Decky Loader build process...")
self.check_python_version()
self.check_dependencies()
self.setup_directories()
self.clone_repository()
self.setup_homebrew()
self.build_frontend()
self.prepare_backend()
self.install_requirements()
self.build_executables()
self.install_files()
self.setup_steam_config()
self.setup_autostart()
print("\nBuild process completed successfully!")
print("\nNext steps:")
print("1. Close Steam if it's running")
print("2. Launch Steam using the new shortcut on your desktop")
print("3. Enter Big Picture Mode")
print("4. Hold the STEAM button and press A to access the Decky menu")
except Exception as e:
print(f"Error during build process: {str(e)}")
raise
finally:
self.cleanup()
def main():
parser = argparse.ArgumentParser(description='Build and Install Decky Loader for Windows')
parser.add_argument('--release', required=False, default=None,
help='Release version/branch to build (if not specified, will prompt for version)')
args = parser.parse_args()
try:
builder = DeckyBuilder(args.release)
builder.run()
print(f"\nDecky Loader has been installed to: {builder.user_homebrew_dir}")
except Exception as e:
print(f"Error during build process: {str(e)}")
sys.exit(1)
if __name__ == "__main__":
main()