Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/build-linux-arm64.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: Builder ARM64

on:
push:
pull_request:
workflow_call:

jobs:
build:
uses: ./.github/workflows/build-linux.yml
with:
arch: arm64
artifact_name: PluginLoader-arm64
13 changes: 13 additions & 0 deletions .github/workflows/build-linux-x86_64.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: Builder

on:
push:
pull_request:
workflow_call:

jobs:
build:
uses: ./.github/workflows/build-linux.yml
with:
arch: x86_64
artifact_name: PluginLoader
21 changes: 13 additions & 8 deletions .github/workflows/build.yml → .github/workflows/build-linux.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
name: Builder
name: Builder (Linux)

on:
push:
pull_request:
workflow_call:
# schedule:
# - cron: '0 13 * * *' # run at 1 PM UTC
inputs:
arch:
description: Architecture to build for (x86_64, arm64)
type: string
default: x86_64
artifact_name:
description: Name of the uploaded artifact; the binary inside is always PluginLoader
type: string
default: PluginLoader

jobs:
build:
name: Build PluginLoader
runs-on: ubuntu-22.04
name: Build ${{ inputs.artifact_name }} (Linux ${{ inputs.arch }})
runs-on: ${{ inputs.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-22.04' }}

steps:
- name: Checkout 🧰
Expand Down Expand Up @@ -59,7 +64,7 @@ jobs:
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v4
with:
name: PluginLoader
name: ${{ inputs.artifact_name }}
path: ./backend/dist/PluginLoader

- name: Download package artifact locally
Expand Down
54 changes: 48 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ name: Release
on:
workflow_dispatch:
inputs:
preview_tag:
type: string
description: Optional DroidDeck Preview tag (droiddeck-arm64-preview.N)
default: ''
release:
type: choice
description: Release the asset
Expand Down Expand Up @@ -51,7 +55,17 @@ jobs:

- name: Prepare tag ⚙️
id: ready_tag
env:
PREVIEW_TAG: ${{ inputs.preview_tag }}
run: |
if [[ -n "$PREVIEW_TAG" ]]; then
if [[ "${{ inputs.release }}" != "prerelease" || ! "$PREVIEW_TAG" =~ ^droiddeck-arm64-preview\.[1-9][0-9]*$ ]]; then
echo "DroidDeck Preview tags require prerelease and the format droiddeck-arm64-preview.N"
exit 1
fi
echo "tag_name=$PREVIEW_TAG" >> "$GITHUB_OUTPUT"
exit 0
fi
export VERSION=${{ steps.latest_release.outputs.release }}
echo "VERS: $VERSION"
if [[ ${{github.event.inputs.release}} == "release" ]]; then
Expand Down Expand Up @@ -123,26 +137,50 @@ jobs:

build:
name: Build tagged artifact
uses: ./.github/workflows/build.yml
uses: ./.github/workflows/build-linux-x86_64.yml
needs: [create_tag]

build_arm64:
name: Build tagged artifact (arm64)
uses: ./.github/workflows/build-linux-arm64.yml
needs: [create_tag]

release:
name: Release tagged artifact
name: Release tagged artifacts
runs-on: ubuntu-22.04
needs: [create_tag, build]
needs: [create_tag, build, build_arm64]
steps:
- name: Fetch package artifact ⬇️
- name: Fetch package artifact (x86_64) ⬇️
uses: actions/download-artifact@v4
with:
name: PluginLoader
path: ./artifacts/x86_64

- name: Fetch package artifact (arm64) ⬇️
uses: actions/download-artifact@v4
with:
name: PluginLoader-arm64
path: ./artifacts/arm64

# Release assets need unique names; the artifacts both contain "PluginLoader"
- name: Name release assets 🛠️
run: |
mv ./artifacts/x86_64/PluginLoader ./PluginLoader
mv ./artifacts/arm64/PluginLoader ./PluginLoader-arm64
sha256sum PluginLoader > PluginLoader.sha256
sha256sum PluginLoader-arm64 > PluginLoader-arm64.sha256

- name: Pre-release 📦
if: github.event.inputs.release == 'prerelease'
uses: softprops/action-gh-release@v1
with:
name: Prerelease ${{ needs.create_tag.outputs.tag_name }}
tag_name: ${{ needs.create_tag.outputs.tag_name }}
files: ./PluginLoader
files: |
./PluginLoader
./PluginLoader-arm64
./PluginLoader.sha256
./PluginLoader-arm64.sha256
prerelease: true
generate_release_notes: true
- name: Release 📦
Expand All @@ -151,6 +189,10 @@ jobs:
with:
name: Release ${{ needs.create_tag.outputs.tag_name }}
tag_name: ${{ needs.create_tag.outputs.tag_name }}
files: ./PluginLoader
files: |
./PluginLoader
./PluginLoader-arm64
./PluginLoader.sha256
./PluginLoader-arm64.sha256
prerelease: false
generate_release_notes: true
3 changes: 3 additions & 0 deletions backend/decky_loader/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,9 @@
"developer_mode": {
"label": "Developer mode"
},
"sort_plugins": {
"label": "Sort plugins alphabetically"
},
"notifications": {
"decky_updates_label": "Decky update available",
"header": "Notifications",
Expand Down
3 changes: 3 additions & 0 deletions backend/decky_loader/localplatform/localplatform.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
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
Expand Down
61 changes: 54 additions & 7 deletions backend/decky_loader/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
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_ARM64, get_keep_systemd_service, get_selinux
import shutil
from typing import List, TYPE_CHECKING, TypedDict
import zipfile
Expand Down Expand Up @@ -92,15 +92,24 @@ def get_service_url(self):
return str(url)

async def get_version_info(self):
managed_externally = ON_LINUX and ON_ARM64
return {
"current": self.localVer,
"remote": self.remoteVer,
"all": self.allRemoteVers,
"updatable": self.localVer != "unknown" and self.localVer != "dev"
"all": None if managed_externally else self.allRemoteVers,
"updatable": self.localVer != "unknown" and self.localVer != "dev" and not managed_externally,
"managed_externally": managed_externally,
}

async def check_for_updates(self):
logger.debug("checking for updates")
# DroidDeck installs and updates the ARM64 binary from its compatible release
# assets. The upstream updater only knows the regular Linux binary and could
# replace the working ARM64 loader with an incompatible executable.
if ON_LINUX and ON_ARM64:
self.remoteVer = None
self.allRemoteVers = []
return await self.get_version_info()
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:
Expand Down Expand Up @@ -138,8 +147,35 @@ async def version_reloader(self):
pass
await sleep(60 * 60 * 6) # 6 hours

def get_remote_binary_name(self):
"""
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 += ".exe"

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:
Expand Down Expand Up @@ -187,6 +223,9 @@ async def download_decky_binary(self, download_url: str, version: str, is_zip: b

async def do_update(self):
logger.debug("Starting update.")
if ON_LINUX and ON_ARM64:
logger.warning("ARM64 Decky updates are managed by DroidDeck.")
return
try:
assert self.remoteVer
except AssertionError:
Expand All @@ -196,10 +235,10 @@ async def do_update(self):
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
Expand Down Expand Up @@ -247,6 +286,8 @@ async def do_shutdown(self):
await service_stop("plugin_loader")

async def get_testing_versions(self) -> List[TestingVersion]:
if ON_LINUX and ON_ARM64:
return []
result: List[TestingVersion] = []
async with ClientSession() as web:
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/pulls",
Expand All @@ -262,6 +303,9 @@ async def get_testing_versions(self) -> List[TestingVersion]:
return result

async def download_testing_version(self, pr_id: int, sha_id: str):
if ON_LINUX and ON_ARM64:
logger.warning("ARM64 testing builds are managed by DroidDeck.")
return
down_id = ''
#Get all the associated workflow run for the given sha_id code hash
async with ClientSession() as web:
Expand All @@ -273,7 +317,10 @@ async def download_testing_version(self, pr_id: int, sha_id: str):
if ON_WINDOWS and work['name'] == 'Builder Win':
down_id=work['id']
break
elif ON_LINUX and work['name'] == 'Builder':
elif ON_LINUX and ON_ARM64 and work['name'] == 'Builder ARM64':
down_id=work['id']
break
elif ON_LINUX and not ON_ARM64 and work['name'] == 'Builder':
down_id=work['id']
break
if down_id != '':
Expand Down
30 changes: 17 additions & 13 deletions backend/decky_loader/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,20 +333,24 @@ async def filepicker_ls(self,

#Resolving all files/folders in the requested directory
for file in path_obj.iterdir():
if file.exists():
filest = file.stat()
is_hidden = file.name.startswith('.')
if ON_WINDOWS and not is_hidden:
is_hidden = bool(filest.st_file_attributes & FILE_ATTRIBUTE_HIDDEN) # type: ignore
if include_folders and file.is_dir():
if (is_hidden and include_hidden) or not is_hidden:
folders.append({"file": file, "filest": filest, "is_dir": True})
elif include_files:
# Handle requested extensions if present
if include_ext == None or len(include_ext) == 0 or 'all_files' in include_ext \
or splitext(file.name)[1].lstrip('.').upper() in (ext.upper() for ext in include_ext):
try:
if file.exists():
filest = file.stat()
is_hidden = file.name.startswith('.')
if ON_WINDOWS and not is_hidden:
is_hidden = bool(filest.st_file_attributes & FILE_ATTRIBUTE_HIDDEN) # type: ignore
if include_folders and file.is_dir():
if (is_hidden and include_hidden) or not is_hidden:
files.append({"file": file, "filest": filest, "is_dir": False})
folders.append({"file": file, "filest": filest, "is_dir": True})
elif include_files:
# Handle requested extensions if present
if include_ext == None or len(include_ext) == 0 or 'all_files' in include_ext \
or splitext(file.name)[1].lstrip('.').upper() in (ext.upper() for ext in include_ext):
if (is_hidden and include_hidden) or not is_hidden:
files.append({"file": file, "filest": filest, "is_dir": False})
except (PermissionError, OSError):
self.logger.debug(f"Skipping inaccessible file picker entry: {file}")
continue
# Filter logic
if filter_for is not None:
try:
Expand Down
2 changes: 1 addition & 1 deletion backend/pyinstaller.spec
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ a = Analysis(
('decky_loader/locales', 'decky_loader/locales'),
('decky_loader/static', 'decky_loader/static'),
] + copy_metadata('decky_loader'),
hiddenimports=['logging.handlers', 'sqlite3', 'decky_plugin', 'decky'],
hiddenimports=['logging.handlers', 'sqlite3', 'http.server', 'socketserver', 'configparser', 'decky_plugin', 'decky'],
)
pyz = PYZ(a.pure, a.zipped_data)

Expand Down
Loading
Loading