mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42aa7692dc | ||
|
|
5786ddf81f | ||
|
|
ec3243a997 | ||
|
|
d5ef1c832a | ||
|
|
61ea96a9e8 | ||
|
|
ed2160c137 | ||
|
|
c9fb19b0cd | ||
|
|
9a6558d8b3 |
No files matched your search
@@ -0,0 +1,49 @@
|
||||
name: Build FEX (x86-64 Steam client experiment)
|
||||
|
||||
# FEX built for the runtime in an Arch Linux ARM container on an arm64 runner (tools/fex), for the
|
||||
# opt-in x86-64 Steam client (bannerlator-steam-x64). Published as a release asset that script
|
||||
# fetches; the runtime image and the default arm64 client are untouched.
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'tools/fex/**'
|
||||
- '.github/workflows/build-fex.yml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to publish as (e.g. fex-2609-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 in Arch Linux ARM
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker run --rm -v "$PWD:/work" -w /work menci/archlinuxarm:base-devel bash tools/fex/build-in-arch.sh
|
||||
sudo chown -R "$(id -u):$(id -g)" .
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: fex
|
||||
path: |
|
||||
fex.tzst
|
||||
fex.tzst.sha256
|
||||
fex-needed.txt
|
||||
fex-files.txt
|
||||
- name: Publish
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.tag != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh release create "${{ inputs.tag }}" --latest=false --prerelease=false --title "${{ inputs.tag }}" --notes "FEX for the runtime (tools/fex/build-in-arch.sh); fetched by bannerlator-steam-x64. sha256 $(cut -d' ' -f1 fex.tzst.sha256)" fex.tzst fex.tzst.sha256 || gh release upload "${{ inputs.tag }}" fex.tzst fex.tzst.sha256 --clobber
|
||||
# A component the apk fetches by tag, never something users install: it must not
|
||||
# become the repo's Latest release or appear as a pre-release, also when re-uploaded.
|
||||
gh release edit "${{ inputs.tag }}" --latest=false --prerelease=false
|
||||
@@ -279,6 +279,7 @@ class MainActivity : ComponentActivity() {
|
||||
private var emulatorList by mutableStateOf<List<Library.Emulator>>(emptyList())
|
||||
private var runningLabel by mutableStateOf<String?>(null)
|
||||
private var logsEnabled by mutableStateOf(true)
|
||||
private var vacDiagnostics by mutableStateOf(false)
|
||||
private var showRoms by mutableStateOf(false)
|
||||
private var homeAppSelected by mutableStateOf(false)
|
||||
private var homeScreenEnabled by mutableStateOf(false)
|
||||
@@ -363,7 +364,7 @@ class MainActivity : ComponentActivity() {
|
||||
busy = busy, stage = stage, percent = percent,
|
||||
desktopInstalled = desktopInstalled,
|
||||
offlineAccount = offlineAccount, offline = offline,
|
||||
frameGenLabel = frameGenLabel, romsDir = romsDir, logsEnabled = logsEnabled,
|
||||
frameGenLabel = frameGenLabel, romsDir = romsDir, logsEnabled = logsEnabled, vacDiagnostics = vacDiagnostics,
|
||||
steamGames = steamGames, emulators = emulatorList, running = runningLabel,
|
||||
frameGen = FrameGen.mode(this),
|
||||
lossless = lossless,
|
||||
@@ -461,6 +462,10 @@ class MainActivity : ComponentActivity() {
|
||||
SessionPrefs.setLogsEnabled(this, !SessionPrefs.logsEnabled(this))
|
||||
logsEnabled = SessionPrefs.logsEnabled(this)
|
||||
},
|
||||
onVacDiagnostics = {
|
||||
SessionPrefs.setVacDiagnostics(this, !SessionPrefs.vacDiagnostics(this))
|
||||
vacDiagnostics = SessionPrefs.vacDiagnostics(this)
|
||||
},
|
||||
onShareLogs = {
|
||||
Thread({
|
||||
val zip = runCatching { SessionLogShare.zipLatest(this) }.getOrNull()
|
||||
@@ -1192,6 +1197,7 @@ class MainActivity : ComponentActivity() {
|
||||
frameGenLabel = FrameGen.label(this)
|
||||
romsDir = SessionPrefs.romsDir(this).takeIf { it.isNotEmpty() }
|
||||
logsEnabled = SessionPrefs.logsEnabled(this)
|
||||
vacDiagnostics = SessionPrefs.vacDiagnostics(this)
|
||||
runningLabel = if (SessionState.running) when (SessionState.mode) {
|
||||
SessionService.MODE_DESKTOP -> "Desktop"
|
||||
SessionService.MODE_RUN -> Library.nameForProgram(SessionState.program)
|
||||
|
||||
@@ -45,6 +45,7 @@ object SessionArtifacts {
|
||||
}
|
||||
LogRedactor.learnFromRuntime(LinuxRuntime.rootDir(context))
|
||||
copySteamLogs(context, dir)
|
||||
VacDiagnostics.collect(context, dir)
|
||||
// A session the system killed leaves its trace here and nowhere else.
|
||||
SessionLogCapture.dumpCrashBuffer(File(dir, "crash.log"))
|
||||
scrubFolder(dir)
|
||||
@@ -145,6 +146,7 @@ object SessionArtifacts {
|
||||
)
|
||||
if (newest) {
|
||||
copySteamLogs(context, dir)
|
||||
VacDiagnostics.collect(context, dir)
|
||||
}
|
||||
SessionLogCapture.dumpCrashBuffer(File(dir, "crash.log"))
|
||||
SessionEvents.record("session.artifacts_recovered", mapOf("newest" to newest), dir)
|
||||
|
||||
@@ -424,6 +424,18 @@ object SessionPrefs {
|
||||
prefs(context).edit().putBoolean("logs", on).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* VAC diagnostics (see [VacDiagnostics]): Source-engine games launched with -condebug and their
|
||||
* console plus the client's VAC lines summarised into vac.txt. Off by default: it changes a
|
||||
* game's command line.
|
||||
*/
|
||||
fun vacDiagnostics(context: Context): Boolean =
|
||||
prefs(context).getBoolean("vac_diagnostics", false)
|
||||
|
||||
fun setVacDiagnostics(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("vac_diagnostics", on).apply()
|
||||
}
|
||||
|
||||
// ── Per-mode display ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -267,6 +267,7 @@ class SessionService : Service() {
|
||||
val runtimeDir = File(filesDir, ".wayland-rt").apply { mkdirs() }
|
||||
killStragglers()
|
||||
SessionFiles.stage(this, root)
|
||||
VacDiagnostics.arm(this)
|
||||
|
||||
val sessionDir = openSessionFolder()
|
||||
val sessionLog = File(sessionDir, "session.log")
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.core.LogRedactor
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
import java.io.File
|
||||
import java.io.RandomAccessFile
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* VAC diagnostics: what a session can show about whether a game ran VAC-secure, from the files
|
||||
* Valve's own software writes - nothing is inferred from our side of the launch.
|
||||
*
|
||||
* The verdict is the game's to give. A Source-engine game decides secure or insecure once, as it
|
||||
* starts, and says so only in its own console ("You are in insecure mode. You must restart before
|
||||
* you can connect to secure servers."). That console reaches a file only when the game runs with
|
||||
* -condebug, as console.log in the game's mod folder. With this on, the launch wrapper
|
||||
* (bannerlator-proton, bl_vacdiag) adds -condebug to Source titles and notes each launch; at the end
|
||||
* of the session this copies each console.log written during it into vac/, scrubbed, and writes
|
||||
* vac.txt: the game's own secure/insecure lines, any VAC lines in the client's logs, and which
|
||||
* client files are present.
|
||||
*
|
||||
* The SteamLite lesson applies: its "INSECURE - VAC may kick" warning was our own parser reading the
|
||||
* wrong log while the client was secure all along. So vac.txt quotes the lines it found and says
|
||||
* "inconclusive" when the game never reached a server, rather than guessing either way.
|
||||
*/
|
||||
object VacDiagnostics {
|
||||
private const val TAG = "VacDiagnostics"
|
||||
|
||||
/** In the guest's home; the launch wrapper checks for it. Holds the session's start, epoch seconds. */
|
||||
private const val MARKER = "root/.droiddeck-vac-diagnostics"
|
||||
|
||||
/** One line per game launch the wrapper saw: epoch, app id, -condebug added or not, the exe. */
|
||||
private const val LAUNCHES = "root/.droiddeck-vac-launches"
|
||||
|
||||
/** The console log grows across launches; the end of it is the session that just ran. */
|
||||
private const val CONSOLE_TAIL_BYTES = 4L * 1024 * 1024
|
||||
|
||||
private const val MAX_QUOTED = 40
|
||||
|
||||
private val INSECURE = Regex("insecure mode", RegexOption.IGNORE_CASE)
|
||||
// "Connecting to" is only an attempt: L4D2 logs it, retries, and then refuses a VAC server with a
|
||||
// dialog ("Connection Blocked: Please remove plug-ins before attempting to connect to a VAC
|
||||
// secured server") that never reaches the console. Only "Connected to" means the game got in.
|
||||
private val CONNECTED = Regex("^Connected to", RegexOption.IGNORE_CASE)
|
||||
private val ATTEMPTED = Regex("^Connecting to", RegexOption.IGNORE_CASE)
|
||||
private val NOTABLE = Regex("\\bVAC\\b|secure|Connected to|Connecting to|Disconnect|challenge|game coordinator", RegexOption.IGNORE_CASE)
|
||||
private val STEAM_VAC = Regex("\\bVAC\\b|anti-?cheat", RegexOption.IGNORE_CASE)
|
||||
|
||||
fun enabled(context: Context): Boolean = SessionPrefs.vacDiagnostics(context)
|
||||
|
||||
/**
|
||||
* At session start, after the scripts are staged: arms the wrapper for this session (or disarms
|
||||
* it) and starts a fresh launch list, so the summary names only games this session ran.
|
||||
*/
|
||||
fun arm(context: Context) {
|
||||
val root = LinuxRuntime.rootDir(context)
|
||||
val marker = File(root, MARKER)
|
||||
try {
|
||||
File(root, LAUNCHES).delete()
|
||||
if (enabled(context)) marker.writeText("${System.currentTimeMillis() / 1000}\n")
|
||||
else marker.delete()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "could not arm VAC diagnostics", e)
|
||||
}
|
||||
}
|
||||
|
||||
/** At the end of a session, into its folder. Does nothing unless the session was armed. */
|
||||
fun collect(context: Context, dir: File) {
|
||||
val root = LinuxRuntime.rootDir(context)
|
||||
val marker = File(root, MARKER)
|
||||
if (!marker.isFile) return
|
||||
try {
|
||||
val since = (marker.readText().trim().toLongOrNull() ?: 0L) * 1000 - 5_000
|
||||
val out = File(dir, "vac").apply { mkdirs() }
|
||||
val consoles = consoleLogs(context).filter { it.lastModified() >= since }
|
||||
val report = StringBuilder()
|
||||
report.appendLine("VAC diagnostics - ${now()}")
|
||||
report.appendLine("Read from the game's own console (-condebug) and the Steam client's logs.")
|
||||
report.appendLine()
|
||||
report.appendLine("== Client")
|
||||
clientFiles(root).forEach { report.appendLine(" $it") }
|
||||
report.appendLine()
|
||||
report.appendLine("== Game launches this session (from the launch wrapper)")
|
||||
val launches = File(root, LAUNCHES).takeIf { it.isFile }?.readLines().orEmpty().filter { it.isNotBlank() }
|
||||
if (launches.isEmpty()) report.appendLine(" none - no Windows game was launched through the client")
|
||||
launches.forEach { report.appendLine(" " + LogRedactor.redact(it)) }
|
||||
report.appendLine()
|
||||
report.appendLine("== Game consoles")
|
||||
if (consoles.isEmpty()) {
|
||||
report.appendLine(" No console.log was written this session. Only Source-engine games write one,")
|
||||
report.appendLine(" and only when started with -condebug (this setting adds it).")
|
||||
}
|
||||
consoles.forEach { log ->
|
||||
val name = consoleName(log)
|
||||
val lines = tail(log)
|
||||
File(out, "$name-console.log").bufferedWriter().use { w ->
|
||||
lines.forEach { w.write(LogRedactor.redact(it)); w.newLine() }
|
||||
}
|
||||
report.appendLine("-- $name (vac/$name-console.log)")
|
||||
report.appendLine(" Verdict: " + verdict(lines))
|
||||
lines.filter { NOTABLE.containsMatchIn(it) }.takeLast(MAX_QUOTED)
|
||||
.forEach { report.appendLine(" | " + LogRedactor.redact(it.trim())) }
|
||||
report.appendLine()
|
||||
}
|
||||
report.appendLine("== VAC in the Steam client's logs")
|
||||
val steamHits = File(dir, "steam").listFiles { f -> f.isFile }?.sortedBy { it.name }.orEmpty()
|
||||
.flatMap { f -> runCatching { f.readLines() }.getOrDefault(emptyList()).filter { STEAM_VAC.containsMatchIn(it) }.map { "${f.name}: ${it.trim()}" } }
|
||||
if (steamHits.isEmpty()) report.appendLine(" none - the client logged no VAC activity this session")
|
||||
steamHits.takeLast(MAX_QUOTED).forEach { report.appendLine(" $it") }
|
||||
File(dir, "vac.txt").writeText(report.toString())
|
||||
Log.i(TAG, "VAC diagnostics: ${consoles.size} console log(s), ${steamHits.size} client VAC line(s), into $dir")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "collecting VAC diagnostics", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun verdict(lines: List<String>): String = when {
|
||||
lines.any { INSECURE.containsMatchIn(it) } ->
|
||||
"INSECURE - the game started in insecure mode and cannot join VAC-secured servers until it restarts."
|
||||
lines.any { CONNECTED.containsMatchIn(it) } ->
|
||||
"No insecure notice, and the game connected to a server. Confirm with the server's own info that it is VAC-secured."
|
||||
lines.any { ATTEMPTED.containsMatchIn(it) } ->
|
||||
"NOT CONNECTED - the game tried a server and never got in. If the screen said \"Connection Blocked: " +
|
||||
"Please remove plug-ins ... VAC secured server\" (Left 4 Dead 2's wording), the game is insecure; " +
|
||||
"that dialog never reaches the console."
|
||||
else ->
|
||||
"Inconclusive - the game never connected to a server. Join a VAC-secured server, then check again."
|
||||
}
|
||||
|
||||
/** Every console.log in a Steam library: <game>/<mod>/console.log (Source) and <game>/game/<mod>/console.log (Source 2). */
|
||||
private fun consoleLogs(context: Context): List<File> {
|
||||
val found = LinkedHashMap<String, File>()
|
||||
for (library in libraryRoots(context)) {
|
||||
val games = File(library, "steamapps/common").listFiles { f -> f.isDirectory } ?: continue
|
||||
for (game in games) {
|
||||
val mods = (game.listFiles { f -> f.isDirectory }.orEmpty().toList() +
|
||||
File(game, "game").listFiles { f -> f.isDirectory }.orEmpty().toList())
|
||||
for (mod in mods) {
|
||||
val log = File(mod, "console.log")
|
||||
if (log.isFile) found[runCatching { log.canonicalPath }.getOrDefault(log.path)] = log
|
||||
}
|
||||
}
|
||||
}
|
||||
return found.values.toList()
|
||||
}
|
||||
|
||||
private fun libraryRoots(context: Context): List<File> {
|
||||
val root = LinuxRuntime.rootDir(context)
|
||||
val steam = File(root, "root/.local/share/Steam")
|
||||
val roots = mutableListOf(steam)
|
||||
GameStorage.effective(context)?.let { roots.add(File(it.path)) }
|
||||
runCatching {
|
||||
Regex("\"path\"\\s+\"([^\"]+)\"").findAll(File(steam, "steamapps/libraryfolders.vdf").readText()).forEach {
|
||||
roots.add(File(root, it.groupValues[1].removePrefix("/")))
|
||||
}
|
||||
}
|
||||
return roots.filter { it.isDirectory }.distinctBy { runCatching { it.canonicalPath }.getOrDefault(it.path) }
|
||||
}
|
||||
|
||||
/** "Team Fortress 2-tf": the game folder and the mod, safe as a file name. */
|
||||
private fun consoleName(log: File): String {
|
||||
val mod = log.parentFile
|
||||
val game = mod?.parentFile?.let { if (it.name == "game") it.parentFile else it }
|
||||
return "${game?.name ?: "game"}-${mod?.name ?: "mod"}".replace(Regex("[^A-Za-z0-9._ +-]+"), "_")
|
||||
}
|
||||
|
||||
private fun tail(file: File): List<String> = try {
|
||||
RandomAccessFile(file, "r").use { raf ->
|
||||
val start = (raf.length() - CONSOLE_TAIL_BYTES).coerceAtLeast(0)
|
||||
raf.seek(start)
|
||||
val bytes = ByteArray((raf.length() - start).toInt())
|
||||
raf.readFully(bytes)
|
||||
String(bytes, Charsets.UTF_8).lines().let { if (start > 0) it.drop(1) else it }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
/** Which Steam client builds are on disk - the arm64 one has no VAC module of its own to run. */
|
||||
private fun clientFiles(root: File): List<String> {
|
||||
val steam = File(root, "root/.local/share/Steam")
|
||||
return listOf(
|
||||
"linuxarm64/steamclient.so" to "arm64 Linux client",
|
||||
"linux64/steamclient.so" to "x86-64 Linux client",
|
||||
"ubuntu12_64/steamclient.so" to "x86-64 Linux client (ubuntu12_64)",
|
||||
).map { (path, label) -> "$label: ${if (File(steam, path).isFile) "present" else "absent"} ($path)" }
|
||||
}
|
||||
|
||||
private fun now(): String = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(Date())
|
||||
}
|
||||
@@ -91,6 +91,7 @@ class FrontEndState(
|
||||
val frameGenLabel: String,
|
||||
val romsDir: String?,
|
||||
val logsEnabled: Boolean,
|
||||
val vacDiagnostics: Boolean = false,
|
||||
val steamGames: List<Library.SteamGame>,
|
||||
val emulators: List<Library.Emulator>,
|
||||
val running: String?,
|
||||
@@ -152,6 +153,7 @@ class FrontEndActions(
|
||||
val onFiles: () -> Unit,
|
||||
val onBrowseFiles: (File) -> Unit = {},
|
||||
val onLogs: () -> Unit,
|
||||
val onVacDiagnostics: () -> Unit = {},
|
||||
val onShareLogs: () -> Unit = {},
|
||||
val onClearLogs: () -> Unit = {},
|
||||
val onOffline: () -> Unit,
|
||||
|
||||
@@ -289,6 +289,7 @@ internal fun SetupPanel(
|
||||
}
|
||||
}
|
||||
ToggleRow(host, "logs", stringResource(R.string.setup_logs), stringResource(R.string.setup_logs_hint), s.logsEnabled) { a.onLogs() }
|
||||
ToggleRow(host, "vacdiag", stringResource(R.string.setup_vac_diag), stringResource(R.string.setup_vac_diag_hint), s.vacDiagnostics) { a.onVacDiagnostics() }
|
||||
ActionRow(stringResource(R.string.setup_latest_logs), stringResource(R.string.drawer_logs_hint), stringResource(R.string.drawer_share_logs), a.onShareLogs)
|
||||
ActionRow(stringResource(R.string.setup_saved_logs), stringResource(R.string.setup_saved_logs_hint, com.droiddeck.launcher.session.SessionPaths.KEEP_SESSIONS), stringResource(R.string.setup_clear_logs), a.onClearLogs)
|
||||
ToggleRow(
|
||||
|
||||
@@ -483,6 +483,8 @@
|
||||
<string name="setup_session">Sesión</string>
|
||||
<string name="setup_logs">Registros de sesión</string>
|
||||
<string name="setup_logs_hint">Se guardan tras cada sesión</string>
|
||||
<string name="setup_vac_diag">Diagnóstico de VAC</string>
|
||||
<string name="setup_vac_diag_hint">Los juegos Source registran su consola (-condebug); vac.txt en los registros dice si es seguro o no</string>
|
||||
<string name="setup_latest_logs">Últimos registros de sesión</string>
|
||||
<string name="setup_saved_logs">Registros de sesión guardados</string>
|
||||
<string name="setup_saved_logs_hint">Conserva los %1$d más recientes</string>
|
||||
|
||||
@@ -508,6 +508,8 @@
|
||||
<string name="setup_session">Session</string>
|
||||
<string name="setup_logs">Session logs</string>
|
||||
<string name="setup_logs_hint">Saved after each session</string>
|
||||
<string name="setup_vac_diag">VAC diagnostics</string>
|
||||
<string name="setup_vac_diag_hint">Source games log their console (-condebug); vac.txt in the session logs says secure or insecure</string>
|
||||
<string name="setup_latest_logs">Latest session logs</string>
|
||||
<string name="setup_saved_logs">Saved session logs</string>
|
||||
<string name="setup_saved_logs_hint">Keeps the newest %1$d</string>
|
||||
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/bash
|
||||
# Runs INSIDE an Arch Linux ARM container (menci/archlinuxarm:base-devel) on an arm64 runner.
|
||||
# Builds FEX for the runtime, with tools/fex/patches, (Arch Linux ARM ships no package and FEX publishes no Linux
|
||||
# binaries), plus the runtime's own unsquashfs to unpack FEX's x86 rootfs image, and packs both
|
||||
# as fex.tzst (usr/local/...) for bannerlator-steam-x64 to stage. No thunks: they need an x86
|
||||
# cross toolchain, and the x86-64 Steam client only needs FEX to run, not host GPU drivers.
|
||||
set -euxo pipefail
|
||||
VERSION=${FEX_VERSION:-FEX-2609}
|
||||
WORK=/work
|
||||
cd "$WORK"
|
||||
# Same pacman workarounds as tools/gamescope/build-in-arch.sh.
|
||||
grep -q '^DisableSandbox' /etc/pacman.conf || sed -i 's/^\[options\]/[options]\nDisableSandbox/' /etc/pacman.conf
|
||||
{ for m in https://ca.us.mirror.archlinuxarm.org https://fl.us.mirror.archlinuxarm.org https://de3.mirror.archlinuxarm.org https://nl.mirror.archlinuxarm.org; do echo "Server = $m/\$arch/\$repo"; done; cat /etc/pacman.d/mirrorlist; } > /etc/pacman.d/mirrorlist.new
|
||||
mv /etc/pacman.d/mirrorlist.new /etc/pacman.d/mirrorlist
|
||||
pacman -Syu --noconfirm --needed git zstd binutils cmake ninja clang lld llvm python squashfs-tools file
|
||||
|
||||
rm -rf fex-src out && git clone -q --depth 1 --branch "$VERSION" --recurse-submodules --shallow-submodules https://github.com/FEX-Emu/FEX.git fex-src
|
||||
# Our proot answers openat2 with ENOSYS (tools/proot/PATCHES.md, 0008); FEX opened every rootfs
|
||||
# path with openat2 and fell through to the arm64 guest's files on anything but EXDEV.
|
||||
for p in tools/fex/patches/*.patch; do patch -d fex-src -p1 --no-backup-if-mismatch < "$p"; done
|
||||
# TUNE_CPU none: the default (native) would tune for the runner's Neoverse cores, not the device.
|
||||
cmake -S fex-src -B fex-build -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local \
|
||||
-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DUSE_LINKER=lld \
|
||||
-DTUNE_CPU=none -DTUNE_ARCH=armv8-a \
|
||||
-DBUILD_TESTING=OFF -DBUILD_THUNKS=OFF -DBUILD_FEXCONFIG=OFF -DENABLE_ASSERTIONS=OFF \
|
||||
-DENABLE_CCACHE=OFF -DENABLE_OFFLINE_TELEMETRY=OFF
|
||||
ninja -C fex-build
|
||||
DESTDIR="$WORK/out" ninja -C fex-build install
|
||||
install -Dm755 /usr/bin/unsquashfs out/usr/local/bin/unsquashfs
|
||||
# Nothing outside the runtime's own libraries: every needed soname must resolve in a stock guest.
|
||||
for f in out/usr/local/bin/*; do
|
||||
file "$f" | grep -q ELF || continue
|
||||
readelf -d "$f" | awk '/NEEDED/ { gsub(/[\[\]]/, "", $5); print $5 }'
|
||||
done | sort -u | tee fex-needed.txt
|
||||
(cd out && find . -type f -o -type l | sort) | tee fex-files.txt
|
||||
tar -C out -c . | zstd -19 -T0 -o fex.tzst
|
||||
sha256sum fex.tzst | tee fex.tzst.sha256
|
||||
@@ -0,0 +1,42 @@
|
||||
diff --git a/Source/Tools/LinuxEmulation/LinuxSyscalls/FileManagement.cpp b/Source/Tools/LinuxEmulation/LinuxSyscalls/FileManagement.cpp
|
||||
index 7172460..d566e3b 100644
|
||||
--- a/Source/Tools/LinuxEmulation/LinuxSyscalls/FileManagement.cpp
|
||||
+++ b/Source/Tools/LinuxEmulation/LinuxSyscalls/FileManagement.cpp
|
||||
@@ -674,9 +674,10 @@ uint64_t FileManager::Open(const char* pathname, int flags, uint32_t mode) {
|
||||
};
|
||||
fd = ::syscall(SYSCALL_DEF(openat2), Path.FD, Path.Path, &how, sizeof(how));
|
||||
|
||||
- if (fd == -1 && errno == EXDEV) {
|
||||
- // This means a magic symlink (/proc/foo) was involved. In this case we
|
||||
- // just punt and do the access without RESOLVE_IN_ROOT.
|
||||
+ if (fd == -1 && (errno == EXDEV || errno == ENOSYS)) {
|
||||
+ // EXDEV: a magic symlink (/proc/foo) was involved. ENOSYS: no openat2 here (proot).
|
||||
+ // Either way, do the access without RESOLVE_IN_ROOT; GetEmulatedFDPath has already
|
||||
+ // followed absolute symlinks inside the rootfs.
|
||||
fd = ::syscall(SYSCALL_DEF(openat), Path.FD, Path.Path, flags, mode);
|
||||
}
|
||||
}
|
||||
@@ -927,9 +928,10 @@ uint64_t FileManager::Openat([[maybe_unused]] int dirfs, const char* pathname, i
|
||||
.resolve = (Path.FD == AT_FDCWD) ? 0u : RESOLVE_IN_ROOT, // AT_FDCWD means it's a thunk and not via RootFS
|
||||
};
|
||||
fd = ::syscall(SYSCALL_DEF(openat2), Path.FD, Path.Path, &how, sizeof(how));
|
||||
- if (fd == -1 && errno == EXDEV) {
|
||||
- // This means a magic symlink (/proc/foo) was involved. In this case we
|
||||
- // just punt and do the access without RESOLVE_IN_ROOT.
|
||||
+ if (fd == -1 && (errno == EXDEV || errno == ENOSYS)) {
|
||||
+ // EXDEV: a magic symlink (/proc/foo) was involved. ENOSYS: no openat2 here (proot).
|
||||
+ // Either way, do the access without RESOLVE_IN_ROOT; GetEmulatedFDPath has already
|
||||
+ // followed absolute symlinks inside the rootfs.
|
||||
fd = ::syscall(SYSCALL_DEF(openat), Path.FD, Path.Path, flags, mode);
|
||||
}
|
||||
}
|
||||
@@ -967,6 +969,9 @@ uint64_t FileManager::Openat2(int dirfs, const char* pathname, FEX::HLE::open_ho
|
||||
// This means a magic symlink (/proc/foo) was involved. In this case we
|
||||
// just punt and do the access without RESOLVE_IN_ROOT.
|
||||
fd = ::syscall(SYSCALL_DEF(openat2), Path.FD, Path.Path, how, usize);
|
||||
+ } else if (fd == -1 && errno == ENOSYS && how->resolve == 0) {
|
||||
+ // No openat2 here (proot): the plain openat inside the rootfs, as above.
|
||||
+ fd = ::syscall(SYSCALL_DEF(openat), Path.FD, Path.Path, (int)how->flags, (int)how->mode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,17 @@ export BL_DESKTOP=1
|
||||
ln -sf /usr/local/bin/bannerlator-steam-launch /usr/local/bin/steamdeck-steam 2>/dev/null
|
||||
ln -sf /usr/local/bin/bannerlator-steam-shim /usr/local/bin/steam 2>/dev/null
|
||||
python3 /usr/local/bin/bannerlator-desktop-games 2>&1 | sed 's/^/[games] /'
|
||||
# EXPERIMENT: Valve's x86-64 client under FEX (bannerlator-steam-x64), in a terminal so its
|
||||
# first-run downloads show. Not a Game entry, so droiddeck-desktop-gpu leaves its Exec alone.
|
||||
cat > /usr/share/applications/droiddeck-steam-x64.desktop <<'ENTRY'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Steam x86-64 (FEX, experimental)
|
||||
Comment=Valve's x86-64 Linux client under FEX, in its own home with its own login
|
||||
Exec=qterminal -e /usr/local/bin/bannerlator-steam-x64
|
||||
Icon=steam
|
||||
Categories=System;
|
||||
ENTRY
|
||||
# Player 1 on the controller in every emulator that has no profile of its own yet - now, and again
|
||||
# every half minute: an emulator started for the first time writes its own config, which is then
|
||||
# given the controller for its next start without waiting for the next desktop.
|
||||
|
||||
@@ -498,6 +498,31 @@ case $mode in
|
||||
done
|
||||
return 1
|
||||
}
|
||||
# A title the compat pass maps while the client runs only takes that mapping at the client's
|
||||
# next start (see bannerlator-steam-compat): until then the client launches it its own way -
|
||||
# a Linux build stuck on "Downloading content (0%)" for a Steam Linux Runtime and FEX that
|
||||
# cannot run here (Left 4 Dead 2, 2026-10-02). So restart the client once for it, through the
|
||||
# same marker and -shutdown as the ARM64 Proton's restart: only when every new title has
|
||||
# finished installing (StateFlags 4) and no game is running; another download just resumes.
|
||||
# Once per title and three times per session at most, so a client that rewrites config.vdf
|
||||
# can never loop it.
|
||||
compat_pending=$HOME/.bl-compat-pending
|
||||
compat_restarted=$HOME/.bl-compat-restarted
|
||||
rm -f "$compat_pending"
|
||||
: > "$compat_restarted"
|
||||
compat_restarts=0
|
||||
compat_settled() {
|
||||
local app m flags
|
||||
while read -r app; do
|
||||
[ -n "$app" ] || continue
|
||||
m="$steam_root/steamapps/appmanifest_$app.acf"
|
||||
[ -f "$m" ] || m="/mnt/droiddeck-sd/steamapps/appmanifest_$app.acf"
|
||||
[ -f "$m" ] || return 1
|
||||
flags=$(sed -n 's/^[[:space:]]*"StateFlags"[[:space:]]*"\([0-9]*\)".*/\1/p' "$m" | head -n 1)
|
||||
[ "$flags" = 4 ] || return 1
|
||||
done < "$compat_pending"
|
||||
! pgrep -f 'reaper SteamLaunch' >/dev/null 2>&1
|
||||
}
|
||||
for ((_i = 0; _i < 720; _i++)); do
|
||||
nap 5
|
||||
beat=$((beat + 1))
|
||||
@@ -505,7 +530,17 @@ case $mode in
|
||||
bannerlator-seed-redists "$steam_root" >/dev/null 2>&1 || true
|
||||
if compat_due; then
|
||||
: > "$compat_stamp"
|
||||
bannerlator-steam-compat "$steam_root" >/dev/null 2>&1 || true
|
||||
bannerlator-steam-compat "$steam_root" --live >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ -s "$compat_pending" ] && compat_settled; then
|
||||
_bl_new=$(sort -u "$compat_pending" | grep -vxF -f "$compat_restarted" || true)
|
||||
rm -f "$compat_pending"
|
||||
[ -n "$_bl_new" ] && [ "$compat_restarts" -lt 3 ] || continue
|
||||
compat_restarts=$((compat_restarts + 1))
|
||||
printf '%s\n' $_bl_new >> "$compat_restarted"
|
||||
step "newly installed title(s) $(echo $_bl_new) set to the ARM64 Proton: restarting the Steam client once so it takes effect"
|
||||
: > "$HOME/.bl-restart-once"
|
||||
"$steam_root/steamrtarm64/steam" -shutdown >/dev/null 2>&1 || true
|
||||
fi
|
||||
done
|
||||
) &
|
||||
@@ -719,6 +754,9 @@ BUSCONF
|
||||
# arguments, which were acted on. Not counted against the update cap.
|
||||
if [ -f "$HOME/.bl-restart-once" ]; then
|
||||
echo "== steam: restarting once so the new compatibility layer is in the client's lists"
|
||||
# The client wrote config.vdf back from its own memory as it exited, which can drop the
|
||||
# per-title entries written while it ran - the very ones this restart is for.
|
||||
bannerlator-steam-compat "$steam_root" >/dev/null 2>&1 || true
|
||||
set --
|
||||
continue
|
||||
fi
|
||||
|
||||
@@ -189,6 +189,37 @@ bl_idtech3() {
|
||||
}
|
||||
"""
|
||||
|
||||
# VAC diagnostics (the app's Setup page): a Source-engine game says whether it started VAC-secure
|
||||
# only in its own console, and writes that console to <mod>/console.log only with -condebug. Armed
|
||||
# per session by the app (~/.droiddeck-vac-diagnostics); each launch is noted for its summary.
|
||||
# Nothing else about the launch changes - this only asks the game to keep its own log.
|
||||
BL_VACDIAG_SETUP = """
|
||||
bl_vacdiag() {
|
||||
[ -f "${HOME:-/root}/.droiddeck-vac-diagnostics" ] || return 0
|
||||
[ "${verb:-}" = waitforexitandrun ] || return 0
|
||||
local exe= a dir added=no app=${SteamAppId:-${STEAM_COMPAT_APP_ID:-0}}
|
||||
for a in "$@"; do case $a in *.exe|*.EXE) exe=$a ;; esac; done
|
||||
[ -n "$exe" ] || return 0
|
||||
dir=$(dirname "$exe")
|
||||
# Source: <game>/<mod>/gameinfo.txt beside the exe. Source 2: <game>/game/bin/win64/<exe> with
|
||||
# <game>/game/<mod>/gameinfo.gi.
|
||||
if ls "$dir"/*/gameinfo.txt >/dev/null 2>&1 || ls "$dir"/../../*/gameinfo.gi >/dev/null 2>&1; then
|
||||
case " $* " in
|
||||
*" -condebug "*) echo "bannerlator-proton: vac-diag: app $app already has -condebug" >&2 ;;
|
||||
*) BL_EXTRA_ARGS+=(-condebug); added=yes
|
||||
echo "bannerlator-proton: vac-diag: app $app is a Source title; launched with -condebug" >&2 ;;
|
||||
esac
|
||||
else
|
||||
added=not-source
|
||||
echo "bannerlator-proton: vac-diag: app $app is not a Source title; it keeps no console log to read" >&2
|
||||
fi
|
||||
case " $* " in
|
||||
*" -insecure "*) echo "bannerlator-proton: vac-diag: app $app has -insecure in its launch options - it can never be VAC-secure this way" >&2 ;;
|
||||
esac
|
||||
printf '%s\tapp %s\tcondebug %s\t%s\n' "$(date +%s)" "$app" "$added" "${exe##*/}" >> "${HOME:-/root}/.droiddeck-vac-launches"
|
||||
}
|
||||
"""
|
||||
|
||||
BL_DIRECTAUDIO_SETUP = """
|
||||
bl_directaudio() {
|
||||
base=$1
|
||||
@@ -271,6 +302,7 @@ if [ -n "$depot" ]; then
|
||||
bl_directaudio "$depot"
|
||||
bl_game_cpus
|
||||
bl_idtech3 "$@"
|
||||
bl_vacdiag "$@"
|
||||
bl_libpath
|
||||
exec ${BL_TASKSET:-} /usr/local/bin/bannerlator-game-env "$depot/proton" "$verb" "$@" "${BL_EXTRA_ARGS[@]}"
|
||||
fi
|
||||
@@ -282,7 +314,7 @@ exit 1
|
||||
# something else.
|
||||
LAUNCHER_SH = LAUNCHER_SH.replace(
|
||||
"verb=$1; shift", "verb=$1; shift\n" + BL_DIRECTAUDIO_SETUP + BL_GAME_CPUS_SETUP + BL_IDTECH3_SETUP
|
||||
+ BL_COMPONENTS_SETUP + BL_LIBPATH_SETUP, 1)
|
||||
+ BL_VACDIAG_SETUP + BL_COMPONENTS_SETUP + BL_LIBPATH_SETUP, 1)
|
||||
|
||||
# A third-party Proton dropped into compatibilitytools.d - GE-Proton, proton-cachyos, anything of
|
||||
# that shape - is adopted rather than used as shipped, for the same two reasons Valve's ARM64 depot
|
||||
@@ -318,6 +350,7 @@ bl_components "$here"
|
||||
bl_directaudio "$here"
|
||||
bl_game_cpus
|
||||
bl_idtech3 "$@"
|
||||
bl_vacdiag "$@"
|
||||
bl_libpath
|
||||
# Proton takes its base directory from dirname(sys.argv[0]) without resolving it, so it has to be
|
||||
# started from its own tree.
|
||||
@@ -503,7 +536,7 @@ def adopt_extras(tools):
|
||||
body = (EXTRA_WRAPPER_SH % entry_point).replace(
|
||||
'here=$(dirname "$0")',
|
||||
'here=$(dirname "$0")\n' + BL_DIRECTAUDIO_SETUP + BL_GAME_CPUS_SETUP + BL_IDTECH3_SETUP
|
||||
+ BL_COMPONENTS_SETUP + BL_LIBPATH_SETUP, 1)
|
||||
+ BL_VACDIAG_SETUP + BL_COMPONENTS_SETUP + BL_LIBPATH_SETUP, 1)
|
||||
write_changed(wrapper, body, 0o755)
|
||||
write_changed(manifest, EXTRA_MANIFEST_VDF)
|
||||
write_changed(compat_vdf, EXTRA_COMPAT_TOOL_VDF % (name, entry))
|
||||
@@ -546,7 +579,8 @@ def installed_apps(steam_root):
|
||||
|
||||
|
||||
def register_default(config_path, apps=(), name=None, protect=()):
|
||||
"""Writes the mapping. `name` defaults to our own tool; the first-run seed passes Valve's."""
|
||||
"""Writes the mapping. `name` defaults to our own tool; the first-run seed passes Valve's.
|
||||
Returns the app ids given a per-app entry by this call."""
|
||||
tool = name or TOOL
|
||||
# Tool names the user may deliberately have chosen (see adopt_extras); never repointed.
|
||||
keep = {n.lower() for n in protect}
|
||||
@@ -555,7 +589,8 @@ def register_default(config_path, apps=(), name=None, protect=()):
|
||||
tokens = tokenize(original)
|
||||
steam = find_block(tokens, ["InstallConfigStore", "Software", "Valve", "Steam"])
|
||||
if steam is None:
|
||||
return
|
||||
return []
|
||||
added = []
|
||||
mapping = find_block(tokens[steam + 1:], ["CompatToolMapping"])
|
||||
entry = ['"0"', "{", '"name"', '"%s"' % tool, '"config"', '""', '"priority"', '"75"', "}"]
|
||||
if mapping is None:
|
||||
@@ -579,10 +614,12 @@ def register_default(config_path, apps=(), name=None, protect=()):
|
||||
continue
|
||||
tokens[mapping + 1:mapping + 1] = [
|
||||
'"%s"' % app, "{", '"name"', '"%s"' % tool, '"config"', '""', '"priority"', '"250"', "}"]
|
||||
added.append(app)
|
||||
# Nothing to change is the usual case on the session's beat: the client's own file is left alone.
|
||||
text = serialize(tokens)
|
||||
if text != serialize(tokenize(original)):
|
||||
write_changed(config_path, text)
|
||||
return added
|
||||
|
||||
|
||||
def depot_build(depot):
|
||||
@@ -633,8 +670,18 @@ def main():
|
||||
config = os.path.join(steam_root, "config", "config.vdf")
|
||||
if os.path.isfile(config):
|
||||
apps = installed_apps(steam_root)
|
||||
register_default(config, apps, protect=extras)
|
||||
added = register_default(config, apps, protect=extras)
|
||||
print("bannerlator-steam-compat: default and %d installed title(s) set to %s" % (len(apps), TOOL))
|
||||
# The client reads config.vdf once, as it starts, and keeps its own decisions in memory for
|
||||
# the rest of its run: a title installed while it runs gets the client's own pick (native
|
||||
# for a Linux build, which then waits for Steam Linux Runtime + FEX it can never run here;
|
||||
# proton-stable for a Windows one), and the entry written here only counts from the next
|
||||
# start. Run from the session's beat (--live), the new entries are noted for the session,
|
||||
# which restarts the client once the install has settled (bannerlator-session).
|
||||
if added and "--live" in sys.argv[2:]:
|
||||
with open(os.path.join(os.path.expanduser("~"), ".bl-compat-pending"), "a") as f:
|
||||
f.write("".join("%s\n" % a for a in added))
|
||||
print("bannerlator-steam-compat: mapped while the client runs: %s" % " ".join(added))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
#!/bin/bash
|
||||
# EXPERIMENT, opt-in: Valve's x86-64 Linux Steam client under FEX, beside the default arm64 one.
|
||||
# bannerlator-steam-x64 [steam:// URL or client arguments]
|
||||
#
|
||||
# Why: the arm64 Linux client never runs VAC (TF2, L4D2: "Connection Blocked ... VAC secured
|
||||
# server", no VAC line in any client log) - VAC modules are x86 code. The x86-64 client under FEX
|
||||
# has a module it can run, the way SteamLite does in Wine. This runs Valve's client unchanged.
|
||||
#
|
||||
# Everything lives in its own home ($X64_HOME): the x86 client gets its own ~/.steam links, its
|
||||
# own login and its own config, and never touches the arm64 client's. Games are shared: its
|
||||
# steamapps is the arm64 client's. Only one client runs at a time (same account), so the arm64
|
||||
# one is shut down first.
|
||||
#
|
||||
# First run downloads FEX (this repo's release), FEX's Ubuntu 24.04 x86 rootfs (~525 MB, unpacked
|
||||
# ~1.5 GB) and the x86 client (~600 MB). Log: $X64_HOME/steam-x64.log.
|
||||
set -eu
|
||||
FEX_TAG=fex-2609-r2
|
||||
FEX_SHA256=PENDING
|
||||
FEX_URL=https://github.com/Droid-Deck/DroidDeck/releases/download/$FEX_TAG/fex.tzst
|
||||
ROOTFS_URL=https://rootfs.fex-emu.gg/Ubuntu_24_04/2026-08-11/Ubuntu_24_04.sqsh
|
||||
CDN=https://client-update.fastly.steamstatic.com
|
||||
MANIFEST=steam_client_publicbeta_ubuntu12
|
||||
|
||||
REAL_HOME=${HOME:-/root}
|
||||
X64_HOME=${BL_X64_HOME:-$REAL_HOME/.droiddeck-x64}
|
||||
ROOTFS=$X64_HOME/rootfs
|
||||
STEAM_X64=$X64_HOME/.local/share/Steam
|
||||
ARM_STEAM=${XDG_DATA_HOME:-$REAL_HOME/.local/share}/Steam
|
||||
LOG=$X64_HOME/steam-x64.log
|
||||
mkdir -p "$X64_HOME"
|
||||
step() { echo "== $(date -u +%H:%M:%S) $*" | tee -a "$LOG"; }
|
||||
|
||||
# --- FEX -------------------------------------------------------------------------------------
|
||||
if [ ! -x /usr/local/bin/FEX ] || [ "$(cat /usr/local/share/fex-emu/.droiddeck-tag 2>/dev/null)" != "$FEX_TAG" ]; then
|
||||
step "downloading FEX ($FEX_TAG)"
|
||||
tmp=$(mktemp -d)
|
||||
curl -fL --progress-bar "$FEX_URL" -o "$tmp/fex.tzst"
|
||||
if [ "$FEX_SHA256" != PENDING ]; then echo "$FEX_SHA256 $tmp/fex.tzst" | sha256sum -c --quiet; fi
|
||||
zstd -dc "$tmp/fex.tzst" | tar -C / -x
|
||||
rm -rf "$tmp"
|
||||
mkdir -p /usr/local/share/fex-emu && echo "$FEX_TAG" > /usr/local/share/fex-emu/.droiddeck-tag
|
||||
fi
|
||||
step "FEX ready ($FEX_TAG)"
|
||||
|
||||
# --- x86 rootfs --------------------------------------------------------------------------------
|
||||
if [ ! -f "$ROOTFS/.droiddeck-complete" ]; then
|
||||
step "downloading FEX's Ubuntu 24.04 x86 rootfs"
|
||||
# Resumes a part-done download; a finished one answers 416, which is not a failure here.
|
||||
curl -fL --progress-bar -C - "$ROOTFS_URL" -o "$X64_HOME/rootfs.sqsh" || {
|
||||
rc=$?
|
||||
[ "$rc" = 22 ] && [ -s "$X64_HOME/rootfs.sqsh" ] || { step "rootfs download failed (curl $rc)"; exit 1; }
|
||||
}
|
||||
step "unpacking the rootfs"
|
||||
rm -rf "$ROOTFS"
|
||||
# Ownership and xattrs mean nothing in this guest; device nodes cannot be made here and are not
|
||||
# needed (FEX passes /dev through from the host).
|
||||
/usr/local/bin/unsquashfs -no-xattrs -no-progress -ignore-errors -d "$ROOTFS" "$X64_HOME/rootfs.sqsh" >>"$LOG" 2>&1 || true
|
||||
[ -x "$ROOTFS/usr/bin/bash" ] || { step "rootfs unpack failed (no usr/bin/bash); see $LOG"; exit 1; }
|
||||
rm -f "$X64_HOME/rootfs.sqsh"
|
||||
touch "$ROOTFS/.droiddeck-complete"
|
||||
fi
|
||||
# A path missing from the rootfs falls through to the guest: without its own, the x86 loader
|
||||
# reads the guest's /etc/ld.so.preload and tries to load the session's arm64 preloads.
|
||||
if [ ! -e "$ROOTFS/etc/ld.so.preload" ]; then
|
||||
: > "$ROOTFS/etc/ld.so.preload"
|
||||
fi
|
||||
|
||||
# --- the x86-64 client -------------------------------------------------------------------------
|
||||
if [ ! -x "$STEAM_X64/steamrt64/steam" ] || [ ! -f "$STEAM_X64/package/droiddeck-installed" ]; then
|
||||
step "downloading the x86-64 Steam client ($MANIFEST)"
|
||||
work=$(mktemp -d)
|
||||
curl -fsSL "$CDN/$MANIFEST" -o "$work/manifest"
|
||||
version=$(awk -F'"' '$2 == "version" { print $4; exit }' "$work/manifest")
|
||||
# Same parsing as bannerlator-steam-install: every component block's plain zip and sha256.
|
||||
awk -F'"' '
|
||||
/^\t"/ && NF >= 3 && $4 == "" { block = $2 }
|
||||
$2 == "file" { file[block] = $4 }
|
||||
$2 == "sha2" { sha[block] = $4 }
|
||||
END { for (b in file) if (b ~ /_all$/ || b ~ /_ubuntu12$/) print b, file[b], sha[b] }
|
||||
' "$work/manifest" | sort > "$work/parts"
|
||||
[ -s "$work/parts" ] || { step "no client components in $MANIFEST"; exit 1; }
|
||||
mkdir -p "$STEAM_X64/package"
|
||||
total=$(grep -c . "$work/parts")
|
||||
i=0
|
||||
while read -r block file sum; do
|
||||
i=$((i + 1))
|
||||
step "client: $block ($i/$total)"
|
||||
curl -fsSL "$CDN/$file" -o "$work/$file"
|
||||
echo "$sum $work/$file" | sha256sum -c --quiet
|
||||
unzip -qo "$work/$file" -d "$STEAM_X64"
|
||||
rm -f "$work/$file"
|
||||
done < "$work/parts"
|
||||
rm -rf "$work"
|
||||
echo publicbeta > "$STEAM_X64/package/beta"
|
||||
echo "$version" > "$STEAM_X64/package/droiddeck-installed"
|
||||
chmod +x "$STEAM_X64/steam.sh" "$STEAM_X64/steamrt64/steam" "$STEAM_X64/ubuntu12_32/steam" 2>/dev/null || true
|
||||
fi
|
||||
# Shared games: the arm64 client's library, as long as it is not running at the same time.
|
||||
if [ -d "$ARM_STEAM/steamapps" ] && [ ! -e "$STEAM_X64/steamapps" ]; then
|
||||
ln -s "$ARM_STEAM/steamapps" "$STEAM_X64/steamapps"
|
||||
fi
|
||||
|
||||
# --- one client at a time ----------------------------------------------------------------------
|
||||
if pgrep -f 'steamrtarm64/steam( |$)' >/dev/null 2>&1; then
|
||||
step "shutting down the arm64 client first"
|
||||
"$ARM_STEAM/steamrtarm64/steam" -shutdown >/dev/null 2>&1 || true
|
||||
for _ in $(seq 1 30); do pgrep -f 'steamrtarm64/steam( |$)' >/dev/null 2>&1 || break; sleep 1; done
|
||||
fi
|
||||
|
||||
# --- run ---------------------------------------------------------------------------------------
|
||||
# The arm64 session's library paths and preloads are aarch64 and mean nothing to x86 processes.
|
||||
unset LD_LIBRARY_PATH LD_PRELOAD STEAM_RUNTIME STEAM_RUNTIME_PREFER_HOST_LIBRARIES
|
||||
export HOME=$X64_HOME
|
||||
export FEX_ROOTFS=$ROOTFS
|
||||
# A FEX process takes its rootfs from FEXServer, not from its own environment, and a server
|
||||
# started earlier without FEX_ROOTFS (any FEX run in this guest) answers with no rootfs at all:
|
||||
# every x86 library lookup then falls through to the arm64 guest. Stop it, so the next FEX
|
||||
# starts one with this rootfs.
|
||||
/usr/local/bin/FEXServer -k >/dev/null 2>&1 || true
|
||||
export STEAM_EXTRA_COMPAT_TOOLS_PATHS=
|
||||
step "starting the x86-64 client under FEX: steam.sh -no-cef-sandbox $*"
|
||||
cd "$STEAM_X64"
|
||||
exec /usr/local/bin/FEX "$ROOTFS/usr/bin/bash" "$STEAM_X64/steam.sh" -no-cef-sandbox "$@" >>"$LOG" 2>&1
|
||||
Reference in new issue
Block a user