summaryrefslogtreecommitdiff
path: root/backend
diff options
context:
space:
mode:
authorAAGaming <aagaming@riseup.net>2026-10-17 20:04:39 -0400
committerGitHub <noreply@github.com>2026-10-17 20:04:39 -0400
commit636b86cf3291e3934a1dc0dd57afb5eb7056a431 (patch)
tree854301c45c6ea1fbee28dd0a1399ed55fdb113f2 /backend
parent410a141cf3c5d0192e4a1a07d7d10be298ab02f4 (diff)
downloaddecky-loader-636b86cf3291e3934a1dc0dd57afb5eb7056a431.tar.gz
decky-loader-636b86cf3291e3934a1dc0dd57afb5eb7056a431.zip
feat(loader): support ARM Linux, experimental builds for every platform that exists (#990)
Co-authored-by: Gianni Spadoni <me@gio.blue> Co-authored-by: virtudude <virtudude@protonmail.com> Signed-off-by: Gianni Spadoni <me@gio.blue>
Diffstat (limited to 'backend')
-rw-r--r--backend/decky_loader/injector.py4
-rw-r--r--backend/decky_loader/localplatform/localplatform.py3
-rw-r--r--backend/decky_loader/plugin/sandboxed_plugin.py3
-rw-r--r--backend/decky_loader/updater.py106
-rw-r--r--backend/pyinstaller.spec27
5 files changed, 108 insertions, 35 deletions
diff --git a/backend/decky_loader/injector.py b/backend/decky_loader/injector.py
index 39b5b15d..ded58193 100644
--- a/backend/decky_loader/injector.py
+++ b/backend/decky_loader/injector.py
@@ -4,7 +4,7 @@ from asyncio import sleep
from logging import getLogger
from typing import Any, Callable, List, TypedDict, Dict
-from aiohttp import ClientSession
+from aiohttp import ClientSession, ClientTimeout
from aiohttp.client_exceptions import ClientConnectorError, ClientOSError
from asyncio.exceptions import TimeoutError
import uuid
@@ -374,7 +374,7 @@ async def get_tabs() -> List[Tab]:
while True:
try:
async with ClientSession() as web:
- res = await web.get(f"{BASE_ADDRESS}/json", timeout=3)
+ res = await web.get(f"{BASE_ADDRESS}/json", timeout=ClientTimeout(total=3))
except ClientConnectorError:
if not na:
logger.debug("Steam isn't available yet. Wait for a moment...")
diff --git a/backend/decky_loader/localplatform/localplatform.py b/backend/decky_loader/localplatform/localplatform.py
index 6cb08982..e7760315 100644
--- a/backend/decky_loader/localplatform/localplatform.py
+++ b/backend/decky_loader/localplatform/localplatform.py
@@ -4,6 +4,9 @@ ON_WINDOWS = platform.system() == "Windows"
ON_MAC = platform.system() == "Darwin"
ON_LINUX = not ON_WINDOWS and not ON_MAC
+# macOS reports arm64, Linux reports aarch64
+ON_ARM64 = platform.machine() == "arm64" or platform.machine() == "aarch64"
+
if ON_WINDOWS:
from .localplatformwin import *
from . import localplatformwin as localplatform
diff --git a/backend/decky_loader/plugin/sandboxed_plugin.py b/backend/decky_loader/plugin/sandboxed_plugin.py
index 675eb464..668e1d25 100644
--- a/backend/decky_loader/plugin/sandboxed_plugin.py
+++ b/backend/decky_loader/plugin/sandboxed_plugin.py
@@ -211,5 +211,4 @@ class SandboxedPlugin:
except Exception as e:
d["res"] = str(e)
d["success"] = False
- finally:
- return dumps(d, ensure_ascii=False)
+ return dumps(d, ensure_ascii=False)
diff --git a/backend/decky_loader/updater.py b/backend/decky_loader/updater.py
index 3fdfcc04..0a9356b6 100644
--- a/backend/decky_loader/updater.py
+++ b/backend/decky_loader/updater.py
@@ -1,12 +1,11 @@
from __future__ import annotations
from asyncio import sleep
from logging import getLogger
-import os
-from os import getcwd, path, remove
+from os import getcwd, getenv, path, remove, mkdir
from typing import TYPE_CHECKING, List, TypedDict
if TYPE_CHECKING:
from .main import PluginManager
-from .localplatform.localplatform import chmod, service_restart, service_stop, ON_LINUX, ON_WINDOWS, get_keep_systemd_service, get_selinux
+from .localplatform.localplatform import chmod, service_restart, service_stop, ON_LINUX, ON_WINDOWS, ON_MAC, ON_ARM64, get_keep_systemd_service, get_selinux
import shutil
from typing import List, TYPE_CHECKING, TypedDict
import zipfile
@@ -42,6 +41,8 @@ class Updater:
self.remoteVer: RemoteVer | None = None
self.allRemoteVers: List[RemoteVer] = []
self.localVer = helpers.get_loader_version()
+ self.repo = getenv('DECKY_REPO', 'SteamDeckHomebrew/decky-loader')
+ self.git_branch = getenv('DECKY_BRANCH', 'dirty')
try:
self.currentBranch = self.get_branch(self.context.settings)
@@ -50,13 +51,13 @@ class Updater:
logger.error("Current branch could not be determined, defaulting to \"Stable\"")
if context:
- context.ws.add_route("updater/get_version_info", self.get_version_info);
- context.ws.add_route("updater/check_for_updates", self.check_for_updates);
- context.ws.add_route("updater/do_restart", self.do_restart);
- context.ws.add_route("updater/do_shutdown", self.do_shutdown);
- context.ws.add_route("updater/do_update", self.do_update);
- context.ws.add_route("updater/get_testing_versions", self.get_testing_versions);
- context.ws.add_route("updater/download_testing_version", self.download_testing_version);
+ context.ws.add_route("updater/get_version_info", self.get_version_info)
+ context.ws.add_route("updater/check_for_updates", self.check_for_updates)
+ context.ws.add_route("updater/do_restart", self.do_restart)
+ context.ws.add_route("updater/do_shutdown", self.do_shutdown)
+ context.ws.add_route("updater/do_update", self.do_update)
+ context.ws.add_route("updater/get_testing_versions", self.get_testing_versions)
+ context.ws.add_route("updater/download_testing_version", self.download_testing_version)
context.loop.create_task(self.version_reloader())
def get_branch(self, manager: SettingsManager):
@@ -83,17 +84,19 @@ class Updater:
branch = self.get_branch(self.context.settings)
match branch:
case 0:
- url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-release.service"
+ url = f"https://raw.githubusercontent.com/{self.repo}/{self.git_branch}/dist/plugin_loader-release.service"
case 1 | 2:
- url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service"
+ url = f"https://raw.githubusercontent.com/{self.repo}/{self.git_branch}/dist/plugin_loader-prerelease.service"
case _:
logger.error("You have an invalid branch set... Defaulting to prerelease service, please send the logs to the devs!")
- url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service"
+ url = f"https://raw.githubusercontent.com/{self.repo}/{self.git_branch}/dist/plugin_loader-prerelease.service"
return str(url)
async def get_version_info(self):
return {
"current": self.localVer,
+ "repo": self.repo,
+ "gitBranch": self.git_branch,
"remote": self.remoteVer,
"all": self.allRemoteVers,
"updatable": self.localVer != "unknown" and self.localVer != "dev"
@@ -103,7 +106,7 @@ class Updater:
logger.debug("checking for updates")
selectedBranch = self.get_branch(self.context.settings)
async with ClientSession() as web:
- async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases", headers={'X-GitHub-Api-Version': '2022-11-28'}, ssl=helpers.get_ssl_context()) as res:
+ async with web.request("GET", f"https://api.github.com/repos/{self.repo}/releases", headers={'X-GitHub-Api-Version': '2022-11-28'}, ssl=helpers.get_ssl_context()) as res:
remoteVersions: List[RemoteVer] = await res.json()
if selectedBranch == 0:
logger.debug("release type: release")
@@ -138,12 +141,48 @@ class Updater:
pass
await sleep(60 * 60 * 6) # 6 hours
+ def get_remote_binary_name(self, with_extension: bool = True):
+ """
+ The binaries on GitHub contain an extra part in their name to denote their architecture
+
+ For example:
+ Windows: PluginLoader.exe
+ Linux (x86_64): PluginLoader
+ Linux (arm64): PluginLoader-arm64
+ """
+ binary_name = "PluginLoader"
+
+ if ON_ARM64 and ON_LINUX:
+ binary_name += "-arm64"
+
+ if ON_WINDOWS:
+ binary_name += "-win"
+ if ON_ARM64:
+ binary_name += "-arm64"
+ if bool(getenv("DECKY_NOCONSOLE", "0")):
+ binary_name += "-noconsole"
+ if with_extension:
+ binary_name += ".exe"
+
+ if ON_MAC:
+ binary_name += "-mac"
+
+ return binary_name
+
+ def get_local_binary_name(self):
+ binary_name = "PluginLoader"
+
+ if ON_WINDOWS:
+ binary_name += ".exe"
+
+ return binary_name
+
async def download_decky_binary(self, download_url: str, version: str, is_zip: bool = False, size_in_bytes: int | None = None):
- download_filename = "PluginLoader" if ON_LINUX else "PluginLoader.exe"
+ download_filename = self.get_local_binary_name()
download_temp_filename = download_filename + ".new"
if size_in_bytes == None:
- size_in_bytes = 26214400 # 25MiB, a reasonable overestimate (19.6MiB as of 2024/02/25)
+ size_in_bytes = 35000000 # 35MiB, a reasonable overestimate (30.00 MiB as of 2026/10/05)
async with ClientSession() as web:
logger.debug("Downloading binary")
@@ -196,10 +235,10 @@ class Updater:
version = self.remoteVer["tag_name"]
download_url = None
size_in_bytes = None
- download_filename = "PluginLoader" if ON_LINUX else "PluginLoader.exe"
+ remote_binary_name = self.get_remote_binary_name()
for x in self.remoteVer["assets"]:
- if x["name"] == download_filename:
+ if x["name"] == remote_binary_name:
download_url = x["browser_download_url"]
size_in_bytes = x["size"]
break
@@ -233,8 +272,8 @@ class Updater:
logger.debug("Saved service file")
logger.debug("Copying service file over current file.")
shutil.copy(service_file_path, "/etc/systemd/system/plugin_loader.service")
- if not os.path.exists(path.join(getcwd(), ".systemd")):
- os.mkdir(path.join(getcwd(), ".systemd"))
+ if not path.exists(path.join(getcwd(), ".systemd")):
+ mkdir(path.join(getcwd(), ".systemd"))
shutil.move(service_file_path, path.join(getcwd(), ".systemd")+"/plugin_loader.service")
await self.download_decky_binary(download_url, version, size_in_bytes=size_in_bytes)
@@ -249,7 +288,7 @@ class Updater:
async def get_testing_versions(self) -> List[TestingVersion]:
result: List[TestingVersion] = []
async with ClientSession() as web:
- async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/pulls",
+ async with web.request("GET", f"https://api.github.com/repos/{self.repo}/pulls",
headers={'X-GitHub-Api-Version': '2022-11-28'}, params={'state':'open'}, ssl=helpers.get_ssl_context()) as res:
open_prs = await res.json()
for pr in open_prs:
@@ -265,27 +304,42 @@ class Updater:
down_id = ''
#Get all the associated workflow run for the given sha_id code hash
async with ClientSession() as web:
- async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/actions/runs",
+ async with web.request("GET", f"https://api.github.com/repos/{self.repo}/actions/runs",
headers={'X-GitHub-Api-Version': '2022-11-28'}, params={'head_sha': sha_id}, ssl=helpers.get_ssl_context()) as res:
works = await res.json()
#Iterate over the workflow_run to get the two builds if they exists
for work in works['workflow_runs']:
- if ON_WINDOWS and work['name'] == 'Builder Win':
+ if ON_WINDOWS and ON_ARM64 and work['name'] == 'Builder (Windows, arm64)':
down_id=work['id']
break
- elif ON_LINUX and work['name'] == 'Builder':
+ elif ON_WINDOWS and not ON_ARM64 and work['name'] == 'Builder (Windows, x86_64)':
down_id=work['id']
break
+ elif ON_LINUX and ON_ARM64 and work['name'] == 'Builder (Linux, arm64)':
+ down_id=work['id']
+ break
+ elif ON_LINUX and not ON_ARM64 and work['name'] == 'Builder (Linux, x86_64)':
+ down_id=work['id']
+ break
+ elif ON_MAC and work['name'] == 'Builder (macOS)':
+ down_id=work['id']
+ break
+
if down_id != '':
async with ClientSession() as web:
- async with web.request("GET", f"https://api.github.com/repos/SteamDeckHomebrew/decky-loader/actions/runs/{down_id}/artifacts",
+ async with web.request("GET", f"https://api.github.com/repos/{self.repo}/actions/runs/{down_id}/artifacts",
headers={'X-GitHub-Api-Version': '2022-11-28'}, ssl=helpers.get_ssl_context()) as res:
jresp = await res.json()
#If the request found at least one artifact to download...
if int(jresp['total_count']) != 0:
# this assumes that the artifact we want is the first one!
artifact = jresp['artifacts'][0]
- down_link = f"https://nightly.link/SteamDeckHomebrew/decky-loader/actions/artifacts/{artifact['id']}.zip"
+ target_art = self.get_remote_binary_name(False)
+ for art in jresp['artifacts']:
+ if art['name'] == target_art:
+ artifact = art
+ break
+ down_link = f"https://nightly.link/{self.repo}/actions/artifacts/{artifact['id']}.zip"
#Then fetch it and restart itself
await self.download_decky_binary(down_link, f'PR-{pr_id}', is_zip=True, size_in_bytes=artifact.get('size_in_bytes',None))
else:
diff --git a/backend/pyinstaller.spec b/backend/pyinstaller.spec
index 13ac66a0..3ef888f5 100644
--- a/backend/pyinstaller.spec
+++ b/backend/pyinstaller.spec
@@ -3,21 +3,38 @@ from PyInstaller.building.build_main import Analysis
from PyInstaller.building.api import EXE, PYZ
from PyInstaller.utils.hooks import copy_metadata
+noconsole_str = os.getenv('DECKY_NOCONSOLE', '0')
+noconsole = bool(noconsole_str)
+repo = os.getenv('DECKY_REPO', 'SteamDeckHomebrew/decky-loader')
+branch = os.getenv('DECKY_BRANCH', 'main')
+with open('build_config.py', 'w') as f:
+ f.write(f"""
+import os
+os.environ['DECKY_PACKAGED_BUILD'] = '1'
+
+# Build info
+os.environ['DECKY_REPO'] = {repr(repo)}
+os.environ['DECKY_BRANCH'] = {repr(branch)}
+
+# Windows-specific
+os.environ['DECKY_NOCONSOLE'] = {repr(noconsole_str)}
+""")
+
+name = "PluginLoader"
+# if noconsole:
+# name += "_noconsole"
+
a = Analysis(
['main.py'],
datas=[
('decky_loader/locales', 'decky_loader/locales'),
('decky_loader/static', 'decky_loader/static'),
] + copy_metadata('decky_loader'),
+ runtime_hooks=['build_config.py'],
hiddenimports=['logging.handlers', 'sqlite3', 'http.server', 'socketserver', 'configparser', 'decky_plugin', 'decky'],
)
pyz = PYZ(a.pure, a.zipped_data)
-noconsole = bool(os.getenv('DECKY_NOCONSOLE'))
-name = "PluginLoader"
-if noconsole:
- name += "_noconsole"
-
exe = EXE(
pyz,
a.scripts,