Compare commits

...
19 changed files with 415 additions and 6 deletions

No files matched your search

+61
View File
@@ -0,0 +1,61 @@
name: Build KDE Plasma package
# The Plasma shell package (tools/plasma): Arch Linux ARM's KWin and Plasma, closed over their
# dependencies less what the runtime and the desktop package already carry, installed over both in
# a smoke test on an arm64 runner, and published as a release asset that catalog/shells.json names.
on:
push:
paths:
- 'tools/plasma/**'
- '.github/workflows/build-plasma.yml'
workflow_dispatch:
inputs:
tag:
description: 'Release tag to publish as (e.g. plasma-r1); empty = artifact only'
required: false
default: ''
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-24.04-arm
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- name: Build
run: bash tools/plasma/build.sh out
- name: Smoke test over the runtime and the desktop
run: bash tools/plasma/smoke-test.sh out/plasma.tar.zst
- uses: actions/upload-artifact@v4
with:
name: plasma
path: |
out/plasma.tar.zst
out/plasma.sha256
out/plasma.packages.txt
- name: Catalog entry
run: |
set -euo pipefail
tag=${{ inputs.tag || 'plasma-rN' }}
size=$(stat -c %s out/plasma.tar.zst)
sha=$(cat out/plasma.sha256)
{
echo "### catalog/shells.json entry"
echo '```json'
printf '{ "id": "plasma", "name": "KDE Plasma", "tier": 1, "version": "%s", "kind": "tar",\n' "${tag#plasma-}"
printf ' "url": "https://github.com/%s/releases/download/%s/plasma.tar.zst",\n' "$GITHUB_REPOSITORY" "$tag"
printf ' "sha256": "%s", "size": %s,\n' "$sha" "$size"
printf ' "notes": "KWin and plasmashell over the desktop package; Dolphin, Konsole, System Settings" }\n'
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- name: Publish
if: github.event_name == 'workflow_dispatch' && inputs.tag != ''
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
cd out
gh release create "${{ inputs.tag }}" --title "${{ inputs.tag }}" --notes "KDE Plasma for the desktop: tools/plasma/seeds.txt over Arch Linux ARM, less the runtime's and the desktop package's own packages (Qt and KDE libraries shipped again). Extracts over the desktop package. sha256 $(cat plasma.sha256)" plasma.tar.zst plasma.sha256 plasma.packages.txt \
|| gh release upload "${{ inputs.tag }}" plasma.tar.zst plasma.sha256 plasma.packages.txt --clobber
+2
View File
@@ -213,6 +213,8 @@ jobs:
install -Dm644 tools/linuxfs/desktop/droiddeck-desktop "$D/usr/local/bin/droiddeck-desktop"
install -Dm644 tools/linuxfs/desktop/droiddeck-gpu "$D/usr/local/bin/droiddeck-gpu"
install -Dm644 tools/linuxfs/desktop/droiddeck-desktop-gpu "$D/usr/local/bin/droiddeck-desktop-gpu"
install -Dm644 tools/linuxfs/desktop/droiddeck-plasma "$D/usr/local/bin/droiddeck-plasma"
install -Dm644 tools/linuxfs/desktop/droiddeck-plasma-session "$D/usr/local/bin/droiddeck-plasma-session"
install -Dm644 tools/linuxfs/desktop/autostart "$D/etc/xdg/labwc/autostart"
install -Dm644 tools/linuxfs/desktop/rc.xml "$D/etc/xdg/labwc/rc.xml"
install -Dm644 tools/linuxfs/desktop/panel.conf "$D/etc/xdg/lxqt/panel.conf"
@@ -190,6 +190,7 @@ class MainActivity : ComponentActivity() {
private var oscMode by mutableStateOf(SessionPrefs.OSC_AUTO)
private var backActionsInverted by mutableStateOf(false)
private var renderer by mutableStateOf("vulkan")
private var desktopShell by mutableStateOf(SessionPrefs.SHELL_LXQT)
private var gameStorage by mutableStateOf("")
private var storageOptions by mutableStateOf<List<Pair<String, String>>>(emptyList())
private val pickGameStorage = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { r ->
@@ -779,6 +780,7 @@ class MainActivity : ComponentActivity() {
forceFullscreen = if (mode == SessionService.MODE_STEAM) forceFullscreen else null,
mic = if (mode == SessionService.MODE_STEAM) mic else null,
renderer = if (mode == SessionService.MODE_DESKTOP) renderer else null,
shell = if (mode == SessionService.MODE_DESKTOP) desktopShell else null,
gameStorage = if (mode == SessionService.MODE_STEAM) gameStorage else null,
storageOptions = storageOptions,
fexPreset = if (mode == SessionService.MODE_STEAM) fexPreset else null,
@@ -828,6 +830,7 @@ class MainActivity : ComponentActivity() {
}
},
onRenderer = { r -> SessionPrefs.setDesktopRenderer(this, r); renderer = r },
onShell = { shell -> SessionPrefs.setDesktopShell(this, shell); desktopShell = shell },
onGameStorage = { path, label -> setGameStorage(path, label) },
onPickGameStorageFolder = {
pickGameStorage.launch(InAppFilePicker.buildDirIntent(this, "Choose the game storage folder", gameStorage.ifEmpty { null }))
@@ -932,6 +935,7 @@ class MainActivity : ComponentActivity() {
forceFullscreen = SessionPrefs.forceFullscreen(this)
mic = SessionPrefs.micEnabled(this)
renderer = SessionPrefs.desktopRenderer(this)
desktopShell = SessionPrefs.desktopShell(this)
gameStorage = SessionPrefs.gameStorage(this)
storageOptions = GameStorage.options(this).map { it.label to it.path }
if (mode == SessionService.MODE_STEAM) {
@@ -241,7 +241,10 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
!com.droiddeck.launcher.runtime.DesktopCatalog.desktopInstalled(this)
val needProton = (intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM) == SessionService.MODE_STEAM &&
com.droiddeck.launcher.runtime.DesktopCatalog.protonSeedNeeded(this)
if (needRuntime || needDesktop || needProton) installThenStart(needRuntime, needDesktop, needProton)
val needPlasma = intent.getStringExtra(SessionService.EXTRA_MODE) == SessionService.MODE_DESKTOP &&
SessionPrefs.desktopShell(this) == SessionPrefs.SHELL_PLASMA &&
!com.droiddeck.launcher.runtime.DesktopCatalog.plasmaInstalled(this)
if (needRuntime || needDesktop || needProton || needPlasma) installThenStart(needRuntime, needDesktop, needProton, needPlasma)
}
hud = PerfHud(this)
hud.onPresentingWindowChanged = {
@@ -562,16 +565,16 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
* dialog, download or client restart. Not being able to fetch it stops nothing: the session
* then asks the client for it after sign-in, as it always has.
*/
private fun installThenStart(runtime: Boolean, desktop: Boolean, proton: Boolean = false) {
private fun installThenStart(runtime: Boolean, desktop: Boolean, proton: Boolean = false, plasma: Boolean = false) {
if (SessionState.stopRequested) return
installingRuntime = true
SessionState.installing = if (runtime) "runtime" else if (desktop) "desktop" else "proton"
SessionState.installing = if (runtime) "runtime" else if (desktop || plasma) "desktop" else "proton"
SessionEvents.transition(
SessionPhase.INSTALLING_RUNTIME,
"${SessionState.installing}.installing",
mapOf("component" to SessionState.installing),
)
loading.step = if (runtime) "downloading the Linux runtime" else if (desktop) "downloading the desktop"
loading.step = if (runtime) "downloading the Linux runtime" else if (desktop || plasma) "downloading the desktop"
else "downloading Proton Experimental (ARM64)"
loading.percent = -1
Thread({
@@ -587,6 +590,14 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
problem = installDesktop()
if (problem == null) SessionEvents.record("desktop.ready") else failedComponent = "desktop"
}
// KDE Plasma over the desktop. Not having it stops nothing: the desktop starts on LXQt.
if (problem == null && plasma && !SessionState.stopRequested) {
SessionState.installing = "desktop"
SessionEvents.transition(SessionPhase.INSTALLING_RUNTIME, "plasma.installing", mapOf("component" to "plasma"))
val plasmaProblem = installPlasma()
if (plasmaProblem == null) SessionEvents.record("plasma.ready")
else SessionEvents.record("plasma.skipped", mapOf("reason" to plasmaProblem))
}
if (problem == null && proton && !SessionState.stopRequested) {
SessionState.installing = "proton"
SessionEvents.transition(SessionPhase.INSTALLING_RUNTIME, "proton.installing", mapOf("component" to "proton"))
@@ -660,6 +671,15 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
return problem?.let { "The desktop did not install ($it). Check the connection and press Desktop again." }
}
/** Null when KDE Plasma is in, else why not; the desktop then starts on LXQt. */
private fun installPlasma(): String? {
uiHandler.post { loading.percent = -1; loading.step = "downloading the desktop (KDE Plasma)" }
val catalog = com.droiddeck.launcher.runtime.DesktopCatalog
val entry = (catalog.fetch(catalog.SHELLS_URL) ?: return "catalog unreachable")
.firstOrNull { it.id == catalog.PLASMA_ID } ?: return "not published yet"
return catalog.install(this, entry, progressFor("the desktop (KDE Plasma)", entry.size / 1_000_000))
}
/** Null when the ARM64 Proton is in, else why not; the session goes on either way. */
private fun installProton(): String? {
uiHandler.post { loading.percent = -1; loading.step = "downloading Proton Experimental (ARM64)" }
@@ -52,7 +52,7 @@ object DeviceReport {
b.append("DroidDeck session report\n")
b.append("========================\n")
k("Written", SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzz", Locale.US).format(Date()))
k("Session mode", when (mode) { SessionService.MODE_DESKTOP -> "desktop (labwc/LXQt)"; SessionService.MODE_RUN -> "a program under gamescope"; else -> "Steam client (gamescope)" })
k("Session mode", when (mode) { SessionService.MODE_DESKTOP -> if (SessionPrefs.desktopShell(context) == SessionPrefs.SHELL_PLASMA) "desktop (KWin/KDE Plasma)" else "desktop (labwc/LXQt)"; SessionService.MODE_RUN -> "a program under gamescope"; else -> "Steam client (gamescope)" })
h("App")
runCatching {
@@ -133,6 +133,7 @@ object DeviceReport {
k("Installed version", LinuxRuntimeInstaller.installedVersion(context))
k("Runtime ready", LinuxRuntime.isInstalled(context))
k("Desktop installed", DesktopCatalog.desktopInstalled(context))
k("KDE Plasma installed", DesktopCatalog.plasmaInstalled(context))
k("Runtime root", LinuxRuntime.rootDir(context).path)
h("Settings in effect")
@@ -25,6 +25,12 @@ object DesktopCatalog {
*/
const val STEAM_SEED_URL = "https://raw.githubusercontent.com/The412Banner/winlator-contents/main/steam-seed.json"
const val PROTON_SEED_ID = "proton-arm64"
/**
* Desktop shells other than LXQt, built in this repo (tools/plasma, build-plasma.yml) and
* installed over the desktop package when one is chosen in the Desktop settings.
*/
const val SHELLS_URL = "https://raw.githubusercontent.com/Droid-Deck/DroidDeck/main/catalog/shells.json"
const val PLASMA_ID = "plasma"
class Entry(
val id: String, val name: String, val tier: Int, val version: String, val kind: String,
@@ -78,6 +84,10 @@ object DesktopCatalog {
fun desktopInstalled(context: Context): Boolean =
File(LinuxRuntime.rootDir(context), "usr/bin/labwc").isFile
/** KDE Plasma's package is in (droiddeck-desktop tests the same file). */
fun plasmaInstalled(context: Context): Boolean =
File(LinuxRuntime.rootDir(context), "usr/bin/kwin_wayland").isFile
/** Downloads, verifies and installs one package. Returns null on success, else a message. */
fun install(context: Context, entry: Entry, listener: LinuxRuntimeInstaller.ProgressListener?): String? {
val root = LinuxRuntime.rootDir(context)
@@ -65,6 +65,10 @@ object SessionFiles {
// Games and emulators from the menu, full screen in a gamescope of their own.
"usr/local/bin/droiddeck-gpu" to "usr/local/bin/droiddeck-gpu",
"usr/local/bin/droiddeck-desktop-gpu" to "usr/local/bin/droiddeck-desktop-gpu",
// KDE Plasma's launcher and the session it runs in KWin; used only when the plasma
// package is in and the Desktop settings pick it (droiddeck-desktop checks both).
"usr/local/bin/droiddeck-plasma" to "usr/local/bin/droiddeck-plasma",
"usr/local/bin/droiddeck-plasma-session" to "usr/local/bin/droiddeck-plasma-session",
"etc/xdg/labwc/autostart" to "etc/xdg/labwc/autostart",
"etc/xdg/labwc/rc.xml" to "etc/xdg/labwc/rc.xml",
"etc/xdg/lxqt/panel.conf" to "etc/xdg/lxqt/panel.conf",
@@ -419,6 +419,20 @@ object SessionPrefs {
java.io.File(com.droiddeck.launcher.runtime.LinuxRuntime.rootDir(context), "root/.droiddeck-renderer-failed").delete()
}
/**
* The desktop's shell: SHELL_LXQT (labwc, the desktop package) or SHELL_PLASMA (KWin, the plasma
* package over it, fetched at the next Desktop start when it is not in yet).
*/
fun desktopShell(context: Context): String =
prefs(context).getString("desktopShell", SHELL_LXQT)?.takeIf { it == SHELL_PLASMA } ?: SHELL_LXQT
fun setDesktopShell(context: Context, shell: String) {
prefs(context).edit().putString("desktopShell", shell).apply()
}
const val SHELL_LXQT = "lxqt"
const val SHELL_PLASMA = "plasma"
/**
* HDR10 output for MODE_STEAM / MODE_DESKTOP. Off by default. Honoured only when the panel
* lists HDR10 (HdrSupport), and decided when the compositor starts, which is once per app
@@ -302,6 +302,8 @@ class SessionService : Service() {
val override = File(Environment.getExternalStorageDirectory(), "Download/droiddeck-wlr-renderer")
.takeIf { it.isFile }?.let { FileUtils.readString(it)?.trim() }
guest.add("BL_WLR_RENDERER=" + (override?.takeIf { it.isNotEmpty() } ?: SessionPrefs.desktopRenderer(this)))
// LXQt on labwc or KDE Plasma on KWin; droiddeck-desktop falls back to LXQt without Plasma.
guest.add("BL_DESKTOP_SHELL=" + SessionPrefs.desktopShell(this))
}
// The user's own games, for the runtime's shortcuts writer to put in the client's library
// before the client starts (see frontend/AddedGames and bannerlator-steam-shortcuts).
@@ -61,6 +61,8 @@ class ModeSettings(
val clientDirectAudio: Boolean = false,
val mic: Boolean?,
val renderer: String?,
/** Desktop only: SessionPrefs.SHELL_LXQT or SHELL_PLASMA. */
val shell: String? = null,
val gameStorage: String? = null,
val storageOptions: List<Pair<String, String>> = emptyList(),
val fexPreset: String? = null,
@@ -118,6 +120,7 @@ class ModeSettingsActions(
val onClientDirectAudio: (Boolean) -> Unit = {},
val onMic: (Boolean) -> Unit,
val onRenderer: (String) -> Unit,
val onShell: (String) -> Unit = {},
val onGameStorage: (path: String, label: String) -> Unit = { _, _ -> },
val onPickGameStorageFolder: () -> Unit = {},
val onFexPreset: (String) -> Unit = {},
@@ -423,7 +426,17 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
}
}
}
if (!steam && s.renderer != null) SettingsGroup("Renderer") {
if (!steam && s.shell != null) SettingsGroup("Shell") {
ChoiceRow(
host, "shell", "Desktop shell", "Applies at the next start.",
listOf(SessionPrefs.SHELL_LXQT to "LXQt", SessionPrefs.SHELL_PLASMA to "KDE Plasma"), s.shell,
note = "KDE Plasma downloads the first time it starts and composites in software, so games " +
"and emulators from its menu open full screen on the GPU. If it cannot start, the desktop comes up on LXQt.",
onPick = a.onShell,
)
}
// labwc's renderer; KDE Plasma's KWin always composites in software here.
if (!steam && s.renderer != null && s.shell != SessionPrefs.SHELL_PLASMA) SettingsGroup("Renderer") {
ChoiceRow(
host, "renderer", "Desktop renderer", "Composites the desktop.",
listOf("vulkan" to "vulkan - GPU", "gles2" to "gles2 - GPU (experimental)", "pixman" to "pixman - software"), s.renderer,
+6
View File
@@ -0,0 +1,6 @@
{
"schemaVersion": 1,
"category": "droiddeck-shells",
"note": "Desktop shells the app installs over the desktop package when one is chosen in the Desktop settings. Built by .github/workflows/build-plasma.yml, whose run summary prints the entry to paste here.",
"packages": []
}
+2
View File
@@ -116,6 +116,8 @@ docker run --rm --platform linux/amd64 \
install -Dm644 tools/linuxfs/desktop/droiddeck-desktop "$d/usr/local/bin/droiddeck-desktop"
install -Dm644 tools/linuxfs/desktop/droiddeck-gpu "$d/usr/local/bin/droiddeck-gpu"
install -Dm644 tools/linuxfs/desktop/droiddeck-desktop-gpu "$d/usr/local/bin/droiddeck-desktop-gpu"
install -Dm644 tools/linuxfs/desktop/droiddeck-plasma "$d/usr/local/bin/droiddeck-plasma"
install -Dm644 tools/linuxfs/desktop/droiddeck-plasma-session "$d/usr/local/bin/droiddeck-plasma-session"
install -Dm644 tools/linuxfs/desktop/autostart "$d/etc/xdg/labwc/autostart"
install -Dm644 tools/linuxfs/desktop/rc.xml "$d/etc/xdg/labwc/rc.xml"
install -Dm644 tools/linuxfs/desktop/panel.conf "$d/etc/xdg/lxqt/panel.conf"
+11
View File
@@ -130,6 +130,17 @@ python3 /usr/local/bin/bannerlator-pad-defaults 2>&1 | sed 's/^/[pads] /'
gpu_entries() { python3 /usr/local/bin/droiddeck-desktop-gpu 2>&1 | sed 's/^/[gpu] /'; }
gpu_entries
# KDE Plasma picked in the Desktop settings (BL_DESKTOP_SHELL): KWin takes labwc's place. It
# composites in software, so the menu's entries go full screen on the GPU as they do on pixman.
if [ "${BL_DESKTOP_SHELL:-lxqt}" = plasma ]; then
if [ -x /usr/bin/kwin_wayland ] && [ -x /usr/local/bin/droiddeck-plasma ]; then
use_renderer pixman
gpu_entries
exec /usr/local/bin/droiddeck-plasma
fi
echo "== droiddeck-desktop: KDE Plasma is not installed; starting LXQt"
fi
# ~/.droiddeck-desktop-debug: labwc's own debug log (views and layer surfaces mapping) too. The GPU
# renderers are experimental and always log in full, so a desktop that does not come up says why.
DEBUG=; { [ -f "$HOME/.droiddeck-desktop-debug" ] || [ "$WLR_RENDERER" != pixman ]; } && DEBUG=-d
+81
View File
@@ -0,0 +1,81 @@
#!/bin/bash
# KDE Plasma as the desktop: KWin nested as a client of the app's compositor (its Wayland backend,
# chosen because WAYLAND_DISPLAY names the app's socket), with Plasma's session on it and Xwayland
# for the X11 programs. Started by droiddeck-desktop when the Desktop settings pick KDE Plasma and
# the plasma package is in; what droiddeck-desktop prepares (the log, fonts, the menu's game and
# emulator entries, controller defaults) is done by then. If KWin does not come up, the desktop
# starts on LXQt instead and the log says why.
# Staged by the app on every launch, like droiddeck-desktop.
set -u
export XDG_SESSION_TYPE=wayland XDG_CURRENT_DESKTOP=KDE XDG_SESSION_DESKTOP=KDE
export KDE_FULL_SESSION=true KDE_SESSION_VERSION=6
# Plasma's own platform theme (plasma-integration), not LXQt's.
unset QT_QPA_PLATFORMTHEME WLR_RENDERER WLR_BACKENDS
export QT_QPA_PLATFORM="wayland;xcb"
# KWin composites in software (QPainter): its OpenGL path allocates through gbm on a DRM render node,
# which the KGSL stand-in is not - the same wall stock wlroots hit. QPainter hands programs shm
# buffers only, so games and emulators from the menu open full screen on the GPU through
# droiddeck-gpu, as on LXQt's pixman. BL_KWIN_COMPOSE (O2, Q) overrides for experiments.
export KWIN_COMPOSE=${BL_KWIN_COMPOSE:-Q}
echo "== droiddeck-plasma: KWin ($KWIN_COMPOSE) nested on $WAYLAND_DISPLAY at ${BL_WIDTH:-1280}x${BL_HEIGHT:-720}"
# Settings Plasma reads at start, written every session: no splash, no systemd boot (there is no
# systemd user instance in proot), no screen locker (it needs PAM and a greeter), no wallet
# prompts, no file indexer eating the cores a game needs.
kset() { kwriteconfig6 --file "$1" --group "$2" --key "$3" "$4" 2>/dev/null; }
kset ksplashrc KSplash Engine none
kset ksplashrc KSplash Theme None
kset startkderc General systemdBoot false
kset kscreenlockerrc Daemon Autolock false
kset kscreenlockerrc Daemon LockOnResume false
kset kwalletrc Wallet Enabled false
kset baloofilerc "Basic Settings" "Indexing-Enabled" false
# The nested output is the app's surface: no scaling, the app already sized it.
kset kwinrc Xwayland Scale 1
SOCKET=wayland-1
rm -f "$XDG_RUNTIME_DIR/$SOCKET" "$XDG_RUNTIME_DIR/$SOCKET.lock"
run_kwin() {
local args=(--socket "$SOCKET" --width "${BL_WIDTH:-1280}" --height "${BL_HEIGHT:-720}" --scale 1
--xwayland --no-lockscreen --exit-with-session /usr/local/bin/droiddeck-plasma-session)
# A session bus: kded, plasmashell and the portal all live on it.
if command -v dbus-run-session >/dev/null 2>&1; then
dbus-run-session -- kwin_wayland "${args[@]}"
else
kwin_wayland "${args[@]}"
fi
}
kwin_answers() { WAYLAND_DISPLAY=$SOCKET timeout 3 wayland-info >/dev/null 2>&1; }
pids_named() {
local p
for p in /proc/[0-9]*; do
[ "$(cat "$p/comm" 2>/dev/null)" = "$1" ] && echo "${p#/proc/}"
done
}
fall_back() {
echo "== droiddeck-plasma: $1; starting LXQt instead"
for prog in plasmashell kded6 ksmserver kwin_wayland Xwayland; do kill -KILL $(pids_named "$prog") 2>/dev/null; done
find "$XDG_RUNTIME_DIR" -mindepth 1 -maxdepth 1 ! -name 'wayland-0*' ! -name 'keymap.xkb' -exec rm -rf {} + 2>/dev/null
BL_DESKTOP_SHELL=lxqt exec /usr/local/bin/droiddeck-desktop
}
started=$(date +%s)
run_kwin &
kwin_job=$!
answered=
while kill -0 "$kwin_job" 2>/dev/null; do
if kwin_answers; then answered=1; break; fi
[ $(( $(date +%s) - started )) -ge 30 ] && break
sleep 1
done
if [ -z "$answered" ]; then
if kill -0 "$kwin_job" 2>/dev/null; then
kill -TERM $(pids_named kwin_wayland) 2>/dev/null
sleep 2
fall_back "KWin did not answer in 30 s"
fi
wait "$kwin_job"
fall_back "KWin exited at start (exit $?)"
fi
echo "== droiddeck-plasma: KWin is up after $(( $(date +%s) - started )) s"
wait "$kwin_job"
@@ -0,0 +1,25 @@
#!/bin/bash
# What runs inside KWin for the Plasma desktop (droiddeck-plasma's --exit-with-session): the Steam
# client if the desktop was asked for with Steam in it, then Plasma's own session. KWin, and with it
# the desktop, ends when this does.
# The desktop was asked for with Steam in it (the front end's "Steam Desktop UI", Big Picture's
# "Switch to Desktop"): the client's desktop UI, as LXQt's autostart opens it.
if [ "${BL_DESKTOP_STEAM:-0}" = 1 ]; then
/usr/local/bin/bannerlator-steam-launch desktop 2>&1 | sed 's/^/[steam] /' &
fi
# The desktop's wallpaper once, on the first Plasma start; after that it is the user's to change.
if [ ! -f "$HOME/.config/.droiddeck-plasma-wallpaper" ] && [ -f /usr/share/backgrounds/steamdeck.png ]; then
( set -o pipefail; sleep 8
plasma-apply-wallpaperimage /usr/share/backgrounds/steamdeck.png 2>&1 | sed 's/^/[wallpaper] /' \
&& touch "$HOME/.config/.droiddeck-plasma-wallpaper" ) &
fi
for session in /usr/lib/startplasma-waylandsession /usr/libexec/startplasma-waylandsession; do
if [ -x "$session" ]; then
echo "== droiddeck-plasma: $session"
"$session" 2>&1 | sed 's/^/[plasma] /'
exit "${PIPESTATUS[0]}"
fi
done
echo "== droiddeck-plasma: no startplasma-waylandsession; plasmashell on its own"
plasmashell 2>&1 | sed 's/^/[plasmashell] /'
exit "${PIPESTATUS[0]}"
+50
View File
@@ -0,0 +1,50 @@
#!/bin/bash
# Builds the KDE Plasma shell package: the closure of seeds.txt over Arch Linux ARM, as a zstd
# tarball that extracts over the rootfs, with the desktop package (LXQt) already in it.
#
# The closure and extraction are winlator-contents' own desktop/build-pkg.sh, pinned below, so this
# package is made exactly like the desktop one. What it leaves out is the runtime's packages (as the
# desktop does) plus the desktop package's - except Qt and the KDE libraries, which are shipped
# again: KWin and plasmashell use Qt's private API and must match the Qt they were built against.
#
# tools/plasma/build.sh <out dir>
#
# Needs curl, git, tar, zstd, python3.
set -euo pipefail
out=${1:?out dir}
here=$(cd "$(dirname "$0")" && pwd)
. "$here/release.env"
work=$(mktemp -d)
mkdir -p "$out"
git init -q "$work/contents"
git -C "$work/contents" fetch -q --depth 1 https://github.com/The412Banner/winlator-contents.git "$CONTENTS_REF"
git -C "$work/contents" checkout -q FETCH_HEAD
curl -fsSL --retry 6 --retry-delay 5 --retry-all-errors -o "$work/desktop.packages.txt" "$DESKTOP_PACKAGES_URL"
# Package names from the desktop's file list (name-pkgver-pkgrel-arch.pkg.tar.xz); the ones Plasma
# must bring in its own build are not counted as present.
python3 - "$work/desktop.packages.txt" > "$work/desktop-names.txt" <<'PY'
import re, sys
keep = re.compile(r"^(qt6-|layer-shell-qt|kf6-|kwayland|plasma|breeze|kde|libplasma|kirigami|kpipewire|kscreenlocker|kglobalacc)")
for line in open(sys.argv[1]):
line = line.strip()
if not line: continue
name = line.rsplit("-", 3)[0]
if not keep.match(name): print(name)
PY
cat "$work/contents/desktop/base-packages.txt" "$work/desktop-names.txt" | sort -u > "$work/present.txt"
echo "== plasma: $(wc -l < "$work/present.txt") packages counted as present (runtime + desktop, less Qt/KDE)"
bash "$work/contents/desktop/build-pkg.sh" plasma "$here/seeds.txt" "$work/present.txt" "$out"
# What this package replaces from the desktop's: printed so a Qt bump is never a surprise.
python3 - "$work/desktop.packages.txt" "$out/plasma.packages.txt" <<'PY'
import sys
def names(p): return {l.strip().rsplit("-", 3)[0]: l.strip() for l in open(p) if l.strip()}
d, p = names(sys.argv[1]), names(sys.argv[2])
for n in sorted(set(d) & set(p)):
if d[n] != p[n]: print("== replaces", d[n], "->", p[n])
PY
rm -rf "$work"
+9
View File
@@ -0,0 +1,9 @@
# What the Plasma package is built on and tested against.
# winlator-contents at the commit whose desktop/build-pkg.sh and base-packages.txt are used.
CONTENTS_REF=843e9c7dcdf52260993ee9f23c33a2e17acb5e20
DESKTOP_PACKAGES_URL=https://github.com/The412Banner/winlator-contents/releases/download/steamdeck-desktop-r1/desktop.packages.txt
# The runtime and desktop package the smoke test installs it over (linuxfs.json, desktop.json).
RUNTIME_URL=https://github.com/The412Banner/winlator-contents/releases/download/linuxfs-r9/linuxfs.tar.zst
RUNTIME_SHA256=d3240c1531531615b0dd06b6ad56e92db9015a8641eb18c3a613835ae45992e7
DESKTOP_URL=https://github.com/The412Banner/winlator-contents/releases/download/steamdeck-desktop-r1/desktop.tar.zst
DESKTOP_SHA256=c2bb860b3b1688287451e2495be5db313ec07f8c8af501dfac2768a9f417bc86
+15
View File
@@ -0,0 +1,15 @@
# KDE Plasma as a second desktop shell: KWin nested as a client of the app's compositor, with
# plasmashell's panel and desktop on it. Installed over the desktop package (LXQt), which keeps
# Xwayland, Firefox, mpv and the launchers; this only adds the shell and its everyday apps.
# One package name per line; the closure over Arch Linux ARM is taken at build time.
kwin
plasma-workspace
plasma-desktop
plasma-pa
breeze
breeze-icons
kde-cli-tools
xdg-desktop-portal-kde
systemsettings
dolphin
konsole
+79
View File
@@ -0,0 +1,79 @@
#!/bin/bash
# Installs the Plasma package the way the app does - runtime, then the desktop package, then this
# one, each extracted over the last - and checks that its programs load in that rootfs: every
# library they link resolves (with the runtime's glibc), and KWin and plasmashell start far enough
# to print their versions. Runs on an arm64 host; needs sudo for chroot.
#
# tools/plasma/smoke-test.sh <plasma.tar.zst>
set -euo pipefail
# On Actions, failures also go out as annotations, which can be read back without the log.
annotate() { [ -n "${GITHUB_ACTIONS:-}" ] && echo "::error title=plasma smoke test::$*"; echo "$*"; }
notice() { [ -n "${GITHUB_ACTIONS:-}" ] && echo "::notice title=plasma smoke test::$*"; echo "$*"; }
trap 'annotate "stopped at line $LINENO: $BASH_COMMAND"' ERR
pkg=$(realpath "${1:?plasma.tar.zst}")
here=$(cd "$(dirname "$0")" && pwd)
. "$here/release.env"
work=$(mktemp -d)
root=$work/root
mkdir -p "$root"
fetch() { # url sha256 out
curl -fsSL --retry 6 --retry-delay 5 --retry-all-errors -o "$3" "$1"
echo "$2 $3" | sha256sum -c -
}
fetch "$RUNTIME_URL" "$RUNTIME_SHA256" "$work/runtime.tar.zst"
fetch "$DESKTOP_URL" "$DESKTOP_SHA256" "$work/desktop.tar.zst"
for t in "$work/runtime.tar.zst" "$work/desktop.tar.zst" "$pkg"; do
sudo tar --zstd -xf "$t" -C "$root"
done
rm -f "$work/runtime.tar.zst" "$work/desktop.tar.zst"
fail=0
for bin in usr/bin/kwin_wayland usr/bin/plasmashell usr/bin/dolphin usr/bin/konsole usr/bin/systemsettings; do
[ -e "$root/$bin" ] || { annotate "MISSING /$bin"; fail=1; }
done
ls "$root"/usr/lib/startplasma-waylandsession "$root"/usr/lib*/libexec/startplasma-waylandsession 2>/dev/null \
|| echo "note: no startplasma-waylandsession; the launcher starts plasmashell itself"
# Every ELF the package added, checked with the rootfs's own loader.
tar --zstd -tf "$pkg" | sed -n 's#^\./##; /^usr\/\(bin\|lib\)\/.*[^/]$/p' > "$work/files.txt"
: > "$work/unresolved.txt"
while read -r f; do
[ -f "$root/$f" ] && [ ! -L "$root/$f" ] || continue
head -c 4 "$root/$f" | grep -q $'\x7fELF' || continue
sudo chroot "$root" /usr/bin/ldd "/$f" 2>&1 | grep -E "not found" | sed "s#^#/$f: #" >> "$work/unresolved.txt" || true
done < "$work/files.txt"
# What the desktop starts must resolve completely (ldd follows every library they load). Other
# helpers and plugins that need an optional dependency the package does not bring (Python
# bindings, PackageKit, input methods...) only fail to load, as on any Arch install without those
# extras; they are counted, not fatal.
core='^/usr/(bin/(kwin_wayland|kwin_wayland_wrapper|plasmashell|ksmserver|kded6|krunner|dolphin|konsole|systemsettings|plasma_session)|lib/(startplasma-waylandsession|xdg-desktop-portal-kde|kactivitymanagerd|polkit-kde-authentication-agent-1|DiscoverNotifier)):'
if [ -s "$work/unresolved.txt" ]; then
echo "== unresolved libraries or symbol versions:"
sort -u "$work/unresolved.txt" | head -80
if grep -qE "$core" "$work/unresolved.txt"; then
annotate "unresolved in the desktop's own programs: $(grep -E "$core" "$work/unresolved.txt" | sort -u | head -12 | tr -s ' \t' ' ' | tr '\n' ';')"
fail=1
fi
notice "optional helpers and plugins missing a dependency: $(grep -vE "$core" "$work/unresolved.txt" | cut -d: -f1 | sort -u | wc -l)"
else
echo "== every library in the package resolves in the runtime"
fi
# KWin must start far enough to answer. plasmashell sets up its scene before it reads its arguments
# and cannot without a compositor, so it is held to its libraries (above) and its answer only noted.
# A fresh proc (never a bind of the host's /dev or /proc: the rootfs is deleted at the end).
sudo mount -t proc proc "$root/proc"
sudo mkdir -p "$root/tmp/xdg" && sudo chmod 700 "$root/tmp/xdg"
for prog in kwin_wayland plasmashell; do
rc=0
out=$(sudo chroot "$root" /usr/bin/env -i PATH=/usr/bin HOME=/root XDG_RUNTIME_DIR=/tmp/xdg QT_QPA_PLATFORM=offscreen "/usr/bin/$prog" --version 2>&1) || rc=$?
echo "$prog --version (exit $rc): $out"
if [ "$rc" -ne 0 ]; then
if [ "$prog" = kwin_wayland ]; then annotate "FAILED: $prog --version exit $rc: $(echo "$out" | tail -3 | tr '\n' ' ')"; fail=1
else notice "$prog --version exit $rc: $(echo "$out" | tail -3 | tr '\n' ' ')"; fi
fi
done
sudo umount "$root/proc"
mountpoint -q "$root/proc" || sudo rm -rf "$work"
exit $fail