diff --git a/.gitea/actions/local-ci-build/action.yml b/.gitea/actions/local-ci-build/action.yml index 3e9b990..6746986 100644 --- a/.gitea/actions/local-ci-build/action.yml +++ b/.gitea/actions/local-ci-build/action.yml @@ -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) } diff --git a/docs/LINUX-TEMPLATE-SETUP.md b/docs/LINUX-TEMPLATE-SETUP.md index cc2b69d..43cf119 100644 --- a/docs/LINUX-TEMPLATE-SETUP.md +++ b/docs/LINUX-TEMPLATE-SETUP.md @@ -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 \ diff --git a/docs/WORKFLOW-AUTHORING.md b/docs/WORKFLOW-AUTHORING.md index 7432b3b..a546a52 100644 --- a/docs/WORKFLOW-AUTHORING.md +++ b/docs/WORKFLOW-AUTHORING.md @@ -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) diff --git a/src/ci_orchestrator/commands/build.py b/src/ci_orchestrator/commands/build.py index fd4319d..1ed7dfa 100644 --- a/src/ci_orchestrator/commands/build.py +++ b/src/ci_orchestrator/commands/build.py @@ -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 diff --git a/src/ci_orchestrator/commands/job.py b/src/ci_orchestrator/commands/job.py index 162841f..a8d4f3e 100644 --- a/src/ci_orchestrator/commands/job.py +++ b/src/ci_orchestrator/commands/job.py @@ -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( diff --git a/tests/python/test_commands_build.py b/tests/python/test_commands_build.py index 3c1edf8..ed191bc 100644 --- a/tests/python/test_commands_build.py +++ b/tests/python/test_commands_build.py @@ -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: