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
157 changes: 157 additions & 0 deletions .github/workflows/qemu-runtime.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
name: qemu-runtime

# Builds a Weston desktop image with VSCode + the launcher pulled in,
# boots it under QEMU via Yocto's testimage class, and runs OEQA test
# cases (in lib/oeqa/runtime/cases/vscode_launcher.py) that verify:
# - the postinst added the launcher block to weston.ini
# - the VSCode binary is on the target and `--version` works
# - Weston actually started on boot
# - VSCode can connect to the live wayland-0 socket and stay alive
#
# This is heavy CI (full Yocto image build, ~1-3 hours from cold
# sstate) so it runs weekly + on manual dispatch rather than per-PR.

on:
schedule:
# 03:17 UTC Sunday. Off-peak; small offset so cdn.kernel.org +
# mirrors aren't slammed alongside everyone else's nightly.
- cron: '17 3 * * 0'
workflow_dispatch:

# Don't pile up qemu runs if one is already in progress.
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
testimage:
name: testimage (scarthgap / qemux86-64)
runs-on: ubuntu-22.04
timeout-minutes: 360
steps:
- uses: actions/checkout@v4
with:
path: meta-vscode

- name: Free disk space
# Yocto builds need ~25-30 GB; the default GHA runner has ~14
# GB free on /. Reclaim ~30 GB by purging things we don't use.
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \
/opt/hostedtoolcache/CodeQL /usr/local/share/boost \
"$AGENT_TOOLSDIRECTORY"
sudo apt-get autoremove --purge -y \
azure-cli google-chrome-stable firefox powershell mongodb-* \
mysql-* postgresql-* || true
df -h

- name: Install Yocto host + qemu deps
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
gawk wget git diffstat unzip texinfo gcc build-essential chrpath \
socat cpio python3 python3-pip python3-pexpect xz-utils debianutils \
iputils-ping python3-git python3-jinja2 python3-subunit zstd liblz4-tool \
file locales libacl1 \
qemu-system-x86 qemu-utils
sudo locale-gen en_US.UTF-8
# KVM is the difference between qemu booting in seconds vs
# minutes. GHA hosted runners ship /dev/kvm; just make the
# runner user a member of the kvm group.
sudo usermod -aG kvm "$USER"
ls -la /dev/kvm || true

- name: Cache poky checkout
uses: actions/cache@v4
with:
path: poky
key: poky-scarthgap-runtime-v1
restore-keys: poky-scarthgap-

- name: Clone poky (scarthgap)
run: |
if [ ! -d poky/.git ]; then
git clone --depth 1 --branch scarthgap \
https://git.yoctoproject.org/poky poky
else
git -C poky fetch --depth 1 origin scarthgap:refs/remotes/origin/scarthgap
git -C poky checkout -B scarthgap refs/remotes/origin/scarthgap
fi

- name: Cache sstate
# GHA caches cap at 10 GB. Yocto sstate for a full Weston
# image is much larger than that, so we can't cache it whole.
# Cache what fits and let cold cells rebuild.
uses: actions/cache@v4
with:
path: sstate-cache
key: sstate-scarthgap-weston-${{ hashFiles('meta-vscode/recipes-devtools/vscode/*.bb') }}
restore-keys: |
sstate-scarthgap-weston-
sstate-scarthgap-

- name: Cache downloads
uses: actions/cache@v4
with:
path: downloads
key: downloads-scarthgap-weston-${{ hashFiles('meta-vscode/recipes-devtools/vscode/*.bb') }}
restore-keys: |
downloads-scarthgap-weston-
downloads-scarthgap-

- name: Configure build
run: |
set -eo pipefail
cd poky
source oe-init-build-env build
cat > conf/local.conf <<'EOF'
MACHINE = "qemux86-64"
DISTRO = "poky"
BB_NUMBER_THREADS = "2"
PARALLEL_MAKE = "-j 2"
CONF_VERSION = "2"
PACKAGE_CLASSES ?= "package_rpm"
SSTATE_DIR ?= "${TOPDIR}/../../sstate-cache"
DL_DIR ?= "${TOPDIR}/../../downloads"

# Weston + ssh + VSCode + the launcher into the image.
IMAGE_INSTALL:append = " vscode vscode-weston-launcher wayland-utils"
IMAGE_FEATURES:append = " ssh-server-openssh debug-tweaks"

# Wire up testimage. Empty root password (debug-tweaks) lets
# OEQA ssh in as root with no key dance.
INHERIT += "testimage"
TEST_TARGET = "qemu"
TEST_SUITES = "ping ssh vscode_launcher"
TESTIMAGE_AUTO = "1"

# Headless qemu boot.
QB_GRAPHICS = "-vga none -nographic"
EOF
bitbake-layers add-layer "$GITHUB_WORKSPACE/meta-vscode"
bitbake-layers show-layers

- name: Build core-image-weston (with testimage auto-run)
env:
LANG: en_US.UTF-8
LC_ALL: en_US.UTF-8
run: |
set -eo pipefail
cd poky
source oe-init-build-env build
# TESTIMAGE_AUTO=1 means do_testimage fires at the end of
# build automatically, so this single bitbake invocation
# both builds the image AND runs the tests.
bitbake core-image-weston

- name: Surface test results
if: always()
run: |
# OEQA's testimage emits a JUnit-style report under
# build/tmp/log/oeqa/. Surface the most recent one so the
# GHA log makes the failure obvious.
find poky/build/tmp/log/oeqa -name '*.xml' -newer poky/build/conf/local.conf \
-exec echo "==== {} ====" \; -exec cat {} \; 2>/dev/null || true
# And any captured console output.
find poky/build/tmp/log -name 'qemu_boot_log*' \
-exec echo "==== {} ====" \; -exec tail -120 {} \; 2>/dev/null || true
146 changes: 146 additions & 0 deletions lib/oeqa/runtime/cases/vscode_launcher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# OEQA runtime tests for vscode + vscode-weston-launcher.
#
# These are picked up by the `testimage` bbclass when this layer is
# enabled and TEST_SUITES contains "vscode_launcher". They run against
# a booted qemu image via ssh.

import time

from oeqa.runtime.case import OERuntimeTestCase
from oeqa.core.decorator.depends import OETestDepends


class VSCodeLauncherInstallTest(OERuntimeTestCase):
"""Validate the vscode-weston-launcher postinst actually ran on the
target and the VSCode binary itself is in place."""

def test_weston_ini_has_launcher(self):
# The postinst writes a sentinel-delimited [launcher] block
# into weston.ini. If it isn't there the installer ran on a
# target that didn't have /etc/xdg/weston/weston.ini yet (e.g.
# weston-init wasn't installed before vscode-weston-launcher),
# or the postinst failed silently.
status, out = self.target.run(
"grep meta-vscode-launcher-begin /etc/xdg/weston/weston.ini")
self.assertEqual(
status, 0,
"vscode-weston-launcher postinst didn't insert its block "
"into /etc/xdg/weston/weston.ini.\nOutput: %s" % out)

def test_vscode_binary_is_executable(self):
status, out = self.target.run(
"test -x /usr/share/vscode/bin/code "
"&& test -f /usr/share/vscode/resources/app/resources/linux/code.png")
self.assertEqual(
status, 0,
"/usr/share/vscode/bin/code or the launcher icon are not "
"present on the target.\nOutput: %s" % out)

@OETestDepends([
'vscode_launcher.VSCodeLauncherInstallTest.test_vscode_binary_is_executable',
])
def test_vscode_cli_version(self):
# CLI mode doesn't initialise Electron; it just dlopens enough
# of node to print the version triplet:
#
# 1.120.0
# 0958016b2af9f09bb4257e0df4a95e2f90590f9f
# x64
#
# This proves the binary's glibc / libstdc++ / libnss links
# are all resolved on the target.
status, out = self.target.run("/usr/share/vscode/bin/code --version")
self.assertEqual(status, 0,
"code --version exit=%d: %s" % (status, out))
lines = out.strip().splitlines()
self.assertGreaterEqual(
len(lines), 2,
"Expected at least 2 lines from --version, got: %r" % out)


class WestonStartedTest(OERuntimeTestCase):
"""Validate weston-init started Weston on boot."""

def test_weston_process_running(self):
# weston-init is socket-activated via systemd and brings weston
# up shortly after boot. Give it 30s to settle.
deadline = time.time() + 30
while time.time() < deadline:
status, _ = self.target.run("pgrep -x weston")
if status == 0:
return
time.sleep(1)
self.fail("Weston did not start within 30s of boot")

@OETestDepends([
'vscode_launcher.WestonStartedTest.test_weston_process_running',
])
def test_wayland_socket_present(self):
# XDG_RUNTIME_DIR varies (root vs the weston user); look in
# both well-known locations.
status, out = self.target.run(
"for d in /run/user/0 /run/user/1000; do "
" [ -S \"$d/wayland-0\" ] && echo \"$d\" && exit 0; "
"done; exit 1")
self.assertEqual(status, 0,
"No wayland-0 socket found on the target")


class VSCodeWaylandLaunchTest(OERuntimeTestCase):
"""Validate VSCode can talk to Weston: launch the Electron app
against the live Wayland socket, give it a few seconds to settle,
and verify the process is still alive and has the wayland socket
open."""

@OETestDepends([
'vscode_launcher.VSCodeLauncherInstallTest.test_vscode_cli_version',
'vscode_launcher.WestonStartedTest.test_wayland_socket_present',
])
def test_vscode_starts_on_wayland(self):
# Find weston's XDG_RUNTIME_DIR.
status, runtime_dir = self.target.run(
"for d in /run/user/0 /run/user/1000; do "
" [ -S \"$d/wayland-0\" ] && echo \"$d\" && exit 0; "
"done")
self.assertEqual(status, 0)
runtime_dir = runtime_dir.strip()
self.assertTrue(runtime_dir, "no wayland-0 socket found")

# Launch VSCode pointed at the Wayland socket. --no-sandbox
# because the chromium sandbox needs CAP_SYS_ADMIN which the
# test rootfs doesn't grant. user-data-dir and extensions-dir
# under /tmp so the launch doesn't try to write into
# ~/.config / ~/.vscode and trip permission errors.
launch_cmd = (
"rm -rf /tmp/vscode-test* && "
"env WAYLAND_DISPLAY=wayland-0 "
" XDG_RUNTIME_DIR=%s "
"/usr/share/vscode/bin/code "
" --no-sandbox "
" --user-data-dir=/tmp/vscode-test "
" --extensions-dir=/tmp/vscode-test-ext "
" >/tmp/vscode.log 2>&1 &" % runtime_dir)
self.target.run(launch_cmd)

# Electron/VSCode takes 10-15s to fully spin up under qemu.
time.sleep(20)

# Process still running?
status, _ = self.target.run("pgrep -f 'vscode-test'")
self.assertEqual(
status, 0,
"VSCode died within 20s of launch. Last log lines:\n%s"
% self.target.run("tail -40 /tmp/vscode.log")[1])

# Has the wayland-0 socket open in its fd table?
status, fd_list = self.target.run(
"for pid in $(pgrep -f vscode-test); do "
" ls -l /proc/$pid/fd 2>/dev/null | grep -F wayland-0 && exit 0; "
"done; exit 1")
self.assertEqual(
status, 0,
"No VSCode process has wayland-0 open. fd dump:\n%s"
% fd_list)

# Tidy up so subsequent test runs don't trip over each other.
self.target.run("pkill -9 -f vscode-test || true")
Loading