Minimal custom Linux OS image for Looking Glass smart mirrors, targeting Raspberry Pi 4 and Pi 5.
Built with Buildroot. Boots directly into Looking Glass on the
framebuffer via cage (a single-application Wayland compositor over DRM/KMS) — no desktop
environment, no X11.
A small Go management agent (glass-agent) supervises the glass process and hosts an
HTTP API for OTA updates, log access, WiFi configuration, and config/asset/module uploads.
systemd
├── NetworkManager → WiFi / Ethernet / DHCP
├── glassos-data-dirs → ensure /data/{config,assets,modules} exist
└── glass-agent → supervisor + HTTP :80
├── NetworkManager (D-Bus) → WiFi provisioning AP + client
└── cage
└── glass run ...
All partitions are identified by filesystem label so that device paths never need to be hardcoded in config, cmdline, or RAUC bundles.
Label FS Size (Pi4/Pi5) Mount Purpose
glassos-boot FAT32 32 MB / 64 MB /boot RPi firmware, U-Boot, DTBs
glassos-kernel0 squashfs 24 MB — Kernel slot A
glassos-system0 erofs 256 MB / rootfs slot A (active)
glassos-kernel1 squashfs 24 MB — Kernel slot B (RAUC target)
glassos-system1 erofs 256 MB — rootfs slot B (RAUC target)
glassos-bootstate raw 8 MB — U-Boot A/B boot state
glassos-overlay ext4 96 MB — Writable overlay (mutable /etc, /var)
glassos-data ext4 1280 MB /data config, assets, modules (never wiped)
See Documentation/partitions.md for details.
Host packages (Ubuntu/Debian):
sudo apt-get install -y \
automake bash bc binutils bison build-essential bzip2 cpio file \
flex genext2fs genimage gettext git help2man libncurses-dev libssl-dev \
make patch perl python3 python3-setuptools rsync texinfo unzip wgetOr use the Docker build environment (no host dependencies needed beyond Docker):
make docker-build # build the glassos-builder image once
make docker-run-rpi4 # build Pi 4 image inside Docker
make docker-run-rpi5 # build Pi 5 image inside DockerThe
docker-run-*targets use a named Docker volume (glassos-output-rpi4/glassos-output-rpi5) for Buildroot's output directory. This keeps container-compiled host tools isolated from any native host build, preventingExec format errorwhen switching between native and Docker builds. The output image is compressed:sdcard.img.xz. Decompress before flashing:xz -d sdcard.img.xz.
Clone with submodules:
git clone --recurse-submodules https://github.com/glasslabs/os
# or, after cloning:
git submodule update --init# Pi 4
make build-rpi4
# Pi 5
make build-rpi5The first build downloads sources and compiles the toolchain — allow ~90 minutes. Subsequent builds with a warm cache take ~5–10 minutes.
Output image: buildroot/output/<board>/images/sdcard.img
Two Makefile variables control which glass binary is downloaded and embedded:
| Variable | Default | Description |
|---|---|---|
GLASS_VERSION |
v2.0.1 |
looking-glass release tag to download |
GLASS_VARIANT |
wayland |
Gio backend: wayland (no X11 dep), x11, or full |
Both must be kept in sync with their Buildroot counterparts in the defconfig
(BR2_PACKAGE_GLASS_VERSION, BR2_PACKAGE_GLASS_VARIANT) so that Buildroot
tracks the correct version metadata.
Override on the command line without touching any file:
make build-rpi4 GLASS_VERSION=v2.1.0 GLASS_VARIANT=waylandOr update the defaults in Makefile (and the matching BR2_PACKAGE_GLASS_*
values in buildroot-external/configs/glassos_<board>_defconfig) before
pushing a release tag so CI picks them up automatically.
GlassOS uses the
waylandvariant — it boots directly into cage (a pure Wayland compositor) with no X11 stack present, sowaylandis the correct choice. Usex11orfullonly for custom images that include an X server.
# Keep downloads across cleans
make build-rpi4 BR2_DL_DIR=/path/to/shared/dl
# Enable ccache to speed up recompilation
make build-rpi4 BR2_CCACHE_DIR=/path/to/ccache# Flash Pi 4 image to /dev/sdX
make flash BOARD=rpi4 DEV=/dev/sdX- Insert SD card, connect display and power.
- If no network connection is available,
glass-agentautomatically starts an open WiFi access point namedGlassOS-Setup. - Connect to
GlassOS-Setupfrom your phone or laptop, then POST WiFi credentials to the API (see WiFi Provisioning below). - Once the device connects to your network, the AP is automatically removed.
- Find the device IP via your router's DHCP table (hostname:
glass). - SSH in:
ssh root@<ip>(password:glass— change it). - The
glass-agentmanagement API is available on port 80.
glass-agent handles WiFi provisioning automatically via NetworkManager over D-Bus.
On startup, if NetworkManager reports no active (non-loopback) connections, glass-agent
creates and activates an open WiFi access point named GlassOS-Setup (IPv4 shared
mode, no password). You can then connect to that AP and POST credentials to the API:
curl -X POST http://192.168.4.1/network/wifi \
-H 'Content-Type: application/json' \
-d '{"ssid":"MyNetwork","password":"mypassword"}'The agent attempts to connect to the supplied network. If the connection is established within 30 seconds the AP is deactivated and removed, and any previous WiFi profile for the same device is cleaned up. If the connection fails or times out, the new profile is removed and an error is returned so you can try again.
See Documentation/wifi-provisioning.md for full details.
All endpoints are served on :80.
| Method | Path | Description |
|---|---|---|
GET |
/glass/status |
JSON: PID, uptime, restart count. |
GET |
/glass/logs |
Last 2000 lines of glass output. ?follow=true streams live. |
POST |
/glass/restart |
Restarts the Glass process. |
POST |
/glass/update |
JSON {"url":"...","sha256":"<hex>"}. Replaces /usr/lib/glass/glass and restarts. |
GET |
/glass/config |
Returns the current config.yaml. 404 if not yet written. |
POST |
/glass/config |
Upload config.yaml. Restart Glass to apply. |
POST |
/glass/secrets |
Upload secrets.yaml. Restart Glass to apply. |
GET |
/glass/assets |
JSON array of asset filenames in /data/assets/. |
GET |
/glass/assets/{name} |
Download a file from /data/assets/. |
POST |
/glass/assets/{name} |
Upload a file to /data/assets/. |
DELETE |
/glass/assets/{name} |
Delete a file from /data/assets/. |
POST |
/network/wifi |
JSON {"ssid":"...","password":"..."}. Connects to a WiFi network; removes the provisioning AP on success. |
POST |
/os/update |
JSON {"url":"..."}. Downloads and installs a RAUC bundle. Reboot to apply. |
GET |
/os/status |
RAUC slot status: active slot, versions, boot state. |
POST |
/os/reboot |
Gracefully triggers a system reboot. |
curl http://glass.local/glass/statuscurl http://glass.local/glass/logs
curl http://glass.local/glass/logs?follow=true # stream livecurl -X POST http://glass.local/glass/restart# While connected to the GlassOS-Setup AP (device IP is 192.168.4.1)
curl -X POST http://192.168.4.1/network/wifi \
-H 'Content-Type: application/json' \
-d '{"ssid":"MyNetwork","password":"mypassword"}'curl -X POST http://glass.local/glass/update \
-H 'Content-Type: application/json' \
-d '{"url":"https://github.com/glasslabs/looking-glass/releases/download/v1.2.3/glass-v1.2.3-linux-arm64-wayland.zip","sha256":"<hex>"}'curl -X POST http://glass.local/glass/config --data-binary @config.yamlcurl http://glass.local/glass/configcurl -X POST http://glass.local/glass/assets/background.jpg --data-binary @background.jpgcurl http://glass.local/glass/assetscurl -X POST http://glass.local/os/update \
-H 'Content-Type: application/json' \
-d '{"url":"https://github.com/glasslabs/os/releases/download/v1.2.3/glassos-v1.2.3-rpi4.raucb"}'
# Then reboot to apply
curl -X POST http://glass.local/os/rebootmake menuconfig-rpi4 # Buildroot packages
make linux-menuconfig-rpi4 # Kernel options
make savedefconfig-rpi4 # Save changes back to configs/
make clean-rpi4 # Remove build output for one board
make clean-all # Remove all build outputSee Documentation/adding-a-board.md for a full walkthrough. The short version:
- Create
buildroot-external/board/<board-id>/withmeta,config.txt,cmdline.txt,linux.config,genimage.cfg, andglassos-hook.sh. - Add thin
post-build.sh/post-image.shshims that delegate toscripts/post-build.sh/scripts/post-image.sh. - Copy and edit a defconfig in
buildroot-external/configs/. - Add the board ID to
BOARDSinMakefile. - Add the board to the matrix in
.github/workflows/build.ymland.github/workflows/release.yml.
| Workflow | Trigger | Output |
|---|---|---|
| build | workflow_dispatch |
sdcard.img uploaded as a workflow artifact (14 days) |
| release | Tag push | sdcard.img + signed .raucb uploaded to the GitHub release |
| agent | Push to main, PRs |
Lint + test the glass-agent Go module |
The build workflow accepts optional glass_version and glass_variant inputs
(defaulting to the Makefile values) so any combination can be tested without
changing code.
For a release, update GLASS_VERSION (and optionally GLASS_VARIANT) in
Makefile and the matching BR2_PACKAGE_GLASS_* values in the defconfigs, then
push a tag. CI uses the Makefile defaults to download the correct binary.
The RAUC_SIGNING_KEY repository secret must contain the private key matching
buildroot-external/ota/dev-ca.pem. CI writes it to a temporary file, passes it
to Buildroot via GLASSOS_RAUC_KEY, and removes it after the build. Local builds
fall back to the committed dev key automatically. The certificate expires
2026-06-23; run buildroot-external/ota/gen-dev-key.sh to regenerate it
before then.