diff --git a/docs/index.md b/docs/index.md
index 40ec6f79..ce96dc38 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -110,6 +110,10 @@ features:
Allium
experimental
+
diff --git a/docs/linux-support/cfg-patching.md b/docs/linux-support/cfg-patching.md
index 4d4f1f8b..32611748 100644
--- a/docs/linux-support/cfg-patching.md
+++ b/docs/linux-support/cfg-patching.md
@@ -64,6 +64,12 @@ Allium launches stock RetroArch directly, so the usual config is the one that ge
Allium has no RetroAchievements settings of its own, so nothing rewrites these keys behind the proxy: RetroArch's own achievements login is the single source of your account details.
+== dArkOS
+
+```text
+/home/ark/.config/retroarch/retroarch.cfg
+```
+
:::
## What Gets Patched
diff --git a/docs/linux-support/index.md b/docs/linux-support/index.md
index 1b606bc3..ab4932bb 100644
--- a/docs/linux-support/index.md
+++ b/docs/linux-support/index.md
@@ -11,7 +11,7 @@ The Linux version is for handheld Linux devices where you want the same basic of
- Keep earning casual achievements while offline
- Let queued awards sync later when you reconnect
-Use the `KNULLI`, `Onion`, `muOS`, `ROCKNIX`, `spruce`, and `Allium` tabs throughout the Linux section to switch target-specific instructions.
+Use the `KNULLI`, `Onion`, `muOS`, `ROCKNIX`, `spruce`, `Allium`, and `dArkOS` tabs throughout the Linux section to switch target-specific instructions.
## Supported Targets
@@ -21,6 +21,7 @@ Use the `KNULLI`, `Onion`, `muOS`, `ROCKNIX`, `spruce`, and `Allium` tabs throug
- ROCKNIX
- spruce (experimental)
- Allium (experimental)
+- dArkOS (community-contributed, unverified by the developer)
## Specifics
@@ -62,10 +63,20 @@ Allium support is experimental. It is currently compatible with [Allium v1.0.1](
Allium is a Miyoo-only launcher, so this target covers exactly the devices Allium itself runs on: the Miyoo Mini, Mini Plus and Mini Flip.
+== dArkOS
+
+Community-contributed target for [dArkOS](https://github.com/christianhaitian/dArkOS) ("Debian based ArkOS"). Not yet verified against real hardware by the developer — please report issues.
+
+Current rough edges:
+
+- dArkOS is systemd-native (no `custom.sh`-style startup hook), so autostart installs a systemd unit instead — this uses passwordless `sudo` for the device user (dArkOS's own Tools scripts rely on the same mechanism), and is silently skipped with an on-screen hint if that's not available
+- `pygame` comes from `apt` rather than a bundled runtime; the installer installs it automatically via `sudo`, otherwise install it manually if the menu fails to launch
+- No known equivalent of `batocera.conf`/`knulli.conf`, so only the RetroArch-side patch applies
+
:::
## Important Notes
- Linux support is currently in alpha and should still be treated as a prerelease feature.
- Linux install, startup, and UI behavior can vary a lot by firmware and frontend.
-- Always check the correct tab for your device instead of assuming KNULLI, Onion, muOS, ROCKNIX, and spruce behave the same way.
+- Always check the correct tab for your device instead of assuming KNULLI, Onion, muOS, ROCKNIX, spruce, Allium, and dArkOS behave the same way.
diff --git a/docs/linux-support/installation.md b/docs/linux-support/installation.md
index 4849ba6e..27859707 100644
--- a/docs/linux-support/installation.md
+++ b/docs/linux-support/installation.md
@@ -46,6 +46,11 @@ Before using `RAOfflineProxy` on Linux:
- Make sure the system clock is correct so award timestamps are accurate
> Settings -> Wi-Fi -> enable NTP
+== dArkOS
+
+- Enter your RetroAchievements account details in RetroArch
+- The installer installs `python3-pygame` via `apt` automatically using passwordless `sudo`; if that's not available on your setup, install it manually: `sudo apt install -y python3-pygame`
+
:::
::: warning Emulator configs are patched automatically
@@ -168,6 +173,24 @@ RAOfflineProxy patches the emulator configs it needs in order to redirect RetroA
4. Start the proxy while online
5. Launch a game once so its data is cached
+== dArkOS
+
+> dArkOS support is community-contributed and not yet verified by the developer on real hardware.
+
+1. Download the latest `RAOfflineProxy-DarkOS-*-Install.sh` from [GitHub Releases](https://github.com/misantronic/RAOfflineProxy/releases)
+2. Copy it into:
+
+```text
+/roms/tools
+```
+
+3. Refresh or update gamelists so **RAOfflineProxy Install** appears in the **Tools** menu
+4. Launch **RAOfflineProxy Install** from **Tools** — no SSH or root needed; it uses passwordless `sudo` for the few steps that need it (same as dArkOS's own Tools scripts)
+5. Refresh or update gamelists again so the main **RAOfflineProxy** entry appears
+6. Launch **RAOfflineProxy** from the **Tools** menu
+7. Start the proxy while online
+8. Cache games either from **Cached Games** then **Add ROM** or by launching them once in RetroArch
+
:::
See [Caching Games](/linux-support/caching-games) for more detail.
diff --git a/docs/linux-support/pending-awards.md b/docs/linux-support/pending-awards.md
index 2fa08189..0eeb9a8b 100644
--- a/docs/linux-support/pending-awards.md
+++ b/docs/linux-support/pending-awards.md
@@ -11,7 +11,7 @@ From the user side, this means offline unlocks still register locally right away
Queued awards are sent oldest first. Duplicate pending awards are discarded. Authentication failures require refreshed login state. Network failures keep awards queued for later retry.
-On KNULLI and muOS, pending awards can be reviewed directly from the on-device menu. Onion does not currently include a dedicated pending-awards browser yet.
+On KNULLI, muOS, and dArkOS, pending awards can be reviewed directly from the on-device menu. Onion does not currently include a dedicated pending-awards browser yet.
## Anti-Tamper
diff --git a/docs/linux-support/settings.md b/docs/linux-support/settings.md
index 6bd92d29..5aa6ec16 100644
--- a/docs/linux-support/settings.md
+++ b/docs/linux-support/settings.md
@@ -104,6 +104,18 @@ It is also skipped entirely while an Allium firmware update is pending, so the p
An Allium update rewrites `/.tmp_update` and removes the block. The app reinstalls it every time you open RAOfflineProxy, so autostart repairs itself after an Allium update.
:::
+== dArkOS
+
+dArkOS is systemd-native and has no `custom.sh`-style startup hook, so autostart works differently than on the other targets.
+
+If you enable it, RAOfflineProxy installs (and permanently enables) a systemd unit:
+
+```text
+/etc/systemd/system/raofflineproxy-autostart.service
+```
+
+That unit always runs the proxy's boot-reconcile step on boot; enabling/disabling autostart from the menu just flips a config flag that boot-reconcile checks, so toggling it doesn't need `sudo` once the unit is installed. Installing the unit itself is done automatically during install using passwordless `sudo` for the device user (the same mechanism dArkOS's own Tools scripts use). If that's not available on your setup, the installer prints an on-screen message instead of failing.
+
:::
## Start / Stop Behavior
diff --git a/linux/darkos/README.md b/linux/darkos/README.md
new file mode 100644
index 00000000..d91365a1
--- /dev/null
+++ b/linux/darkos/README.md
@@ -0,0 +1,38 @@
+# dArkOS Bundle
+
+Portable bundle for [dArkOS](https://github.com/christianhaitian/dArkOS) ("Debian based ArkOS").
+
+## Status
+
+Community-contributed: a dArkOS user got RAOfflineProxy running by hand-patching the KNULLI bundle's install/uninstall/launcher scripts (`/userdata/roms` → `/roms`, `/userdata/system` → `/home/ark`). This directory formalizes that into a proper installer, and the shared `raofflineproxy/` Python source now recognizes `/home/ark` as an alternate platform root. **Not yet verified against real hardware by the maintainer** — please report issues.
+
+## Differences from the KNULLI bundle
+
+- Install root: `/home/ark/raofflineproxy` (KNULLI: `/userdata/system/raofflineproxy`)
+- ROMs root: `/roms` (KNULLI: `/userdata/roms`); Tools entries live under `/roms/tools`, bind-mounted to `/opt/system/Tools`
+- dArkOS is systemd-native and has no `custom.sh`-style boot hook, so autostart installs a systemd unit at `/etc/systemd/system/raofflineproxy-autostart.service` instead. **ES Tools-menu scripts run unprivileged on dArkOS (confirmed: they never run as root, even on handhelds)**, so this — and the `python3-pygame` install below — go through `sudo -n` (non-interactive `sudo`), relying on passwordless sudo for the device user. That's the same mechanism dArkOS's own Tools scripts use (e.g. `Enable Remote Services.sh` calls `sudo systemctl ...` non-interactively from the same context). `-n` makes sudo fail fast instead of hanging if that assumption is ever wrong, in which case the installer prints an on-screen message and continues; nothing else is affected (proxy start/stop, caching, awards, uninstall never needed root to begin with).
+- dArkOS ships `apt`, so pygame comes from the system package (`python3-pygame`) rather than a vendored runtime like the KNULLI/muOS bundles use. `install.sh` installs it automatically via `sudo -n apt install`; if that fails, it prints `sudo apt install -y python3-pygame` and continues. (`apt-get` doesn't work reliably on dArkOS — confirmed by community testing — so the script uses `apt` specifically.)
+- No `batocera.conf`/`knulli.conf` equivalent was found in dArkOS, so only the RetroArch-side RA host patch applies; there's no known ES-side achievements-screen override to patch yet.
+- In-app "Install Update" now fetches the `*DarkOS*.sh` release asset instead of the KNULLI one.
+
+## Build
+
+From repo root:
+
+```bash
+./linux/darkos/build_bundle.sh
+```
+
+This creates `linux/darkos/dist/RAOfflineProxy-DarkOS-v-Install.sh`.
+
+## Install
+
+Copy the installer to `/roms/tools` on the device and run it from the EmulationStation Tools menu — no SSH or root session needed. It can also be run directly:
+
+```bash
+bash "RAOfflineProxy-DarkOS-v-Install.sh"
+```
+
+## Uninstall
+
+Use `Uninstall` inside the RAOfflineProxy menu, or run `/home/ark/raofflineproxy/bin/raofflineproxy-uninstall` directly.
diff --git a/linux/darkos/build_bundle.sh b/linux/darkos/build_bundle.sh
new file mode 100755
index 00000000..e4f66146
--- /dev/null
+++ b/linux/darkos/build_bundle.sh
@@ -0,0 +1,88 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+LINUX_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
+DIST_DIR="${SCRIPT_DIR}/dist"
+BUILD_DIR="${DIST_DIR}/raofflineproxy-darkos-bundle"
+APP_DIR="${BUILD_DIR}/app"
+LIB_DIR="${BUILD_DIR}/lib"
+INSTALLER_PATH="${DIST_DIR}/RAOfflineProxy-DarkOS-v1.12.0-alpha1-Install.sh"
+TEMP_TARBALL="${DIST_DIR}/.raofflineproxy-darkos-bundle.tar.gz"
+
+TARGET="aarch64-linux-gnu.2.17" OUT_DIR="${SCRIPT_DIR}/native" \
+ "${LINUX_DIR}/build_rchash.sh"
+
+rm -rf "${BUILD_DIR}"
+rm -f "${DIST_DIR}/raofflineproxy-darkos-bundle.tar.gz"
+mkdir -p "${APP_DIR}"
+mkdir -p "${LIB_DIR}"
+
+export COPYFILE_DISABLE=1
+
+cp -r "${LINUX_DIR}/raofflineproxy" "${APP_DIR}/raofflineproxy"
+cp "${LINUX_DIR}/requirements.txt" "${APP_DIR}/requirements.txt"
+cp "${LINUX_DIR}/../docs/public/logo-320.png" "${APP_DIR}/raofflineproxy/logo-320.png"
+cp -r "${SCRIPT_DIR}/scripts" "${BUILD_DIR}/scripts"
+cp "${SCRIPT_DIR}/native/libraproxy_rchash.so" "${LIB_DIR}/libraproxy_rchash.so"
+cp "${SCRIPT_DIR}/scripts/install.sh" "${BUILD_DIR}/install.sh"
+cp "${SCRIPT_DIR}/scripts/uninstall.sh" "${BUILD_DIR}/uninstall.sh"
+
+find "${APP_DIR}" -name "__pycache__" -type d -prune -exec rm -rf {} +
+find "${APP_DIR}" -name "*.pyc" -delete
+
+chmod +x "${BUILD_DIR}/install.sh"
+chmod +x "${BUILD_DIR}/uninstall.sh"
+chmod +x "${BUILD_DIR}/scripts/launcher-raofflineproxy"
+chmod +x "${BUILD_DIR}/scripts/launcher-raofflineproxy-uninstall"
+
+mkdir -p "${DIST_DIR}"
+rm -f "${TEMP_TARBALL}"
+tar -czf "${TEMP_TARBALL}" -C "${DIST_DIR}" "raofflineproxy-darkos-bundle"
+
+cat > "${INSTALLER_PATH}" <<'EOF'
+#!/usr/bin/env bash
+set -euo pipefail
+
+SCRIPT_PATH="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
+PAYLOAD_MARKER="__RAOFFLINEPROXY_PAYLOAD_BELOW__"
+TARGET_DIR="/home/ark/raofflineproxy-darkos-bundle"
+TOOLS_INSTALL_SCRIPT="/roms/tools/RAOfflineProxy Install.sh"
+
+marker_line="$(awk -v marker="${PAYLOAD_MARKER}" '$0 == marker { print NR; exit }' "${SCRIPT_PATH}")"
+if [ -z "${marker_line}" ]; then
+ echo "Installer payload marker not found."
+ exit 1
+fi
+
+payload_line=$((marker_line + 1))
+rm -rf "${TARGET_DIR}"
+mkdir -p "/home/ark"
+
+PAYLOAD_TARBALL="$(mktemp "/home/ark/.raofflineproxy-payload.XXXXXX")"
+trap 'rm -f "${PAYLOAD_TARBALL}"' EXIT
+tail -n +"${payload_line}" "${SCRIPT_PATH}" | base64 -d > "${PAYLOAD_TARBALL}"
+tar -xzf "${PAYLOAD_TARBALL}" -C "/home/ark" --no-same-owner
+
+SENTINEL="${TARGET_DIR}/app/raofflineproxy/main.py"
+if [ ! -f "${SENTINEL}" ]; then
+ echo "Installer payload did not extract correctly (missing ${SENTINEL})."
+ echo "Your device's base64/tar may have truncated the payload."
+ exit 1
+fi
+
+cd "${TARGET_DIR}"
+./install.sh
+rm -f "${TOOLS_INSTALL_SCRIPT}" "${SCRIPT_PATH}"
+echo "RAOfflineProxy installed."
+exit 0
+__RAOFFLINEPROXY_PAYLOAD_BELOW__
+EOF
+
+base64 < "${TEMP_TARBALL}" | fold -w 76 >> "${INSTALLER_PATH}"
+chmod +x "${INSTALLER_PATH}"
+
+rm -f "${TEMP_TARBALL}"
+rm -rf "${BUILD_DIR}"
+
+echo "Created ${INSTALLER_PATH}"
diff --git a/linux/darkos/scripts/install.sh b/linux/darkos/scripts/install.sh
new file mode 100755
index 00000000..66cc0904
--- /dev/null
+++ b/linux/darkos/scripts/install.sh
@@ -0,0 +1,113 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+BASE_DIR="/home/ark/raofflineproxy"
+APP_DIR="${BASE_DIR}/app"
+BIN_DIR="${BASE_DIR}/bin"
+LIB_DIR="${BASE_DIR}/lib"
+TOOLS_DIR="/roms/tools"
+INSTALL_SCRIPT="${TOOLS_DIR}/RAOfflineProxy Install.sh"
+OLD_BIN="${BASE_DIR}/bin/raofflineproxy"
+UPDATE_STATUS_FILE="/home/ark/.config/raofflineproxy/update_status.json"
+AUTOSTART_UNIT="/etc/systemd/system/raofflineproxy-autostart.service"
+FB_WIDTH=0
+FB_HEIGHT=0
+WAS_RUNNING=0
+RESTARTED=0
+
+if [ -r /sys/class/graphics/fb0/virtual_size ]; then
+ IFS=, read -r FB_WIDTH FB_HEIGHT < /sys/class/graphics/fb0/virtual_size || true
+fi
+
+mkdir -p "${APP_DIR}"
+mkdir -p "${BIN_DIR}"
+mkdir -p "${LIB_DIR}"
+mkdir -p "${TOOLS_DIR}"
+
+if [ -x "${OLD_BIN}" ]; then
+ if status_output="$(${OLD_BIN} status 2>/dev/null)" && printf '%s' "${status_output}" | grep -q 'running: yes'; then
+ WAS_RUNNING=1
+ fi
+ ${OLD_BIN} stop-proxy >/dev/null 2>&1 || true
+fi
+
+cp -r "${SCRIPT_DIR}/app/"* "${APP_DIR}/"
+cp -r "${SCRIPT_DIR}/lib/"* "${LIB_DIR}/"
+
+cp "${SCRIPT_DIR}/scripts/launcher-raofflineproxy" "${BIN_DIR}/raofflineproxy"
+cp "${SCRIPT_DIR}/scripts/launcher-raofflineproxy-uninstall" "${BIN_DIR}/raofflineproxy-uninstall"
+
+rm -f "${UPDATE_STATUS_FILE}"
+
+chmod +x "${BIN_DIR}/raofflineproxy"
+chmod +x "${BIN_DIR}/raofflineproxy-uninstall"
+
+# ES Tools-menu scripts run unprivileged on dArkOS, so we rely on
+# passwordless sudo for the device user (the same mechanism dArkOS's own
+# Tools scripts use, e.g. "Enable Remote Services.sh" calling
+# `sudo systemctl ...` non-interactively). `sudo -n` fails fast instead of
+# hanging on a password prompt if that assumption doesn't hold here.
+if ! /usr/bin/python3 -c "import pygame" >/dev/null 2>&1; then
+ if command -v apt >/dev/null 2>&1 && sudo -n apt install -y python3-pygame >/dev/null 2>&1; then
+ echo "python3-pygame installed."
+ else
+ echo "pygame is not installed for python3 -- the RAOfflineProxy menu needs it."
+ echo "Run: sudo apt install -y python3-pygame"
+ fi
+fi
+
+# Installs and enables the systemd boot-reconcile unit once; toggling
+# autostart afterwards only flips a config flag (no sudo needed at runtime).
+"${BIN_DIR}/raofflineproxy" ensure-boot-hook >/dev/null 2>&1 || true
+if [ -f "${AUTOSTART_UNIT}" ]; then
+ echo "Autostart unit installed (${AUTOSTART_UNIT})."
+else
+ echo "Could not install the autostart unit -- needs passwordless sudo for $(whoami)."
+ echo "Enable autostart from the menu once sudo access is available."
+fi
+
+if [ "${WAS_RUNNING}" -eq 1 ]; then
+ if "${BIN_DIR}/raofflineproxy" start-proxy >/dev/null 2>&1; then
+ RESTARTED=1
+ fi
+fi
+
+cat > "${TOOLS_DIR}/RAOfflineProxy.sh" <<'EOF'
+#!/bin/sh
+exec /home/ark/raofflineproxy/bin/raofflineproxy menu
+EOF
+chmod +x "${TOOLS_DIR}/RAOfflineProxy.sh"
+
+rm -f "${INSTALL_SCRIPT}"
+
+cat > "${BASE_DIR}/ui-state.txt" <<'EOF'
+RAOfflineProxy Install
+
+dArkOS bundle installed.
+Please Update Gamelists.
+EOF
+
+if [ "${RESTARTED}" -eq 1 ]; then
+ cat >> "${BASE_DIR}/ui-state.txt" <<'EOF'
+
+Proxy restarted to apply update.
+EOF
+fi
+
+"${BIN_DIR}/raofflineproxy" text-image --output "${BASE_DIR}/ui-state.bmp" --text "$(cat "${BASE_DIR}/ui-state.txt")" --image-width "${FB_WIDTH}" --image-height "${FB_HEIGHT}" --font-scale 2 >/dev/null 2>&1 || true
+
+if command -v fbv >/dev/null 2>&1; then
+ /bin/sh -c '
+ fbv -i -c -u "$1" >/dev/null 2>&1 &
+ viewer_pid=$!
+ sleep 6
+ kill "$viewer_pid" >/dev/null 2>&1 || true
+ ' sh "${BASE_DIR}/ui-state.bmp" >/dev/null 2>&1 || true
+fi
+
+echo "RAOfflineProxy dArkOS bundle installed."
+if [ "${RESTARTED}" -eq 1 ]; then
+ echo "Proxy restarted to apply update."
+fi
+echo "Please Update Gamelists."
diff --git a/linux/darkos/scripts/launcher-raofflineproxy b/linux/darkos/scripts/launcher-raofflineproxy
new file mode 100755
index 00000000..7cedeb33
--- /dev/null
+++ b/linux/darkos/scripts/launcher-raofflineproxy
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+BASE_DIR="/home/ark/raofflineproxy"
+PYTHONPATH_ENTRIES="${BASE_DIR}/app"
+
+export HOME="/home/ark"
+export XDG_CONFIG_HOME="/home/ark/.config"
+export PATH="/usr/local/bin:/usr/bin:/bin:/home/ark/raofflineproxy/bin:${PATH:-}"
+export LD_LIBRARY_PATH="${BASE_DIR}/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
+
+export PYTHONPATH="${PYTHONPATH_ENTRIES}${PYTHONPATH:+:${PYTHONPATH}}"
+
+exec /usr/bin/python3 -m raofflineproxy.main "$@"
diff --git a/linux/darkos/scripts/launcher-raofflineproxy-uninstall b/linux/darkos/scripts/launcher-raofflineproxy-uninstall
new file mode 100755
index 00000000..ff1a3f60
--- /dev/null
+++ b/linux/darkos/scripts/launcher-raofflineproxy-uninstall
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+exec /home/ark/raofflineproxy-darkos-bundle/uninstall.sh
diff --git a/linux/darkos/scripts/uninstall.sh b/linux/darkos/scripts/uninstall.sh
new file mode 100755
index 00000000..14134a1d
--- /dev/null
+++ b/linux/darkos/scripts/uninstall.sh
@@ -0,0 +1,61 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+BASE_DIR="/home/ark/raofflineproxy"
+TOOLS_DIR="/roms/tools"
+BUNDLE_DIR="/home/ark/raofflineproxy-darkos-bundle"
+CONFIG_DIR="/home/ark/.config/raofflineproxy"
+AUTOSTART_UNIT="/etc/systemd/system/raofflineproxy-autostart.service"
+FB_WIDTH=0
+FB_HEIGHT=0
+
+if [ -r /sys/class/graphics/fb0/virtual_size ]; then
+ IFS=, read -r FB_WIDTH FB_HEIGHT < /sys/class/graphics/fb0/virtual_size || true
+fi
+
+mkdir -p "${BASE_DIR}"
+
+if [ -x "${BASE_DIR}/bin/raofflineproxy" ]; then
+ "${BASE_DIR}/bin/raofflineproxy" stop-proxy || true
+ # remove-boot-hook uses passwordless sudo internally (same as install.sh);
+ # falls back to a plain attempt here in case that binary is already gone.
+ "${BASE_DIR}/bin/raofflineproxy" remove-boot-hook || true
+fi
+
+sudo -n systemctl disable --now raofflineproxy-autostart.service >/dev/null 2>&1 || true
+sudo -n rm -f "${AUTOSTART_UNIT}" >/dev/null 2>&1 || rm -f "${AUTOSTART_UNIT}" >/dev/null 2>&1 || true
+sudo -n systemctl daemon-reload >/dev/null 2>&1 || true
+
+if command -v pkill >/dev/null 2>&1; then
+ pkill -f 'raofflineproxy.main' >/dev/null 2>&1 || true
+fi
+
+cat > "${BASE_DIR}/ui-state.txt" <<'EOF'
+RAOfflineProxy Uninstall
+
+dArkOS bundle removed.
+EOF
+
+if [ -x "${BASE_DIR}/bin/raofflineproxy" ]; then
+ "${BASE_DIR}/bin/raofflineproxy" text-image --output "${BASE_DIR}/ui-state.bmp" --text "$(cat "${BASE_DIR}/ui-state.txt")" --image-width "${FB_WIDTH}" --image-height "${FB_HEIGHT}" --font-scale 2 >/dev/null 2>&1 || true
+
+ if command -v fbv >/dev/null 2>&1; then
+ /bin/sh -c '
+ fbv -i -c -u "$1" >/dev/null 2>&1 &
+ viewer_pid=$!
+ sleep 6
+ kill "$viewer_pid" >/dev/null 2>&1 || true
+ ' sh "${BASE_DIR}/ui-state.bmp" >/dev/null 2>&1 || true
+ fi
+fi
+
+rm -f "${TOOLS_DIR}/RAOfflineProxy.sh"
+
+nohup /bin/sh -c '
+ sleep 2
+ rm -rf "$1"
+ rm -rf "$2"
+ rm -rf "$3"
+' sh "${CONFIG_DIR}" "${BASE_DIR}" "${BUNDLE_DIR}" >/dev/null 2>&1 &
+
+echo "RAOfflineProxy dArkOS bundle removed."
diff --git a/linux/raofflineproxy/config.py b/linux/raofflineproxy/config.py
index 5e29e0c4..097e8ab3 100644
--- a/linux/raofflineproxy/config.py
+++ b/linux/raofflineproxy/config.py
@@ -57,6 +57,7 @@
# the live values into this file instead. It is the only place those credentials exist.
DEFAULT_ROCKNIX_APPEND_CFG = Path("/tmp/.retroarch.cfg")
OS_RELEASE_PATH = Path("/etc/os-release")
+DEFAULT_DARKOS_HOME = Path("/home/ark")
def running_on_rocknix() -> bool:
@@ -182,6 +183,9 @@ def resolve_config_dir() -> Path:
if DEFAULT_MUOS_APPLICATION_DIR.exists():
return DEFAULT_MUOS_APPLICATION_DIR / "data"
+ if DEFAULT_DARKOS_HOME.exists():
+ return DEFAULT_DARKOS_HOME / ".config" / "raofflineproxy"
+
if Path("/userdata/system").exists():
return Path("/userdata/system/.config/raofflineproxy")
diff --git a/linux/raofflineproxy/menu_sdl.py b/linux/raofflineproxy/menu_sdl.py
index 8250d364..c10ecbf5 100644
--- a/linux/raofflineproxy/menu_sdl.py
+++ b/linux/raofflineproxy/menu_sdl.py
@@ -32,6 +32,7 @@
APP_VERSION,
CONFIG_DIR,
DEFAULT_ALLIUM_APP_DIR,
+ DEFAULT_DARKOS_HOME,
DEFAULT_ONION_APP_DIR,
load_config,
running_on_allium,
@@ -788,7 +789,11 @@ def labels(self, running: bool) -> list[str]:
if self.main_autostart_enabled
else "Enable autostart"
)
- if (self.is_knulli_platform() and not service_mode) or running_on_muos() or running_on_rocknix():
+ if (
+ ((self.is_knulli_platform() or self.is_darkos_platform()) and not service_mode)
+ or running_on_muos()
+ or running_on_rocknix()
+ ):
labels.append("Uninstall")
labels.append("Support me")
labels.append("Send Logs")
@@ -1382,6 +1387,9 @@ def clear_cache_and_return(self) -> None:
def is_knulli_platform(self) -> bool:
return Path("/userdata/system").exists()
+ def is_darkos_platform(self) -> bool:
+ return DEFAULT_DARKOS_HOME.exists()
+
def update_platform(self) -> str | None:
"""The release channel to check for updates, or None where there is none.
@@ -1399,6 +1407,8 @@ def update_platform(self) -> str | None:
return "allium"
if running_on_rocknix():
return "rocknix"
+ if self.is_darkos_platform():
+ return "darkos"
return "knulli"
def calibration_complete(self) -> bool:
@@ -2871,6 +2881,8 @@ def install_update_muos(self) -> None:
def uninstall(self) -> None:
if running_on_muos():
launcher = "/run/muos/storage/application/RAOfflineProxy/uninstall.sh"
+ elif self.is_darkos_platform():
+ launcher = "/home/ark/raofflineproxy/bin/raofflineproxy-uninstall"
elif self.is_knulli_platform():
launcher = "/userdata/system/raofflineproxy/bin/raofflineproxy-uninstall"
elif running_on_rocknix():
diff --git a/linux/raofflineproxy/platform.py b/linux/raofflineproxy/platform.py
index aaaa269d..6221c67d 100644
--- a/linux/raofflineproxy/platform.py
+++ b/linux/raofflineproxy/platform.py
@@ -1,8 +1,11 @@
from __future__ import annotations
+import os
+import subprocess
from pathlib import Path
from .config import (
+ DEFAULT_DARKOS_HOME,
DEFAULT_MUOS_INIT_DIR,
DEFAULT_ONION_STARTUP_SCRIPT,
MUOS_USER_INIT_CONFIG,
@@ -17,6 +20,7 @@
DEFAULT_MUOS_ROMS_ROOT = Path("/mnt/mmc/ROMS")
DEFAULT_ONION_ROMS_ROOT = Path("/mnt/SDCARD/Roms")
DEFAULT_ROCKNIX_ROMS_ROOT = Path("/storage/roms")
+DEFAULT_DARKOS_ROMS_ROOT = Path("/roms")
DEFAULT_KNULLI_STARTUP_SCRIPT = Path("/userdata/system/custom.sh")
DEFAULT_MUOS_STARTUP_SCRIPT = DEFAULT_MUOS_INIT_DIR / "raofflineproxy.sh"
DEFAULT_ROCKNIX_STARTUP_SCRIPT = Path("/storage/.config/autostart/raofflineproxy.sh")
@@ -35,6 +39,8 @@
ROCKNIX_MODULES_DIR = Path("/storage/.config/modules")
ROCKNIX_MODULES_LAUNCHER = ROCKNIX_MODULES_DIR / "RAOfflineProxy.sh"
ROCKNIX_TOOL_SOURCE = Path("/storage/.local/share/raofflineproxy/RAOfflineProxy.sh")
+DEFAULT_DARKOS_AUTOSTART_UNIT = Path("/etc/systemd/system/raofflineproxy-autostart.service")
+DARKOS_SERVICE_NAME = "raofflineproxy-autostart.service"
ROM_DIRECTORY_KEYS = [
"content_directory",
]
@@ -74,6 +80,9 @@ def resolve_rom_root(config_data: dict) -> Path:
if DEFAULT_ROCKNIX_ROMS_ROOT.exists() and DEFAULT_ROCKNIX_ROMS_ROOT.is_dir():
return DEFAULT_ROCKNIX_ROMS_ROOT
+ if DEFAULT_DARKOS_ROMS_ROOT.exists() and DEFAULT_DARKOS_ROMS_ROOT.is_dir():
+ return DEFAULT_DARKOS_ROMS_ROOT
+
return cfg_path.parent
@@ -175,6 +184,10 @@ def ensure_boot_hook(config_data: dict) -> None:
startup_script.chmod(0o755)
return
+ if startup_script == DEFAULT_DARKOS_AUTOSTART_UNIT:
+ _install_darkos_autostart_unit(config_data)
+ return
+
startup_script.parent.mkdir(parents=True, exist_ok=True)
existing = (
startup_script.read_text(encoding="utf-8", errors="replace")
@@ -200,6 +213,10 @@ def remove_boot_hook(config_data: dict) -> None:
startup_script.unlink()
return
+ if startup_script == DEFAULT_DARKOS_AUTOSTART_UNIT:
+ _remove_darkos_autostart_unit(startup_script)
+ return
+
existing = startup_script.read_text(encoding="utf-8", errors="replace")
cleaned = strip_autostart_block(existing).strip()
startup_script.write_text(f"{cleaned}\n" if cleaned else "", encoding="utf-8")
@@ -227,6 +244,9 @@ def resolve_startup_script_path(config_data: dict) -> Path | None:
if Path("/opt/muos/script/archive").exists():
return DEFAULT_MUOS_STARTUP_SCRIPT
+ if DEFAULT_DARKOS_HOME.exists():
+ return DEFAULT_DARKOS_AUTOSTART_UNIT
+
if Path("/userdata/system").exists():
return DEFAULT_KNULLI_STARTUP_SCRIPT
@@ -276,6 +296,14 @@ def autostart_command(config_data: dict) -> tuple[str]:
),
)
+ if startup_script == DEFAULT_DARKOS_AUTOSTART_UNIT:
+ return (
+ str(
+ config_data.get("autostart_launcher")
+ or "/home/ark/raofflineproxy/bin/raofflineproxy"
+ ),
+ )
+
launcher = str(
config_data.get("autostart_launcher")
or "/userdata/system/raofflineproxy/bin/raofflineproxy"
@@ -435,3 +463,86 @@ def rocknix_boot_hook_script(config_data: dict) -> str:
"",
]
)
+
+
+def darkos_boot_hook_script(config_data: dict) -> str:
+ launcher = autostart_command(config_data)[0]
+ return "\n".join(
+ [
+ "[Unit]",
+ "Description=RAOfflineProxy autostart reconcile",
+ "After=emulationstation.service network.target",
+ "",
+ "[Service]",
+ "Type=oneshot",
+ f"ExecStart={launcher} boot-reconcile",
+ "",
+ "[Install]",
+ "WantedBy=multi-user.target",
+ "",
+ ]
+ )
+
+
+def _run_privileged(args: list[str]) -> bool:
+ # dArkOS's own ES Tools scripts (e.g. "Enable Remote Services.sh") call
+ # `sudo systemctl ...` non-interactively from the same unprivileged Tools
+ # context RAOfflineProxy runs in, which only works with passwordless sudo
+ # configured for the device user. -n makes sudo fail fast instead of
+ # hanging on a password prompt if that assumption turns out to be wrong.
+ candidates = [["sudo", "-n", *args]]
+ if os.geteuid() == 0:
+ candidates.append(list(args))
+
+ for candidate in candidates:
+ try:
+ result = subprocess.run(candidate, capture_output=True, timeout=10)
+ except OSError:
+ continue
+ if result.returncode == 0:
+ return True
+ return False
+
+
+def _write_privileged(path: Path, content: str) -> bool:
+ try:
+ path.write_text(content, encoding="utf-8")
+ return True
+ except OSError:
+ pass
+
+ try:
+ result = subprocess.run(
+ ["sudo", "-n", "tee", str(path)],
+ input=content,
+ text=True,
+ capture_output=True,
+ timeout=10,
+ )
+ return result.returncode == 0
+ except OSError:
+ return False
+
+
+def _install_darkos_autostart_unit(config_data: dict) -> None:
+ # boot-reconcile decides at runtime whether to apply/revert based on the
+ # config flag, so this unit is installed once and left permanently
+ # enabled — toggling autostart never needs to touch systemctl again.
+ # Called both from install.sh and unprivileged from the SDL menu, so any
+ # failure here (e.g. no sudo access) is swallowed rather than raised.
+ if not _write_privileged(
+ DEFAULT_DARKOS_AUTOSTART_UNIT, darkos_boot_hook_script(config_data)
+ ):
+ return
+ _run_privileged(["systemctl", "daemon-reload"])
+ _run_privileged(["systemctl", "enable", DARKOS_SERVICE_NAME])
+
+
+def _remove_darkos_autostart_unit(startup_script: Path) -> None:
+ _run_privileged(["systemctl", "disable", "--now", DARKOS_SERVICE_NAME])
+ if not _run_privileged(["rm", "-f", str(startup_script)]):
+ try:
+ startup_script.unlink(missing_ok=True)
+ except OSError:
+ pass
+ _run_privileged(["systemctl", "daemon-reload"])
diff --git a/linux/raofflineproxy/proxy_service.py b/linux/raofflineproxy/proxy_service.py
index 7ac534f6..38b0e374 100644
--- a/linux/raofflineproxy/proxy_service.py
+++ b/linux/raofflineproxy/proxy_service.py
@@ -20,7 +20,6 @@
from .config import (
DATABASE_FILE,
FALLBACK_USER_AGENT,
- LOG_FILE,
RA_MEDIA_HOST,
image_caching_enabled,
proxy_host,
@@ -958,7 +957,7 @@ def __init__(self, server: ProxyRuntimeServer, interval_seconds: int = 15):
self.server = server
self.interval_seconds = interval_seconds
self.stop_event = threading.Event()
- self.watched_inodes = _capture_file_inodes([LOG_FILE, DATABASE_FILE])
+ self.watched_inodes = _capture_file_inodes([DATABASE_FILE])
def stop(self) -> None:
self.stop_event.set()
diff --git a/linux/raofflineproxy/rom_hashing.py b/linux/raofflineproxy/rom_hashing.py
index 4e741c39..aab4ea38 100644
--- a/linux/raofflineproxy/rom_hashing.py
+++ b/linux/raofflineproxy/rom_hashing.py
@@ -96,6 +96,7 @@ def _candidate_library_paths() -> list[str | None]:
"/mnt/SDCARD/App/RAOfflineProxy/lib",
"/run/muos/storage/application/RAOfflineProxy/lib",
"/storage/.local/share/raofflineproxy/lib",
+ "/home/ark/raofflineproxy/lib",
)
for base in bases:
for name in names:
diff --git a/linux/raofflineproxy/update.py b/linux/raofflineproxy/update.py
index d4303e54..798d8758 100644
--- a/linux/raofflineproxy/update.py
+++ b/linux/raofflineproxy/update.py
@@ -27,6 +27,7 @@
PLATFORM_MUOS = "muos"
PLATFORM_ROCKNIX = "rocknix"
PLATFORM_ALLIUM = "allium"
+PLATFORM_DARKOS = "darkos"
KNULLI_UPDATE_INSTALLER_PATH = CONFIG_DIR / "update-installer.sh"
ONION_UPDATE_ARCHIVE_PATH = CONFIG_DIR / "update-onion.zip"
MUOS_UPDATE_ARCHIVE_PATH = CONFIG_DIR / "update-muos.muxapp"
@@ -261,6 +262,9 @@ def find_platform_asset_url(platform: str, assets: list[dict]) -> str | None:
elif platform == PLATFORM_ALLIUM:
if "allium" not in lower_name or not lower_name.endswith(".zip"):
continue
+ elif platform == PLATFORM_DARKOS:
+ if "darkos" not in lower_name or not lower_name.endswith(".sh"):
+ continue
else:
continue
@@ -658,6 +662,7 @@ def validate_platform(platform: str) -> str:
PLATFORM_MUOS,
PLATFORM_ROCKNIX,
PLATFORM_ALLIUM,
+ PLATFORM_DARKOS,
}:
raise ValueError(f"Unsupported update platform: {platform}")
return lowered
diff --git a/linux/tests/test_linux_autostart.py b/linux/tests/test_linux_autostart.py
index f2bfe715..b10b4862 100644
--- a/linux/tests/test_linux_autostart.py
+++ b/linux/tests/test_linux_autostart.py
@@ -7,6 +7,13 @@
from linux.raofflineproxy import platform
+def _stub_systemctl(*_args, **_kwargs):
+ class _Result:
+ returncode = 0
+
+ return _Result()
+
+
class LinuxAutostartTests(unittest.TestCase):
def setUp(self) -> None:
patcher = patch.object(platform, "save_config", lambda *_a, **_k: None)
@@ -222,6 +229,152 @@ def test_muos_disable_autostart_keeps_init_script_and_flag(self) -> None:
platform.DEFAULT_MUOS_STARTUP_SCRIPT = original_muos_startup
platform.MUOS_USER_INIT_CONFIG = original_user_init_config
+ def test_autostart_command_uses_darkos_launcher(self) -> None:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ unit_path = Path(temp_dir) / "raofflineproxy-autostart.service"
+ config_data = {"startup_script": str(unit_path)}
+
+ original_unit = platform.DEFAULT_DARKOS_AUTOSTART_UNIT
+ try:
+ platform.DEFAULT_DARKOS_AUTOSTART_UNIT = unit_path
+ self.assertEqual(
+ platform.autostart_command(config_data),
+ ("/home/ark/raofflineproxy/bin/raofflineproxy",),
+ )
+ finally:
+ platform.DEFAULT_DARKOS_AUTOSTART_UNIT = original_unit
+
+ def test_darkos_ensure_boot_hook_writes_unit_file(self) -> None:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ unit_path = Path(temp_dir) / "raofflineproxy-autostart.service"
+ config_data = {"startup_script": str(unit_path)}
+
+ original_unit = platform.DEFAULT_DARKOS_AUTOSTART_UNIT
+ try:
+ platform.DEFAULT_DARKOS_AUTOSTART_UNIT = unit_path
+ with patch.object(platform.subprocess, "run", _stub_systemctl):
+ platform.enable_autostart(config_data)
+
+ content = unit_path.read_text(encoding="utf-8")
+ self.assertIn("[Unit]", content)
+ self.assertIn(
+ "ExecStart=/home/ark/raofflineproxy/bin/raofflineproxy boot-reconcile",
+ content,
+ )
+ self.assertIn("WantedBy=multi-user.target", content)
+ self.assertTrue(platform.is_autostart_enabled(config_data))
+ finally:
+ platform.DEFAULT_DARKOS_AUTOSTART_UNIT = original_unit
+
+ def test_darkos_ensure_boot_hook_survives_missing_systemctl(self) -> None:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ unit_path = Path(temp_dir) / "raofflineproxy-autostart.service"
+ config_data = {"startup_script": str(unit_path)}
+
+ original_unit = platform.DEFAULT_DARKOS_AUTOSTART_UNIT
+ try:
+ platform.DEFAULT_DARKOS_AUTOSTART_UNIT = unit_path
+ platform.enable_autostart(config_data)
+
+ self.assertTrue(unit_path.exists())
+ self.assertTrue(platform.is_autostart_enabled(config_data))
+ finally:
+ platform.DEFAULT_DARKOS_AUTOSTART_UNIT = original_unit
+
+ def test_darkos_ensure_boot_hook_survives_permission_error(self) -> None:
+ unit_path = Path("/nonexistent-dir/raofflineproxy-autostart.service")
+ config_data = {"startup_script": str(unit_path)}
+
+ original_unit = platform.DEFAULT_DARKOS_AUTOSTART_UNIT
+ try:
+ platform.DEFAULT_DARKOS_AUTOSTART_UNIT = unit_path
+ platform.enable_autostart(config_data)
+
+ self.assertTrue(platform.is_autostart_enabled(config_data))
+ finally:
+ platform.DEFAULT_DARKOS_AUTOSTART_UNIT = original_unit
+
+ def test_darkos_disable_autostart_keeps_unit_file(self) -> None:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ unit_path = Path(temp_dir) / "raofflineproxy-autostart.service"
+ config_data = {"startup_script": str(unit_path)}
+
+ original_unit = platform.DEFAULT_DARKOS_AUTOSTART_UNIT
+ try:
+ platform.DEFAULT_DARKOS_AUTOSTART_UNIT = unit_path
+ platform.enable_autostart(config_data)
+ platform.disable_autostart(config_data)
+
+ self.assertTrue(unit_path.exists())
+ self.assertFalse(platform.is_autostart_enabled(config_data))
+ finally:
+ platform.DEFAULT_DARKOS_AUTOSTART_UNIT = original_unit
+
+ def test_darkos_ensure_boot_hook_falls_back_to_sudo_tee(self) -> None:
+ # Simulates the real device case: direct write fails (unprivileged,
+ # /etc/systemd/system not writable), so it must fall back to
+ # `sudo -n tee`. dArkOS's own ES Tools scripts rely on the same
+ # passwordless-sudo assumption for the device user.
+ unit_path = Path("/nonexistent-dir/raofflineproxy-autostart.service")
+ config_data = {"startup_script": str(unit_path)}
+ written: dict[str, str] = {}
+
+ def fake_run(args, **kwargs):
+ class _Result:
+ returncode = 0
+
+ if args[:2] == ["sudo", "-n"] and args[2] == "tee":
+ written["content"] = kwargs.get("input", "")
+ return _Result()
+
+ original_unit = platform.DEFAULT_DARKOS_AUTOSTART_UNIT
+ try:
+ platform.DEFAULT_DARKOS_AUTOSTART_UNIT = unit_path
+ with patch.object(platform.subprocess, "run", fake_run):
+ platform.enable_autostart(config_data)
+
+ self.assertIn("ExecStart=", written.get("content", ""))
+ self.assertTrue(platform.is_autostart_enabled(config_data))
+ finally:
+ platform.DEFAULT_DARKOS_AUTOSTART_UNIT = original_unit
+
+ def test_darkos_ensure_boot_hook_survives_sudo_denied(self) -> None:
+ unit_path = Path("/nonexistent-dir/raofflineproxy-autostart.service")
+ config_data = {"startup_script": str(unit_path)}
+
+ def fake_run(args, **kwargs):
+ class _Result:
+ returncode = 1
+
+ return _Result()
+
+ original_unit = platform.DEFAULT_DARKOS_AUTOSTART_UNIT
+ try:
+ platform.DEFAULT_DARKOS_AUTOSTART_UNIT = unit_path
+ with patch.object(platform.subprocess, "run", fake_run):
+ platform.enable_autostart(config_data)
+
+ self.assertFalse(unit_path.exists())
+ self.assertTrue(platform.is_autostart_enabled(config_data))
+ finally:
+ platform.DEFAULT_DARKOS_AUTOSTART_UNIT = original_unit
+
+ def test_darkos_remove_boot_hook_deletes_unit_file(self) -> None:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ unit_path = Path(temp_dir) / "raofflineproxy-autostart.service"
+ config_data = {"startup_script": str(unit_path)}
+
+ original_unit = platform.DEFAULT_DARKOS_AUTOSTART_UNIT
+ try:
+ platform.DEFAULT_DARKOS_AUTOSTART_UNIT = unit_path
+ platform.enable_autostart(config_data)
+
+ platform.remove_boot_hook(config_data)
+
+ self.assertFalse(unit_path.exists())
+ finally:
+ platform.DEFAULT_DARKOS_AUTOSTART_UNIT = original_unit
+
if __name__ == "__main__":
unittest.main()
diff --git a/linux/tests/test_linux_config_paths.py b/linux/tests/test_linux_config_paths.py
index a4f61396..d64cd4e1 100644
--- a/linux/tests/test_linux_config_paths.py
+++ b/linux/tests/test_linux_config_paths.py
@@ -71,6 +71,38 @@ def test_resolve_config_dir_uses_muos_application_dir(self) -> None:
else:
os.environ["XDG_CONFIG_HOME"] = original_xdg
+ def test_resolve_config_dir_uses_darkos_home(self) -> None:
+ original_override = os.environ.get("RAOFFLINEPROXY_CONFIG_DIR")
+ original_xdg = os.environ.get("XDG_CONFIG_HOME")
+ original_onion = config.DEFAULT_ONION_APP_DIR
+ original_muos = config.DEFAULT_MUOS_APPLICATION_DIR
+ original_darkos = config.DEFAULT_DARKOS_HOME
+ try:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ os.environ.pop("RAOFFLINEPROXY_CONFIG_DIR", None)
+ os.environ.pop("XDG_CONFIG_HOME", None)
+ config.DEFAULT_ONION_APP_DIR = Path(temp_dir) / "missing-onion"
+ config.DEFAULT_MUOS_APPLICATION_DIR = Path(temp_dir) / "missing-muos"
+ config.DEFAULT_DARKOS_HOME = Path(temp_dir) / "ark"
+ config.DEFAULT_DARKOS_HOME.mkdir(parents=True)
+
+ self.assertEqual(
+ config.resolve_config_dir(),
+ config.DEFAULT_DARKOS_HOME / ".config" / "raofflineproxy",
+ )
+ finally:
+ config.DEFAULT_ONION_APP_DIR = original_onion
+ config.DEFAULT_MUOS_APPLICATION_DIR = original_muos
+ config.DEFAULT_DARKOS_HOME = original_darkos
+ if original_override is None:
+ os.environ.pop("RAOFFLINEPROXY_CONFIG_DIR", None)
+ else:
+ os.environ["RAOFFLINEPROXY_CONFIG_DIR"] = original_override
+ if original_xdg is None:
+ os.environ.pop("XDG_CONFIG_HOME", None)
+ else:
+ os.environ["XDG_CONFIG_HOME"] = original_xdg
+
def test_detect_retroarch_cfg_prefers_muos_path(self) -> None:
original_override = os.environ.get("RAOFFLINEPROXY_RETROARCH_CFG")
original_muos = config.DEFAULT_MUOS_RETROARCH_CFG
diff --git a/linux/tests/test_linux_release_versions.py b/linux/tests/test_linux_release_versions.py
index bc1aa06e..a391bcbc 100644
--- a/linux/tests/test_linux_release_versions.py
+++ b/linux/tests/test_linux_release_versions.py
@@ -80,7 +80,7 @@ def test_every_target_declares_a_version_somewhere(self) -> None:
def test_all_known_targets_are_discovered(self) -> None:
found = {path.parent.name for path in _build_bundles()}
self.assertEqual(
- found, {"allium", "knulli", "muos", "onion", "rocknix", "spruce"}
+ found, {"allium", "darkos", "knulli", "muos", "onion", "rocknix", "spruce"}
)