3 Commits
Author SHA1 Message Date
MichaelScottsmanandClaude Opus 5.5 313d043115 Native glass survives an update: the daemon turns native back on when glassd is in place again; the installer restarts a running LiquidAss around the swap
- Installing while LiquidAss ran swapped the folder under the daemon: glassd's binary vanished for a
  moment, the daemon went CSS-only for good (the glass looked transparent). It now puts the shipped
  binary in place or waits for it, and turns native back on once a second.
- install.sh turns a running LiquidAss off before the swap and on again after it, on the new version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:31:53 -04:00
MichaelScottsmanandClaude Opus 5.5 7d7f55a5b4 Installer puts the release's shipped glassd in place; README on native glass out of the box
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:28:05 -04:00
MichaelScottsmanandClaude Opus 5.5 d9d73383c2 Ship glassd and native mode on by default: install works out of the box
- native/glassd/prebuilt/glassd: the Frame's glassd built from these sources, shipped by sync.
- glass.py install: the shipped binary in place first, then a build from source over it (kept on a failed
  build), then a check that glassd is ready.
- lgs_shell start: puts the shipped binary in place when glassd is missing, so the Liquid Glass toggle run
  during an install never starts CSS-only (what left the glass transparent).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:27:55 -04:00
5 changed files with 86 additions and 10 deletions

No files matched your search

+3 -1
View File
@@ -58,7 +58,7 @@ The [uninstall](#uninstall) command (or `python glass.py uninstall`) removes all
## From the PC
```bash
python glass.py install # copy this checkout to the headset and add the launcher (for development)
python glass.py install # copy this checkout to the headset, set up glassd and add the launcher (for development)
python glass.py on | off | toggle | status
python glass.py dial 0.7 # glass intensity: 0 = clearest, 1 = most opaque (saved with your glass settings)
python glass.py uninstall
@@ -66,6 +66,8 @@ python glass.py uninstall
On the headset itself, `~/.local/share/glass-shell/device/lgs on|off|toggle|status|dial V` does the same.
Native glass works out of the box. Releases ship glassd built for the Frame (`native/glassd/prebuilt/glassd`), and the installer puts it in place. `python glass.py install` does the same, then rebuilds glassd from the checkout's sources over it (if that build fails, the shipped binary stays). The daemon also puts the shipped binary in place whenever glassd is missing, so the LiquidAss toggle never starts CSS-only. Native mode is on by default (`device/defaults.json`).
## How it works
| Piece | Role |
+39 -3
View File
@@ -77,6 +77,7 @@ import collections # noqa: E402
import hashlib # noqa: E402
import json # noqa: E402
import os # noqa: E402
import shutil # noqa: E402
import signal # noqa: E402
import subprocess # noqa: E402
import time # noqa: E402
@@ -930,7 +931,7 @@ class Shell:
self.native_enabled, self.css_reason = False, css_reason_for(self.native_req, self.native_src)
elif not self.glassd_path:
self.native_enabled, self.css_reason = False, "glassd disabled"
elif not os.access(self.glassd_path, os.X_OK):
elif not os.access(self.glassd_path, os.X_OK) and not ensure_glassd(self.glassd_path):
self.native_enabled, self.css_reason = False, "no glassd binary at start; python glass.py native-build"
else:
self.native_enabled, self.css_reason = True, None
@@ -2699,8 +2700,18 @@ class Shell:
if self.gseq and not self.g_files_removed: # native mode ended mid-session
self.g_files_removed = True
remove_glassd_files()
# only the binary was missing (an install swapping the folder under us, a toggle run before the
# install finished): native comes back as soon as a glassd is in place again
if self.native_requested and self.glassd_path and str(self.css_reason or "").startswith(
("glassd binary removed", "no glassd binary at start")) and ensure_glassd(self.glassd_path):
log("glassd: binary in place again; native layer back on")
self.native_enabled, self.css_reason = True, None
self.g_files_removed = False
self.g_given_up = False
self.tasks.append(asyncio.create_task(self.glassd_loop()))
self.changed.set()
return
if not os.access(self.glassd_path, os.X_OK):
if not os.access(self.glassd_path, os.X_OK) and not ensure_glassd(self.glassd_path):
self.disable_native("glassd binary removed")
await self.kill_glassd()
return
@@ -2748,7 +2759,7 @@ class Shell:
return
temp_streak = 0
while not self.stopping and self.native_possible():
if not os.access(self.glassd_path, os.X_OK):
if not os.access(self.glassd_path, os.X_OK) and not ensure_glassd(self.glassd_path):
self.disable_native("glassd binary removed")
return
# glassd needs SteamVR and something to cover (and the theme on: not dormant)
@@ -3362,6 +3373,29 @@ def core_args(argv):
return out
GLASSD_PREBUILT = os.path.join(lgs.ROOT, "native", "glassd", "prebuilt", "glassd")
def ensure_glassd(path=GLASSD_BIN):
"""The shipped glassd (native/glassd/prebuilt/glassd) in place when no build is there yet, so native
glass works out of the box and a Liquid Glass toggle that runs before a build finishes is never stuck
CSS-only. True when an executable glassd is in place."""
if os.access(path, os.X_OK):
return True
if path != GLASSD_BIN or not os.access(GLASSD_PREBUILT, os.R_OK):
return False
try:
tmp = path + ".tmp"
shutil.copyfile(GLASSD_PREBUILT, tmp)
os.chmod(tmp, 0o755)
os.replace(tmp, path)
log("glassd: no build here; the shipped binary is in place")
return True
except OSError as e:
log(f"glassd: could not put the shipped binary in place: {e}")
return False
def start(glassd=GLASSD_BIN, stay=False, glassd_args=None, native=None, test_report=None, assume_caps=None):
"""Start the unit. Also resumes SteamVR page theming that a lab "--theme
off" on a vr: page paused.
@@ -3379,6 +3413,8 @@ def start(glassd=GLASSD_BIN, stay=False, glassd_args=None, native=None, test_rep
loser's systemd-run fail with "already loaded" although a unit ran (review
R1 m5); if the lock cannot be had in 60 s, it goes on and treats that
error as running."""
if glassd and glassd != "none":
ensure_glassd(glassd)
try:
lk = FileLock(START_LOCK, 60).__enter__()
except (OSError, TimeoutError):
+25 -4
View File
@@ -161,12 +161,13 @@ def sync(client, only=None):
for f in sorted(p.rglob("*")):
if f.is_dir() or "__pycache__" in f.parts or f.suffix == ".pyc":
continue
# native/: sources only; builds happen on the Frame
if sub == "native" and ("build" in f.parts or f.suffix in (".o", ".so", ".png", ".jpg")
or (f.suffix == "" and f.name not in ("Makefile",))):
# native/: sources only (builds happen on the Frame), plus the shipped glassd binary
prebuilt = sub == "native" and "prebuilt" in f.parts
if sub == "native" and not prebuilt and ("build" in f.parts or f.suffix in (".o", ".so", ".png", ".jpg")
or (f.suffix == "" and f.name not in ("Makefile",))):
continue
ti = tar.gettarinfo(str(f), arcname=str(f.relative_to(ROOT)).replace("\\", "/"))
ti.mode = 0o755 if f.suffix in (".py", ".sh") or f.name == "lgs" else 0o644
ti.mode = 0o755 if f.suffix in (".py", ".sh") or f.name == "lgs" or prebuilt else 0o644
ti.uid = ti.gid = 1000
ti.uname = ti.gname = "steamos"
with open(f, "rb") as fh:
@@ -207,6 +208,21 @@ def native_build(client, fake=False):
return code
def install_glassd(client):
"""glassd for native mode: the shipped binary (native/glassd/prebuilt/glassd), then a build from
the synced sources over it. Exit code 0 when a working glassd is in place."""
d = f"{REMOTE}/native/glassd"
code, _, _ = sh(client, f"test -x {d}/prebuilt/glassd && install -m755 {d}/prebuilt/glassd {d}/glassd "
f"&& echo 'glassd: shipped binary in place'", timeout=60)
if native_build(client):
print("glassd: the build from source failed; the shipped binary stays")
code, _, _ = sh(client, f"{d}/glassd --help >/dev/null 2>&1; test -x {d}/glassd && echo 'glassd: ready ({d}/glassd)'",
timeout=60)
if code:
print("glassd: no working binary; native glass is off until `python glass.py native-build` succeeds")
return code
def lgs(client, *args):
q = " ".join(shlex.quote(a) for a in args)
code, _, _ = sh(client, f"{PY} {REMOTE}/device/lgs.py {q}", timeout=120)
@@ -1136,6 +1152,11 @@ def main(argv):
sync(c, only=rest or None)
elif cmd == "install":
sync(c)
# native glass out of the box: the shipped glassd goes in place first (the Liquid Glass toggle
# may run at once), then a build from these sources replaces it; a failed build keeps it
code = install_glassd(c)
if code:
return code
sh(c, f"install -Dm644 {REMOTE}/device/glass-shell.desktop {DESKTOP} && "
f"chmod 755 {REMOTE}/device/lgs {REMOTE}/device/lgs.py && "
f"(update-desktop-database ~/.local/share/applications >/dev/null 2>&1 || true) && "
+19 -2
View File
@@ -68,12 +68,26 @@ install_app() {
tar -xzf "$tmp/$ASSET" -C "$tmp/new"
[ -f "$tmp/new/device/lgs.py" ] || die "the archive is not a LiquidAss release"
# A running LiquidAss is stopped for the swap (its files, glassd's binary included, vanish for a moment)
# and started again after it, on the new version.
was_on=0
if command -v systemctl >/dev/null 2>&1 && systemctl --user is-active --quiet lgs-shell 2>/dev/null && [ -f "$DEST/device/lgs.py" ]; then
was_on=1
say "turning LiquidAss off for the update"
py "$DEST/device/lgs.py" off --quiet >/dev/null 2>&1 || true
fi
# Swap the whole folder so files a release dropped do not linger; keep a glassd built on this headset.
mkdir -p "$(dirname "$DEST")"
if [ -f "$DEST/native/glassd/glassd" ] && [ ! -e "$tmp/new/native/glassd/glassd" ]; then
mkdir -p "$tmp/new/native/glassd"
cp -p "$DEST/native/glassd/glassd" "$tmp/new/native/glassd/glassd"
fi
# Native glass out of the box: the release's shipped glassd (built for the Frame) when none was kept
if [ ! -e "$tmp/new/native/glassd/glassd" ] && [ -f "$tmp/new/native/glassd/prebuilt/glassd" ]; then
cp "$tmp/new/native/glassd/prebuilt/glassd" "$tmp/new/native/glassd/glassd"
chmod 755 "$tmp/new/native/glassd/glassd"
fi
rm -rf "$DEST.old"
[ -d "$DEST" ] && mv "$DEST" "$DEST.old"
mv "$tmp/new" "$DEST"
@@ -88,8 +102,11 @@ install_app() {
v=$(cat "$DEST/VERSION" 2>/dev/null || echo "")
say "installed ${v:+$v }in $DEST"
say "turn it on: dashboard bar > + > Launch Program > LiquidAss"
say "(already on? launch LiquidAss twice to load the new version)"
if [ "$was_on" = 1 ]; then
py "$DEST/device/lgs.py" on --quiet >/dev/null 2>&1 && say "LiquidAss is back on (the new version)" || say "could not turn LiquidAss back on: launch it from + > Launch Program > LiquidAss"
else
say "turn it on: dashboard bar > + > Launch Program > LiquidAss"
fi
}
uninstall_app() {
Binary file not shown.