Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 75 additions & 5 deletions .github/workflows/build-iso.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,16 @@ name: Build images
#
# The workflow can also be run by hand from the Actions tab, which uploads the
# images as build artifacts without touching releases.
#
# Pull requests run the `extension` job and nothing else. It takes a couple of
# minutes and is the only part of a build that can fail on a code mistake rather
# than on a mirror or a runner; the image jobs are two-hour builds that produce
# gigabyte artifacts, and gating every PR on those would buy very little.
on:
push:
tags:
- 'v*'
pull_request:
workflow_dispatch:
inputs:
targets:
Expand All @@ -31,11 +37,16 @@ permissions:

concurrency:
group: build-images-${{ github.ref }}
cancel-in-progress: false
# A superseded PR check is wasted minutes, but a release build must never be
# cancelled out from under a tag.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
version:
name: Resolve version
# Nothing a pull request runs needs a version, and a check that only ever
# echoes one is noise on the PR.
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
outputs:
version: ${{ steps.v.outputs.version }}
Expand All @@ -50,15 +61,59 @@ jobs:
echo "version=${version}" >> "$GITHUB_OUTPUT"
echo "Building VS Code OS ${version}"

# The desktop shell is architecture-neutral JavaScript, so it is built once and
# both image jobs consume the same artifact. Building it inside each image job
# would mean two Node toolchains - pacman's in an Arch container, and
# setup-node's on an arm64 Ubuntu runner - producing the same bytes twice.
extension:
name: Build VsCodeOsCore
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7

- uses: actions/setup-node@v6
with:
node-version: '24'
cache: npm
cache-dependency-path: extension/package-lock.json

- name: Install
run: npm ci --prefix extension --no-audit --no-fund

- name: Typecheck
run: npm run --prefix extension typecheck

- name: Test
run: npm run --prefix extension test

- name: Build
run: ./scripts/build-extension.sh -o out/extension

- uses: actions/upload-artifact@v7
with:
name: vscodeos-core-extension
path: out/extension
retention-days: 14

x86_64:
name: x86-64 ISO
needs: version
if: github.event_name != 'workflow_dispatch' || contains(fromJSON('["both","x86_64"]'), github.event.inputs.targets)
needs: [version, extension]
if: >-
github.event_name != 'pull_request'
&& (github.event_name != 'workflow_dispatch'
|| contains(fromJSON('["both","x86_64"]'), github.event.inputs.targets))
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- uses: actions/checkout@v7

- name: Fetch the prebuilt extension
uses: actions/download-artifact@v8
with:
name: vscodeos-core-extension
path: extension-dist

- name: Free up disk space
run: |
echo "before:"; df -h / /mnt | tail -2
Expand Down Expand Up @@ -90,6 +145,7 @@ jobs:
-w /build \
-e ISO_VERSION="${BUILD_VERSION}" \
-e VSCODE_VERSION="${VSCODE_VERSION}" \
-e VSCODEOS_EXTENSION_PREBUILT=/build/extension-dist \
archlinux:latest \
bash -euo pipefail -c '
# The keyring goes first and on its own: a base image older than
Expand Down Expand Up @@ -156,15 +212,24 @@ jobs:

raspberrypi:
name: Raspberry Pi image
needs: version
if: github.event_name != 'workflow_dispatch' || contains(fromJSON('["both","raspberrypi"]'), github.event.inputs.targets)
needs: [version, extension]
if: >-
github.event_name != 'pull_request'
&& (github.event_name != 'workflow_dispatch'
|| contains(fromJSON('["both","raspberrypi"]'), github.event.inputs.targets))
# A native aarch64 runner means the build can chroot into the image
# directly instead of emulating every command through qemu.
runs-on: ubuntu-24.04-arm
timeout-minutes: 120
steps:
- uses: actions/checkout@v7

- name: Fetch the prebuilt extension
uses: actions/download-artifact@v8
with:
name: vscodeos-core-extension
path: extension-dist

- name: Install image tooling
run: |
sudo apt-get update -qq
Expand All @@ -176,6 +241,7 @@ jobs:
env:
BUILD_VERSION: ${{ needs.version.outputs.version }}
VSCODE_VERSION: ${{ github.event.inputs.vscode_version || 'latest' }}
VSCODEOS_EXTENSION_PREBUILT: ${{ github.workspace }}/extension-dist
run: sudo -E ./rpi/build-image.sh -v "${BUILD_VERSION}" -o "${PWD}/out" -w /mnt/vscodeos-rpi

- name: Summarise
Expand Down Expand Up @@ -219,6 +285,10 @@ jobs:
steps:
- uses: actions/download-artifact@v8
with:
# Images only. Without the filter the extension bundle would be
# downloaded too, and `gh release create artifacts/*` below would
# publish it as a release asset.
pattern: VSCodeOS-*
path: artifacts
merge-multiple: true

Expand Down
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@
# pacman sync databases, dropped in the working tree by a build run from here
*.db

# Extension build output. The sources are committed; the bundles are not.
/extension/node_modules/
/extension/dist/
/extension/out/
/extension/media/dist/
/extension-dist/

# Editor / OS noise
.DS_Store
*.swp
99 changes: 92 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
A Linux distribution whose entire user interface is Visual Studio Code.

It is a minimal Arch Linux base with the official Microsoft build of VS Code
layered on top. There is no desktop environment, no taskbar and no application
menu: the machine boots, logs in and puts the editor on screen fullscreen, and
that is the whole system. Everything else — package management, networking,
git, compilers — is reached through the editor's integrated terminal.
layered on top. There is no desktop environment and no application menu: the
machine boots, logs in and puts the editor on screen fullscreen, and that is the
whole system. What a desktop would normally give you — a tray with a clock and a
power button, a task manager, a file manager, a browser, a handful of small apps
— is supplied by **VsCodeOsCore**, an extension built into the editor itself.

```
power on
Expand All @@ -30,6 +31,8 @@ Two images are published for every release, sharing the same kiosk:
| **Base** | Arch Linux, built with `archiso` | Arch Linux ARM |
| **Editor** | Official VS Code, `linux-x64` | Official VS Code, `linux-arm64` |
| **Session** | Xorg + Openbox kiosk — no panel, no launcher, no desktop | same |
| **Shell** | VsCodeOsCore, built into the editor | same |
| **Browser** | Chromium | Chromium |
| **Toolchain** | git, git-lfs, Node.js, Python, base-devel, Docker | git, Node.js, Python, base-devel |
| **Boot** | UEFI (x64 and ia32) and legacy BIOS, one hybrid image | Pi firmware from a FAT partition |
| **Getting it onto a machine** | live medium + `vscodeos-install` | flash the image; it *is* the system |
Expand All @@ -51,6 +54,48 @@ and Arch Linux ARM repositories as they stand on the day of the release, so
each release is a current system rather than a frozen one. The `.packages.txt`
file published next to each image lists exactly which versions it shipped.

### The desktop shell

The editor has no menu bar and no window controls, so everything a desktop needs
lives in **VsCodeOsCore** — a VS Code extension that ships *inside* the editor
rather than being installed from the Marketplace. Its source is in
[`extension/`](extension/), and it adds:

- **A tray**, at the right end of the status bar. Left to right: now playing,
battery, volume, network, the clock and date, and the power button in the
corner. Each one opens a flyout in the bottom panel — a Windows-style card
that rises directly above the item you clicked.
- **Power** — sleep, restart, shut down and log out, with a confirmation.
- **Calendar** — a month grid with today highlighted, on the clock.
- **Quick settings** — Wi-Fi, Bluetooth, airplane mode, energy saver, night
light and accessibility, plus brightness and volume sliders. Tiles hide
themselves when the hardware is not there, rather than showing a dead switch.
- **Network** — scan, connect with a password, and switch between saved
connections.
- **Task Manager**, in the activity bar — processes with CPU and memory, per-core
meters, load average, uptime and CPU temperature, sortable and filterable, with
End task.
- **Files** — a graphical file explorer with a places sidebar, grid and list
views, rename, trash, copy and paste. Text opens in the editor; everything else
goes to `xdg-open`.
- **Music** — transport controls for whatever is playing, over MPRIS, plus
one-click launchers that open Spotify Web and YouTube Music as their own
browser windows.
- **Apps** — Calculator, Notepad, Paint, Screenshot and Voice Recorder. `VS Code
OS: All Apps…` in the command palette (**Ctrl** + **Shift** + **P**) lists
everything.

Two honest limits, both imposed by VS Code rather than by this project:
**Spotify audio cannot play inside the editor** (VS Code's Electron ships no
Widevine, so the Web Playback SDK cannot work — which is exactly why the player
controls a real browser window instead), and **Microsoft Edge is not on either
image** (it is AUR-only on Arch, and Microsoft publishes no ARM64 Linux build at
all, so the Pi could never have matched). The browser launcher prefers
`microsoft-edge-stable` if you install it yourself, and falls back to Chromium.

Every part of the shell can be turned off individually in settings under
`vscodeos.*`; [`extension/README.md`](extension/README.md) has the details.

### Supported Raspberry Pi models

64-bit boards only: **Pi 5, Pi 4, Pi 400, CM4, Pi 3/3+ and Zero 2 W**. A 4 GB
Expand Down Expand Up @@ -154,13 +199,21 @@ VSCODEOS_RESPAWN=1 # 0 = do not relaunch when VS Code exits
```bash
sudo pacman -Syu # update the Arch base
sudo vscodeos-update-code # update VS Code itself (it is not a pacman package)
nmtui # join a Wi-Fi network
nmtui # join a Wi-Fi network (or use the tray)
code ~/Projects/thing # open something in the running editor
```

Extensions, settings sync and Marketplace sign-in all work normally;
`gnome-keyring` is started by the session so credentials persist.

`vscodeos-update-code` replaces the whole editor tree, which is where the shell
lives, so it reinstalls VsCodeOsCore afterwards. To do that by hand — say after
unpacking a VS Code build yourself:

```bash
sudo vscodeos-install-extensions --force
```

## Building the images

### With GitHub Actions
Expand All @@ -183,15 +236,26 @@ where you can pick one target or both and pin a specific VS Code version. A
manual run uploads the images as build artifacts and does not create a
release.

Pull requests run the desktop shell's typecheck, tests and bundle build, and
nothing else — a couple of minutes, and the only part of a build that fails on
a code mistake rather than on a mirror or a runner. The images themselves are
two-hour builds producing gigabyte artifacts, so they stay on tags and manual
runs.

### Locally

Both builds compile the desktop shell first, so a local build needs **Node.js**
on the machine doing the building — `pacman -S nodejs npm`, or whatever your
distribution calls it. The container one-liner below installs it itself. Pass
`VSCODEOS_SKIP_EXTENSION=1` to leave the shell out and build the bare kiosk.

**x86-64 ISO** — needs Docker (or an Arch host with `archiso`) and roughly
20 GB of free disk space:

```bash
docker run --rm --privileged --pull always -v "$PWD:/build" -w /build archlinux:latest \
bash -c 'pacman -Sy --noconfirm --needed archlinux-keyring &&
pacman -Syu --noconfirm --needed archiso git grub &&
pacman -Syu --noconfirm --needed archiso git grub nodejs npm &&
./scripts/build-iso.sh -v 1.0.0'
```

Expand All @@ -203,7 +267,7 @@ qemu-system-x86_64 -m 4G -enable-kvm -cdrom out/VSCodeOS-1.0.0-x86_64.iso
laptop, an ARM VM), because the build chroots into the image it is assembling:

```bash
sudo apt-get install -y libarchive-tools dosfstools e2fsprogs xz-utils util-linux
sudo apt-get install -y libarchive-tools dosfstools e2fsprogs xz-utils util-linux nodejs npm
sudo ./rpi/build-image.sh -v 1.0.0
```

Expand All @@ -223,15 +287,23 @@ stack — when it finishes.
## How the repository is laid out

```
extension/ VsCodeOsCore, the desktop shell
src/ extension host: sys/, statusbar/, views/, apps/
media/ one webview bundle per page, plus the stylesheet
(see extension/README.md)

rootfs-common/ the kiosk, shared by both images
etc/passwd, group, shadow the kiosk account (uid 1000)
etc/systemd/system/ autologin on tty1, enabled services
etc/X11/xorg.conf.d/ kiosk hardening (DontVTSwitch, DontZap)
etc/polkit-1/rules.d/ power and NetworkManager without a password
etc/udev/rules.d/ backlight writable by the `video` group
etc/default/vscodeos kiosk settings
etc/skel/ the kiosk user's home: .xinitrc, openbox
rules, VS Code settings and keybindings
usr/local/bin/vscodeos-kiosk the session supervisor
usr/local/bin/vscodeos-update-code
usr/local/bin/vscodeos-install-extensions

archiso/ x86-64 only
profiledef.sh archiso profile: image name, boot modes
Expand All @@ -251,6 +323,7 @@ rpi/ Raspberry Pi only
scripts/
build-iso.sh assembles the profile and runs mkarchiso
fetch-vscode.sh downloads and stages VS Code (x64 or arm64)
build-extension.sh bundles VsCodeOsCore for both images
pkg-versions.sh summarises a build's package manifest
.github/workflows/build-iso.yml tag -> both images -> one GitHub release
```
Expand All @@ -260,6 +333,18 @@ Details worth knowing if you are modifying it:
- **The kiosk is shared, the plumbing is not.** Everything in `rootfs-common/`
is copied into both images; anything architecture-specific lives in
`archiso/airootfs/` or `rpi/overlay/`.
- **The shell is a built-in extension, not a Marketplace one.** Both builds stage
it at `/usr/share/vscodeos/extensions/`, then `vscodeos-install-extensions`
copies it into `/opt/visual-studio-code/resources/app/extensions/`. That
directory is a plain scan — no `extensions.json`, no version check — so a
VS Code update can never decide the desktop is incompatible and disable it. It
does, however, replace the whole tree, which is why `vscodeos-update-code`
re-runs the installer.
- **The extension is built once, for both images.** It is architecture-neutral
JavaScript, so CI has a separate `extension` job and passes the result to both
image jobs via `VSCODEOS_EXTENSION_PREBUILT`. Local builds compile it on the
spot and cache the result; `VSCODEOS_SKIP_EXTENSION=1` leaves it out when you
are only iterating on the OS.
- **Boot menus are not vendored.** `build-iso.sh` copies `syslinux/`, `grub/`
and `efiboot/` out of the `archiso` package installed in the build
environment and rebrands the labels, so the boot configuration always matches
Expand Down
24 changes: 24 additions & 0 deletions archiso/packages.x86_64
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,23 @@ openssh
wget
curl
ca-certificates
# The quick-settings Bluetooth tile hides itself when there is no adapter, but
# without bluez there is no bluetoothctl to ask in the first place.
bluez
bluez-utils

# Web browser.
#
# Chromium rather than Microsoft Edge: Edge is AUR-only on Arch, so neither
# build's `pacman -S` flow can install it, and Microsoft publishes no ARM64
# Linux build at all - the Pi image could never have matched. VsCodeOsCore
# prefers microsoft-edge-stable at run time, so installing it from the AUR is
# enough to make every launcher in the shell use it instead.
#
# This is the single largest thing on this list after the kernel and VS Code
# itself (~600 MiB installed, ~180 MiB once squashfs has had it), so it is the
# first candidate to drop if the ISO ever creeps back over the 2 GiB limit.
chromium

# Filesystems / storage helpers
gvfs
Expand Down Expand Up @@ -89,6 +106,9 @@ vulkan-radeon
vulkan-swrast
xdg-utils
xdg-user-dirs
# Screen capture for the shell's screenshot tool. Nothing inside a VS Code
# webview can read the screen, so this has to be a real X client.
scrot

# Fonts
ttf-dejavu
Expand All @@ -105,6 +125,10 @@ pipewire-alsa
pipewire-pulse
wireplumber
alsa-utils
# MPRIS transport control for the shell's music player. Chromium exports MPRIS
# for whatever is playing in it, which is how Spotify Web and YouTube Music end
# up controllable from the status bar.
playerctl

# Input / laptop bits
libinput
Expand Down
Loading