Add Steam FEX launcher for Apple Silicon (aarch64)
Package the tested Asahi Steam launcher as omarchy-steam-fex, with runtime-only dependencies and offline launcher checks in makepkg. Preserve the original launcher behavior and document its runtime requirements and downstream ownership handoff. Co-authored-by: dl-alexandre <166029845+dl-alexandre@users.noreply.github.com> Co-authored-by: Santeri Hernejärvi <santeri@santeri.se>
This commit is contained in:
1 parent
3553c690a6
commit
534f007d6b
6 files changed
+371
No files matched your search
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"source": "local"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
Copyright (c) David Heinemeier Hansson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,25 @@
|
||||
pkgname=omarchy-steam-fex
|
||||
pkgver=1.0.0
|
||||
pkgrel=2
|
||||
pkgdesc='Steam launcher with login workarounds for Apple Silicon using muvm and FEX'
|
||||
arch=('aarch64')
|
||||
url='https://github.com/omacom/omarchy-pkgs/tree/master/pkgbuilds/omarchy-steam-fex'
|
||||
license=('MIT')
|
||||
checkdepends=('python')
|
||||
source=('omarchy-launch-steam' 'LICENSE' 'test-launcher.py')
|
||||
sha256sums=('d89559ef88b3589e7d972b44a4a29e418605542f44f2756afd24ed3ce3227609'
|
||||
'717ba1949502290f8e47688ae2e323acd06c8ca47aec9f7596b15f678c1af4a2'
|
||||
'1d31b9ed09292b79dc86d9d4b3fff01d19e8210fdc5e3b13e78b846671808ed5')
|
||||
|
||||
check() {
|
||||
python test-launcher.py
|
||||
}
|
||||
|
||||
package() {
|
||||
# These are runtime-only dependencies; the Asahi stack is not needed to
|
||||
# assemble or test the scripts in a standard Arch Linux ARM builder.
|
||||
depends=('bash' 'coreutils' 'python' 'steam' 'muvm' 'FEX-Emu')
|
||||
|
||||
install -Dm755 omarchy-launch-steam "$pkgdir/usr/bin/omarchy-launch-steam"
|
||||
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# Steam launcher for Apple Silicon
|
||||
|
||||
`omarchy-steam-fex` provides `omarchy-launch-steam` for the Asahi Linux `steam`, `muvm`, and `FEX-Emu` stack. Those runtime packages come from `asahi-alarm`; `FEX-Emu` provides `FEXBash`. The package is restricted to aarch64 and assumes that stack's `~/.local/share/fex-steam/steam-launcher/bin_steam.sh` layout.
|
||||
|
||||
The launcher runs Steam through `muvm` and `FEXBash` with the CEF occlusion workaround. Once Steam's client files are present, it also disables bootstrap verification and repair and patches the Steam UI network initialization block that can leave login waiting indefinitely. Initial bootstrap keeps the normal bootstrap flags. If the FEX launcher is unavailable, it falls back to `steam`.
|
||||
|
||||
`omarchy-launch-steam --prepare` writes the current user's desktop override with `Exec=omarchy-launch-steam %U` and applies the same UI patch. Each matching chunk is backed up as `.omarchy-bak` before its first patch; existing backups are preserved, and already-patched and unrecognized chunks are left unchanged. The regex depends on Valve's client code and may need updating when that code changes.
|
||||
|
||||
Only the launcher and license are installed. Steam's system desktop entry and downloaded client files remain outside this package's ownership; preparation runs as the desktop user, never in a package installation hook. A downstream Omarchy package that already owns the launcher must release that path before this package is installed, or in the same upgrade transaction.
|
||||
|
||||
Runtime dependencies are declared in `package()` so assembling the scripts does not require the Asahi stack in the build container. They remain required dependencies in the resulting package. `check()` runs the offline launcher tests using temporary homes and mocked Steam/muvm commands. To run them directly:
|
||||
|
||||
```sh
|
||||
python pkgbuilds/omarchy-steam-fex/test-launcher.py
|
||||
```
|
||||
|
||||
The launcher was extracted from [omarchy-mx-mac commit 5e8e1188](https://github.com/scottjones/omarchy-mx-mac/commit/5e8e1188e39fae70cf4f7bda1a1d85d66eccae51), credited to Scott Jones, dl-alexandre, and Santeri Hernejärvi. The extraction replaces `omarchy-cmd-present` with `command -v`, removing the dependency on Omarchy itself. The launcher matches the version published in [omarchy-pkgs-aarch64](https://github.com/omarchy-mac/omarchy-pkgs-aarch64/tree/main/pkgbuilds/omarchy-steam-fex).
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Launch Steam, applying Asahi muvm login workarounds on Apple Silicon
|
||||
# omarchy:group=launch
|
||||
# omarchy:args=[--prepare|steam-args...]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
steam_root="$HOME/.local/share/Steam"
|
||||
|
||||
write_steam_desktop() {
|
||||
local app_dir="$HOME/.local/share/applications"
|
||||
mkdir -p "$app_dir"
|
||||
cat >"$app_dir/steam.desktop" <<'EOF'
|
||||
[Desktop Entry]
|
||||
Name=Steam
|
||||
Comment=Application for managing and playing games on Steam
|
||||
Exec=omarchy-launch-steam %U
|
||||
Icon=steam
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Network;FileTransfer;Game;
|
||||
MimeType=x-scheme-handler/steam;x-scheme-handler/steamlink;
|
||||
EOF
|
||||
}
|
||||
|
||||
steam_client_ready() {
|
||||
[[ -d $steam_root/steamui ]] && compgen -G "$steam_root/*/steamui.so" >/dev/null
|
||||
}
|
||||
|
||||
launch_fex_steam() {
|
||||
local launcher="$1"
|
||||
shift
|
||||
|
||||
exec muvm -- FEXBash -c 'exec "$@"' omarchy-steam "$launcher" "$@"
|
||||
}
|
||||
|
||||
patch_steam_ui() {
|
||||
local steamui="$steam_root/steamui"
|
||||
[[ -d $steamui ]] || return 0
|
||||
|
||||
python3 - "$steamui" <<'PY'
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
steamui = pathlib.Path(sys.argv[1])
|
||||
pattern = re.compile(
|
||||
r"const t=\(0,(\w+)\.(\w+)\)\(\"System\.Network\.RegisterForDeviceChanges\"\);"
|
||||
r"t&&SteamClient\.System\.Network\.RegisterForDeviceChanges\(this\.OnNetworkDevicesChanged\),"
|
||||
r"\(0,\1\.\2\)\(\"System\.Network\.GetProxyInfo\"\)&&SteamClient\.System\.Network\.GetProxyInfo\(\)\.then\(e=>this\.m_proxyInfo=e\),"
|
||||
r"\(0,\1\.\2\)\(\"System\.Network\.RegisterForConnectivityTestChanges\"\)&&SteamClient\.System\.Network\.RegisterForConnectivityTestChanges\(this\.OnConnectivityTestStateChanged\),"
|
||||
r"t\|\|\(this\.m_bIsAwaitingInitialNetworkState=!1\)"
|
||||
)
|
||||
replacement = (
|
||||
r'const t=(0,\1.\2)("System.Network.RegisterForDeviceChanges")'
|
||||
r'&&!!(SteamClient.System&&SteamClient.System.Network&&"function"==typeof SteamClient.System.Network.RegisterForDeviceChanges);'
|
||||
r'try{t&&SteamClient.System.Network.RegisterForDeviceChanges(this.OnNetworkDevicesChanged)}catch(e){}'
|
||||
r'try{(0,\1.\2)("System.Network.GetProxyInfo")&&SteamClient.System.Network.GetProxyInfo().then(e=>this.m_proxyInfo=e)}catch(e){}'
|
||||
r'try{(0,\1.\2)("System.Network.RegisterForConnectivityTestChanges")&&SteamClient.System.Network.RegisterForConnectivityTestChanges(this.OnConnectivityTestStateChanged)}catch(e){}'
|
||||
r'this.m_bIsAwaitingInitialNetworkState=!1,this.m_bIsConnectedToANetwork=!0'
|
||||
)
|
||||
|
||||
for path in sorted(steamui.glob("chunk~*.js")):
|
||||
text = path.read_text(errors="replace")
|
||||
if "m_bIsConnectedToANetwork=!0" in text and "RegisterForDeviceChanges" in text:
|
||||
continue
|
||||
updated, count = pattern.subn(replacement, text, count=1)
|
||||
if count != 1:
|
||||
continue
|
||||
backup = path.with_suffix(path.suffix + ".omarchy-bak")
|
||||
if not backup.exists():
|
||||
backup.write_text(text)
|
||||
path.write_text(updated)
|
||||
PY
|
||||
}
|
||||
|
||||
prepare_asahi() {
|
||||
[[ $(uname -m) == aarch64 ]] || return 0
|
||||
write_steam_desktop
|
||||
patch_steam_ui
|
||||
}
|
||||
|
||||
if [[ ${1:-} == --prepare ]]; then
|
||||
prepare_asahi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ $(uname -m) == aarch64 ]] && command -v muvm >/dev/null && command -v FEXBash >/dev/null; then
|
||||
launcher="$HOME/.local/share/fex-steam/steam-launcher/bin_steam.sh"
|
||||
|
||||
if [[ -f $launcher ]]; then
|
||||
steam_args=(-cef-force-occlusion)
|
||||
if steam_client_ready; then
|
||||
prepare_asahi
|
||||
steam_args+=(
|
||||
-noverifyfiles
|
||||
-nobootstrapupdate
|
||||
-skipinitialbootstrap
|
||||
-norepairfiles
|
||||
)
|
||||
else
|
||||
write_steam_desktop
|
||||
fi
|
||||
|
||||
launch_fex_steam "$launcher" "${steam_args[@]}" "$@"
|
||||
fi
|
||||
fi
|
||||
|
||||
exec steam "$@"
|
||||
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Offline launcher tests: fake Steam/muvm/FEX, real Bash and Python, temporary HOME."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
LAUNCHER = Path(os.environ.get(
|
||||
'STEAM_LAUNCHER_TEST_SCRIPT',
|
||||
Path(__file__).with_name('omarchy-launch-steam'),
|
||||
))
|
||||
|
||||
# A representative minified Steam network initialization block, including
|
||||
# surrounding code that must survive the patch. Deliberately not a regex.
|
||||
ORIGINAL = (
|
||||
'before();const t=(0,Ab.cd)("System.Network.RegisterForDeviceChanges");'
|
||||
't&&SteamClient.System.Network.RegisterForDeviceChanges(this.OnNetworkDevicesChanged),'
|
||||
'(0,Ab.cd)("System.Network.GetProxyInfo")&&SteamClient.System.Network.GetProxyInfo().then(e=>this.m_proxyInfo=e),'
|
||||
'(0,Ab.cd)("System.Network.RegisterForConnectivityTestChanges")&&SteamClient.System.Network.RegisterForConnectivityTestChanges(this.OnConnectivityTestStateChanged),'
|
||||
't||(this.m_bIsAwaitingInitialNetworkState=!1);after();'
|
||||
)
|
||||
SKIP_BOOTSTRAP = ['-noverifyfiles', '-nobootstrapupdate', '-skipinitialbootstrap', '-norepairfiles']
|
||||
|
||||
MOCK = '''
|
||||
import json, os
|
||||
from pathlib import Path
|
||||
import shutil, sys
|
||||
name = Path(sys.argv[0]).name
|
||||
if name == 'uname':
|
||||
print(os.environ.get('TEST_ARCH', 'aarch64'))
|
||||
sys.exit(0)
|
||||
with open(os.environ['TEST_CALLS'], 'a') as log:
|
||||
log.write(json.dumps([name, *sys.argv[1:]]) + '\\n')
|
||||
if name == 'muvm':
|
||||
assert sys.argv[1:3] == ['--', 'FEXBash']
|
||||
os.execv(shutil.which('FEXBash'), sys.argv[2:])
|
||||
if name == 'FEXBash':
|
||||
os.execv('/bin/bash', ['/bin/bash', *sys.argv[1:]])
|
||||
sys.exit(int(os.environ.get('TEST_EXIT', '0')))
|
||||
'''
|
||||
|
||||
|
||||
class SteamLauncherTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory(prefix='steam-fex-test-')
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
self.user_home = self.root / 'home with spaces'
|
||||
self.user_home.mkdir()
|
||||
self.tools = self.root / 'tools'
|
||||
self.tools.mkdir()
|
||||
self.calls_path = self.root / 'calls.jsonl'
|
||||
self.env = dict(os.environ, HOME=str(self.user_home), PATH=str(self.tools),
|
||||
TEST_CALLS=str(self.calls_path), TEST_ARCH='aarch64', TEST_EXIT='0')
|
||||
for name in ['mkdir', 'cat', 'python3']:
|
||||
(self.tools / name).symlink_to(shutil.which(name))
|
||||
for name in ['uname', 'muvm', 'FEXBash', 'steam']:
|
||||
self.mock(self.tools / name)
|
||||
self.fex_launcher = self.user_home / '.local/share/fex-steam/steam-launcher/bin_steam.sh'
|
||||
self.mock(self.fex_launcher)
|
||||
self.steam_root = self.user_home / '.local/share/Steam'
|
||||
self.ui = self.steam_root / 'steamui'
|
||||
self.desktop = self.user_home / '.local/share/applications/steam.desktop'
|
||||
|
||||
def mock(self, path):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(f'#!{sys.executable}\n' + MOCK)
|
||||
path.chmod(0o755)
|
||||
|
||||
def run_launcher(self, *args, expected=0):
|
||||
result = subprocess.run(['/bin/bash', str(LAUNCHER), *args], env=self.env,
|
||||
capture_output=True, text=True, timeout=10)
|
||||
self.assertEqual(result.returncode, expected, result.stderr)
|
||||
return [json.loads(line) for line in self.calls_path.read_text().splitlines()] if self.calls_path.exists() else []
|
||||
|
||||
def chunk(self, content=ORIGINAL, name='chunk~network.js'):
|
||||
self.ui.mkdir(parents=True, exist_ok=True)
|
||||
path = self.ui / name
|
||||
path.write_text(content)
|
||||
return path
|
||||
|
||||
def client_ready(self):
|
||||
self.ui.mkdir(parents=True, exist_ok=True)
|
||||
binary = self.steam_root / 'ubuntu12_64/steamui.so'
|
||||
binary.parent.mkdir(parents=True, exist_ok=True)
|
||||
binary.touch()
|
||||
|
||||
def assert_desktop(self):
|
||||
text = self.desktop.read_text()
|
||||
self.assertIn('\nExec=omarchy-launch-steam %U\n', text)
|
||||
self.assertIn('x-scheme-handler/steam;x-scheme-handler/steamlink;', text)
|
||||
|
||||
def assert_fex(self, calls, flags, user_args):
|
||||
args = [str(self.fex_launcher), *flags, *user_args]
|
||||
self.assertEqual(calls, [
|
||||
['muvm', '--', 'FEXBash', '-c', 'exec "$@"', 'omarchy-steam', *args],
|
||||
['FEXBash', '-c', 'exec "$@"', 'omarchy-steam', *args],
|
||||
['bin_steam.sh', *flags, *user_args],
|
||||
])
|
||||
|
||||
def test_prepare_without_client_only_writes_user_desktop(self):
|
||||
self.assertEqual(self.run_launcher('--prepare'), [])
|
||||
self.assert_desktop()
|
||||
self.assertFalse(self.steam_root.exists())
|
||||
|
||||
def test_prepare_patches_matching_chunks_and_preserves_original(self):
|
||||
for identifier in ['Ab.cd', '_A2.x9']:
|
||||
with self.subTest(identifier=identifier):
|
||||
original = ORIGINAL.replace('Ab.cd', identifier)
|
||||
path = self.chunk(original, f'chunk~{identifier}.js')
|
||||
self.assertEqual(self.run_launcher('--prepare'), [])
|
||||
patched = path.read_text()
|
||||
self.assertTrue(patched.startswith('before();'))
|
||||
self.assertTrue(patched.endswith(';after();'))
|
||||
self.assertIn(f'const t=(0,{identifier})("System.Network.RegisterForDeviceChanges")&&!!', patched)
|
||||
self.assertEqual(patched.count('catch(e){}'), 3)
|
||||
self.assertIn('this.m_bIsAwaitingInitialNetworkState=!1,this.m_bIsConnectedToANetwork=!0', patched)
|
||||
self.assertEqual(path.with_suffix('.js.omarchy-bak').read_text(), original)
|
||||
self.assert_desktop()
|
||||
|
||||
def test_repeated_prepare_is_idempotent_and_keeps_first_backup(self):
|
||||
path = self.chunk()
|
||||
self.run_launcher('--prepare')
|
||||
patched, modified = path.read_bytes(), path.stat().st_mtime_ns
|
||||
self.run_launcher('--prepare')
|
||||
self.assertEqual(path.read_bytes(), patched)
|
||||
self.assertEqual(path.stat().st_mtime_ns, modified)
|
||||
path.write_text(ORIGINAL.replace('Ab.cd', 'New.api'))
|
||||
self.run_launcher('--prepare')
|
||||
self.assertIn('(0,New.api)', path.read_text())
|
||||
self.assertEqual(path.with_suffix('.js.omarchy-bak').read_text(), ORIGINAL)
|
||||
|
||||
def test_unmatched_code_and_other_filenames_are_untouched(self):
|
||||
for name, content in [('chunk~changed.js', 'otherNetworkCode();'), ('other.js', ORIGINAL)]:
|
||||
path = self.chunk(content, name)
|
||||
self.run_launcher('--prepare')
|
||||
self.assertEqual(path.read_text(), content)
|
||||
self.assertFalse(path.with_suffix('.js.omarchy-bak').exists())
|
||||
|
||||
def test_only_first_matching_block_is_patched(self):
|
||||
path = self.chunk(ORIGINAL + ORIGINAL)
|
||||
self.run_launcher('--prepare')
|
||||
self.assertEqual(path.read_text().count('m_bIsConnectedToANetwork=!0'), 1)
|
||||
self.assertIn(ORIGINAL, path.read_text())
|
||||
|
||||
def test_initial_launch_keeps_bootstrap_enabled_and_preserves_arguments(self):
|
||||
args = ['steam://rungameid/123', 'argument with spaces', '$(touch unwanted)', '']
|
||||
calls = self.run_launcher(*args)
|
||||
self.assert_fex(calls, ['-cef-force-occlusion'], args)
|
||||
self.assert_desktop()
|
||||
self.assertFalse(self.steam_root.exists())
|
||||
|
||||
def test_ui_directory_alone_does_not_disable_bootstrap(self):
|
||||
path = self.chunk()
|
||||
self.assert_fex(self.run_launcher(), ['-cef-force-occlusion'], [])
|
||||
self.assertEqual(path.read_text(), ORIGINAL)
|
||||
|
||||
def test_ready_launch_patches_and_disables_bootstrap(self):
|
||||
path = self.chunk()
|
||||
self.client_ready()
|
||||
args = ['steam://open/main']
|
||||
self.assert_fex(self.run_launcher(*args), ['-cef-force-occlusion', *SKIP_BOOTSTRAP], args)
|
||||
self.assertIn('m_bIsConnectedToANetwork=!0', path.read_text())
|
||||
self.assert_desktop()
|
||||
|
||||
def test_missing_fex_components_fall_back_without_modifying_user_files(self):
|
||||
for missing in [self.tools / 'muvm', self.tools / 'FEXBash', self.fex_launcher]:
|
||||
with self.subTest(missing=missing.name):
|
||||
missing.unlink()
|
||||
self.env['TEST_EXIT'] = '23'
|
||||
self.assertEqual(self.run_launcher('arg with spaces', expected=23), [['steam', 'arg with spaces']])
|
||||
self.assertFalse(self.desktop.exists())
|
||||
self.calls_path.unlink()
|
||||
self.mock(missing)
|
||||
|
||||
def test_x86_prepare_is_noop_and_launch_falls_back(self):
|
||||
self.env['TEST_ARCH'] = 'x86_64'
|
||||
path = self.chunk()
|
||||
self.assertEqual(self.run_launcher('--prepare'), [])
|
||||
self.assertFalse(self.desktop.exists())
|
||||
self.assertEqual(path.read_text(), ORIGINAL)
|
||||
self.assertEqual(self.run_launcher('steam://open/main'), [['steam', 'steam://open/main']])
|
||||
|
||||
def test_fex_exit_status_is_preserved(self):
|
||||
self.env['TEST_EXIT'] = '29'
|
||||
self.assert_fex(self.run_launcher(expected=29), ['-cef-force-occlusion'], [])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user