feat(build): optional xvfb-run for Linux GUI builds
Add an --xvfb switch to 'build run' and 'job' that wraps the Linux build command in 'xvfb-run -a sh -c ...', giving GUI toolkits (GTK4/PyGObject) a headless $DISPLAY. Exported env vars precede xvfb-run so they propagate into the virtual-display session. No-op (with a notice) for Windows guests. The local-ci-build composite action exposes it as the 'xvfb' input (default false), forwarded as INPUT_XVFB -> --xvfb, mirroring the use-shared-cache/skip-artifact wiring. Docs: WORKFLOW-AUTHORING.md parameter table + Linux example note; LINUX-TEMPLATE-SETUP.md Step 4a (the template ships python3-gi gir1.2-gtk-4.0 xvfb). Adds build-run unit tests (wrap, plain, Windows no-op). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
# build-command: 'python build_plugin.py --final --dist-dir dist'
|
||||
# artifact-source: 'dist'
|
||||
# submodules: 'true'
|
||||
# # xvfb: 'true' # Linux: run the build under a headless X server
|
||||
#
|
||||
# Runner label determines the guest OS template:
|
||||
# windows-build -> WinBuild2025 (WinRM/HTTPS transport)
|
||||
@@ -190,6 +191,15 @@ inputs:
|
||||
required: false
|
||||
default: 'false'
|
||||
|
||||
xvfb:
|
||||
description: >
|
||||
Linux only. Set to "true" to run the build command under xvfb-run, a
|
||||
headless X server providing $DISPLAY for GUI toolkits (e.g. GTK4 /
|
||||
PyGObject). Requires xvfb in the Linux template (installed by
|
||||
Install-CIToolchain-Linux2404.sh). Ignored on Windows guests.
|
||||
required: false
|
||||
default: 'false'
|
||||
|
||||
outputs:
|
||||
artifact-path:
|
||||
description: >
|
||||
@@ -228,6 +238,7 @@ runs:
|
||||
INPUT_EXTRA_GUEST_ENV_JSON: ${{ inputs.extra-guest-env-json }}
|
||||
INPUT_USE_SHARED_CACHE: ${{ inputs.use-shared-cache }}
|
||||
INPUT_SKIP_ARTIFACT: ${{ inputs.skip-artifact }}
|
||||
INPUT_XVFB: ${{ inputs.xvfb }}
|
||||
INPUT_GUEST_CPU: ${{ inputs.guest-cpu }}
|
||||
INPUT_GUEST_MEMORY_MB: ${{ inputs.guest-memory-mb }}
|
||||
run: |
|
||||
@@ -347,6 +358,7 @@ runs:
|
||||
else { $pyArgs += '--use-git-clone' }
|
||||
if ($env:INPUT_USE_SHARED_CACHE -eq 'true') { $pyArgs += '--use-shared-cache' }
|
||||
if ($env:INPUT_SKIP_ARTIFACT -eq 'true') { $pyArgs += '--skip-artifact' }
|
||||
if ($env:INPUT_XVFB -eq 'true') { $pyArgs += '--xvfb' }
|
||||
# VMX overrides: forward only when > 0 (0 = keep template value).
|
||||
if ([int]($env:INPUT_GUEST_CPU) -gt 0) { $pyArgs += @('--guest-cpu', $env:INPUT_GUEST_CPU) }
|
||||
if ([int]($env:INPUT_GUEST_MEMORY_MB) -gt 0) { $pyArgs += @('--guest-memory-mb', $env:INPUT_GUEST_MEMORY_MB) }
|
||||
|
||||
@@ -229,11 +229,13 @@ Eseguito da Prepare via SSH come `ci_build` con sudo.
|
||||
Header: `#!/usr/bin/env bash` + `set -euo pipefail`
|
||||
Helper `assert_step()`: stampa `[OK] [Step N] desc` su successo, `exit 1` su fallimento.
|
||||
|
||||
- [x] **Step 1 apt update + upgrade** (saltabile con `--skip-update`)
|
||||
- [x] **Step 1 apt update (+ upgrade)**
|
||||
```bash
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get update -qq
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade -y -qq
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get update -qq # sempre
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade -y -qq # saltato da --skip-update
|
||||
```
|
||||
> `apt-get update` gira **sempre** (l'indice deve essere fresco per trovare i
|
||||
> pacchetti). `--skip-update` salta solo l'`upgrade` (lento).
|
||||
|
||||
- [x] **Step 2 Toolchain build essentials**
|
||||
```bash
|
||||
@@ -257,6 +259,15 @@ Helper `assert_step()`: stampa `[OK] [Step N] desc` su successo, `exit 1` su fal
|
||||
```
|
||||
- assert: `command -v git`, `command -v 7z`
|
||||
|
||||
- [x] **Step 4a GTK4 / GObject + Xvfb** (test GUI headless, es. PyGObject)
|
||||
```bash
|
||||
sudo apt-get install -y -qq python3-gi gir1.2-gtk-4.0 xvfb
|
||||
```
|
||||
- assert: `python3 -c "import gi"`, `command -v Xvfb`
|
||||
- Usato a runtime dai build con il flag `--xvfb` / `-Xvfb` (vedi
|
||||
[WORKFLOW-AUTHORING.md](WORKFLOW-AUTHORING.md) §5): il comando di build gira
|
||||
sotto `xvfb-run` con un `$DISPLAY` virtuale.
|
||||
|
||||
- [x] **Step 4b .NET SDK** (opzionale, flag `--dotnet`)
|
||||
```bash
|
||||
curl -fsSL https://dotnet.microsoft.com/download/dotnet/scripts/v1/dotnet-install.sh \
|
||||
|
||||
@@ -134,6 +134,7 @@ Script location (fixed on host): `N:\Code\Workspace\Local-CI-CD-System\scripts\I
|
||||
| `-GuestOS` | string | `Auto` | `Windows`, `Linux`, or `Auto` (detected from VMX guestOS field). |
|
||||
| `-BuildCommand` | string | `''` | Shell command to run inside VM. Empty = `dotnet build`. |
|
||||
| `-GuestArtifactSource` | string | `dist` | Path inside VM guest from which artifacts are collected. |
|
||||
| `-Xvfb` | switch | off | Linux only: run the build under `xvfb-run` (headless X for GUI toolkits, e.g. GTK4/PyGObject). Needs `xvfb` in the template (installed by the toolchain). Ignored for Windows. |
|
||||
| `-Submodules` | switch | off | Pass to clone with `--recurse-submodules`. |
|
||||
| `-UseGitClone` | switch | off | Skip host-side clone; let the guest VM clone directly via HTTPS. |
|
||||
| `-GiteaCredentialTarget` | string | `GiteaPAT` | Credential Manager target for Gitea PAT (UseGitClone only). |
|
||||
@@ -321,6 +322,15 @@ Notes:
|
||||
- `-SshKeyPath $env:GITEA_CI_SSH_KEY_PATH` uses the injected key path.
|
||||
- The default snapshot name for Linux templates is `BaseClean-Linux`. If you rely on
|
||||
`Auto`, add `-SnapshotName 'BaseClean-Linux'` explicitly.
|
||||
- For builds or tests that need a display (GUI toolkits such as GTK4/PyGObject),
|
||||
add the `-Xvfb` switch — the build command then runs under `xvfb-run` with a
|
||||
headless X server providing `$DISPLAY`. Example:
|
||||
```powershell
|
||||
-BuildCommand 'xvfb-aware-tests.sh' `
|
||||
-Xvfb `
|
||||
```
|
||||
The Ubuntu template ships `python3-gi`, `gir1.2-gtk-4.0` and `xvfb`
|
||||
(installed by the toolchain). `-Xvfb` is ignored for Windows guests.
|
||||
|
||||
### 8.3 Windows build (UseGitClone mode)
|
||||
|
||||
|
||||
@@ -142,6 +142,7 @@ def _linux_build(
|
||||
artifact_source: str,
|
||||
extra_env: dict[str, str],
|
||||
skip_artifact: bool,
|
||||
use_xvfb: bool = False,
|
||||
) -> None:
|
||||
click.echo("[build run] Linux build mode (SSH transport)")
|
||||
|
||||
@@ -220,13 +221,21 @@ def _linux_build(
|
||||
f"export {k}={_sh_quote(v)}; " for k, v in extra_env.items()
|
||||
)
|
||||
cmd = build_command or "make"
|
||||
# Optionally run the build under a headless X server (xvfb-run) so GUI
|
||||
# toolkits (e.g. GTK4/PyGObject) have a $DISPLAY. The exported env vars
|
||||
# precede xvfb-run so they propagate into the virtual-display session;
|
||||
# -a auto-selects a free server number. The whole `cd && build` is
|
||||
# wrapped via `sh -c` so xvfb-run drives a single command.
|
||||
inner = f"cd {_sh_quote(workdir)} && {cmd}"
|
||||
run_part = f"xvfb-run -a sh -c {_sh_quote(inner)}" if use_xvfb else inner
|
||||
# PYTHONUNBUFFERED so the guest python flushes promptly and the
|
||||
# streamed output is timely rather than emitted in one block.
|
||||
full_cmd = (
|
||||
f"{env_prefix}export PYTHONUNBUFFERED=1; "
|
||||
f"cd {_sh_quote(workdir)} && {cmd}"
|
||||
full_cmd = f"{env_prefix}export PYTHONUNBUFFERED=1; {run_part}"
|
||||
xvfb_note = " [xvfb-run]" if use_xvfb else ""
|
||||
click.echo(
|
||||
f"[build run] running build{xvfb_note} (env vars redacted): "
|
||||
f"cd {workdir} && {cmd}"
|
||||
)
|
||||
click.echo(f"[build run] running build (env vars redacted): cd {workdir} && {cmd}")
|
||||
click.echo("[build run] ----- build output (live) -----")
|
||||
result = t.run_streaming(full_cmd, check=False)
|
||||
click.echo("[build run] ----- end build output -----")
|
||||
@@ -508,6 +517,13 @@ def _windows_build(
|
||||
@click.option("--configuration", default="Release", show_default=True)
|
||||
@click.option("--skip-artifact", "skip_artifact", is_flag=True, default=False)
|
||||
@click.option("--use-shared-cache", "use_shared_cache", is_flag=True, default=False)
|
||||
@click.option(
|
||||
"--xvfb",
|
||||
"use_xvfb",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Linux only: run the build under xvfb-run (headless X for GUI toolkits).",
|
||||
)
|
||||
@click.option(
|
||||
"--extra-env",
|
||||
"extra_env",
|
||||
@@ -536,6 +552,7 @@ def build_run(
|
||||
configuration: str,
|
||||
skip_artifact: bool,
|
||||
use_shared_cache: bool,
|
||||
use_xvfb: bool,
|
||||
extra_env: tuple[str, ...],
|
||||
) -> None:
|
||||
"""Run a build inside a guest VM, optionally packaging the result.
|
||||
@@ -589,10 +606,13 @@ def build_run(
|
||||
artifact_source=guest_artifact_source,
|
||||
extra_env=extra,
|
||||
skip_artifact=skip_artifact,
|
||||
use_xvfb=use_xvfb,
|
||||
)
|
||||
except (TransportError, TransportCommandError) as exc:
|
||||
raise click.ClickException(str(exc)) from exc
|
||||
else:
|
||||
if use_xvfb:
|
||||
click.echo("[build run] --xvfb ignored for Windows guest.")
|
||||
workdir = guest_work_dir or "C:\\CI\\build"
|
||||
output_dir = guest_output_dir or "C:\\CI\\output"
|
||||
target = credential_target or config.guest_cred_target
|
||||
|
||||
@@ -327,6 +327,13 @@ def _destroy_clone(
|
||||
)
|
||||
@click.option("--use-shared-cache", "use_shared_cache", is_flag=True, default=False)
|
||||
@click.option("--skip-artifact", "skip_artifact", is_flag=True, default=False)
|
||||
@click.option(
|
||||
"--xvfb",
|
||||
"use_xvfb",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Linux only: run the build under xvfb-run (headless X for GUI toolkits).",
|
||||
)
|
||||
@click.option(
|
||||
"--gitea-credential-target",
|
||||
"gitea_credential_target",
|
||||
@@ -395,6 +402,7 @@ def job(
|
||||
use_git_clone: bool,
|
||||
use_shared_cache: bool,
|
||||
skip_artifact: bool,
|
||||
use_xvfb: bool,
|
||||
gitea_credential_target: str,
|
||||
template_path: str | None,
|
||||
snapshot_name: str,
|
||||
@@ -615,6 +623,7 @@ def job(
|
||||
artifact_source=guest_artifact_source,
|
||||
extra_env=extra_env,
|
||||
skip_artifact=skip_artifact,
|
||||
use_xvfb=use_xvfb,
|
||||
)
|
||||
else:
|
||||
_windows_build(
|
||||
|
||||
@@ -212,6 +212,83 @@ def test_build_run_linux_clone_url_happy_path(
|
||||
assert any("/work/build" in c for c in cmds)
|
||||
|
||||
|
||||
def test_build_run_linux_xvfb_wraps_command(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
"""--xvfb wraps the Linux build in xvfb-run; env exports stay outside."""
|
||||
_patch(monkeypatch)
|
||||
result = CliRunner().invoke(
|
||||
cli,
|
||||
[
|
||||
"build", "run",
|
||||
"--ip-address", "10.0.0.5",
|
||||
"--guest-os", "linux",
|
||||
"--clone-url", "https://example/repo.git",
|
||||
"--build-command", "make all",
|
||||
"--guest-linux-work-dir", "/work",
|
||||
"--extra-env", "FOO=bar",
|
||||
"--xvfb",
|
||||
"--ssh-key-path", str(tmp_path / "key"),
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
cmds = [c[0] for c in _FakeTransport.instances[0].runs]
|
||||
# Build wrapped under xvfb-run via sh -c; env export precedes it.
|
||||
assert any(
|
||||
"xvfb-run -a sh -c 'cd /work && make all'" in c
|
||||
and "export FOO=bar" in c
|
||||
for c in cmds
|
||||
)
|
||||
|
||||
|
||||
def test_build_run_linux_no_xvfb_runs_plain(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
"""Without --xvfb the build runs directly (no xvfb-run wrapper)."""
|
||||
_patch(monkeypatch)
|
||||
result = CliRunner().invoke(
|
||||
cli,
|
||||
[
|
||||
"build", "run",
|
||||
"--ip-address", "10.0.0.5",
|
||||
"--guest-os", "linux",
|
||||
"--clone-url", "https://example/repo.git",
|
||||
"--build-command", "make all",
|
||||
"--guest-linux-work-dir", "/work",
|
||||
"--ssh-key-path", str(tmp_path / "key"),
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
cmds = [c[0] for c in _FakeTransport.instances[0].runs]
|
||||
assert any("cd /work && make all" in c for c in cmds)
|
||||
assert not any("xvfb-run" in c for c in cmds)
|
||||
|
||||
|
||||
def test_build_run_xvfb_ignored_on_windows(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
"""--xvfb is a no-op (with a notice) for Windows guests."""
|
||||
_patch(monkeypatch)
|
||||
src = tmp_path / "src"
|
||||
src.mkdir()
|
||||
(src / "f.txt").write_text("x")
|
||||
result = CliRunner().invoke(
|
||||
cli,
|
||||
[
|
||||
"build", "run",
|
||||
"--ip-address", "10.0.0.9",
|
||||
"--guest-os", "windows",
|
||||
"--host-source-dir", str(src),
|
||||
"--build-command", "echo hi",
|
||||
"--xvfb",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "--xvfb ignored for Windows guest" in result.output
|
||||
cmds = [c[0] for c in _FakeTransport.instances[0].runs]
|
||||
assert not any("xvfb-run" in c for c in cmds)
|
||||
|
||||
|
||||
def test_build_run_linux_host_source_dir(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user