diff --git a/.devcontainer/gz-smoke/devcontainer.json b/.devcontainer/gz-smoke/devcontainer.json new file mode 100644 index 0000000..bfe9bb9 --- /dev/null +++ b/.devcontainer/gz-smoke/devcontainer.json @@ -0,0 +1,29 @@ +{ + "name": "gz-smoke", + "image": "althack/gz:jetty-base", + "remoteUser": "ros", + "updateRemoteUserUID": true, + "containerEnv": { + "GZ_VERSION": "jetty" + }, + "runArgs": [ + "--network=host", + "--ipc=host", + "--cap-add=SYS_PTRACE", + "--security-opt=seccomp:unconfined", + "--security-opt=apparmor:unconfined" + ], + "features": { + "../local-features/x11": {} + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-vscode.cpptools", + "ms-python.python", + "twxs.cmake", + "redhat.vscode-yaml" + ] + } + } +} diff --git a/.devcontainer/local-features/.gitignore b/.devcontainer/local-features/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/.devcontainer/local-features/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/.devcontainer/prepare-local-features.sh b/.devcontainer/prepare-local-features.sh new file mode 100755 index 0000000..86d2d0e --- /dev/null +++ b/.devcontainer/prepare-local-features.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +local_features_dir="${repo_root}/.devcontainer/local-features" + +mkdir -p "${local_features_dir}" +rm -rf "${local_features_dir}/x11" "${local_features_dir}/wayland" +cp -R "${repo_root}/features/src/x11" "${local_features_dir}/x11" +cp -R "${repo_root}/features/src/wayland" "${local_features_dir}/wayland" + +echo "Prepared local smoke-test features in ${local_features_dir}" diff --git a/.devcontainer/wayland-smoke/devcontainer.json b/.devcontainer/wayland-smoke/devcontainer.json new file mode 100644 index 0000000..cf0aee8 --- /dev/null +++ b/.devcontainer/wayland-smoke/devcontainer.json @@ -0,0 +1,10 @@ +{ + "name": "wayland-smoke", + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "../local-features/wayland": { + "waylandDisplay": "${localEnv:WAYLAND_DISPLAY}" + } + }, + "postCreateCommand": "sudo apt-get update && sudo apt-get install -y gtk-3-examples" +} diff --git a/.devcontainer/x11-smoke/devcontainer.json b/.devcontainer/x11-smoke/devcontainer.json new file mode 100644 index 0000000..df590c0 --- /dev/null +++ b/.devcontainer/x11-smoke/devcontainer.json @@ -0,0 +1,10 @@ +{ + "name": "x11-smoke", + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "../local-features/x11": { + "display": "${localEnv:DISPLAY}" + } + }, + "postCreateCommand": "sudo apt-get update && sudo apt-get install -y x11-apps" +} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 60a24aa..37081c9 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -59,6 +59,12 @@ jobs: - uses: actions/checkout@v6 + - name: Install devcontainer CLI + run: npm install -g @devcontainers/cli + + - name: Run real feature tests + run: bash features/test/test_all.sh + - name: Set release version id: bump run: | diff --git a/.github/workflows/test-pr.yaml b/.github/workflows/test-pr.yaml index a975c73..7232e91 100644 --- a/.github/workflows/test-pr.yaml +++ b/.github/workflows/test-pr.yaml @@ -23,7 +23,10 @@ jobs: steps: - uses: actions/checkout@v6 - - name: Validate feature metadata + - name: Install devcontainer CLI + run: npm install -g @devcontainers/cli + + - name: Run real feature tests run: bash features/test/test_all.sh gen-docs: diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 1bb7bbe..097c152 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -40,6 +40,46 @@ "Local Smoke Test GZ: Assert" ], "problemMatcher": [] - } + }, + { + "label": "Prep Local Feature tests", + "type": "shell", + "command": "bash", + "args": [ + "-lc", + "bash .devcontainer/prepare-local-features.sh" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [] + }, + { + "label": "x11 test xeyes", + "type": "shell", + "command": "xeyes", + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [] + }, + { + "label": "wayland test gtk3-demo", + "type": "shell", + "command": "gtk3-demo", + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [] + }, + { + "label": "gz test gz sim", + "type": "shell", + "command": "gz sim", + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [] + }, ] } diff --git a/README.md b/README.md index 10323b2..13b25f6 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,15 @@ Devcontainer templates repository focused on robotics and simulation. - `features/src/x11` - `features/src/wayland` +## Sample configs + +- `.devcontainer/gz-smoke/devcontainer.json` +- `.devcontainer/x11-smoke/devcontainer.json` +- `.devcontainer/wayland-smoke/devcontainer.json` + These sample devcontainers only override the dynamic display name. The features themselves now provide fixed in-container paths and default runtime env values. + Before using them locally, run `bash .devcontainer/prepare-local-features.sh` so `devcontainer up` can resolve the unpublished features from within `.devcontainer/`. + On classic Xorg hosts where the authority cookie lives outside `XDG_RUNTIME_DIR`, add a bind mount to `/tmp/devcontainer-xauthority-host`; XWayland setups are auto-detected from the mounted runtime directory. + ## Workflows - `.github/workflows/test-pr.yaml`: smoke tests template changes, validates features, and refreshes generated docs on pull requests diff --git a/features/src/wayland/README.md b/features/src/wayland/README.md index 30c1a2b..0d3dd8c 100644 --- a/features/src/wayland/README.md +++ b/features/src/wayland/README.md @@ -15,6 +15,7 @@ Configure container environment and mounts for host Wayland display forwarding. | Options Id | Description | Type | Default Value | |-----|-----|-----|-----| +| waylandDisplay | Set WAYLAND_DISPLAY for Wayland clients launched from the container. | string | wayland-0 | | softwareGL | Set LIBGL_ALWAYS_SOFTWARE for GPU compatibility. | string | 1 | diff --git a/features/src/wayland/devcontainer-feature.json b/features/src/wayland/devcontainer-feature.json index c718490..e42c108 100644 --- a/features/src/wayland/devcontainer-feature.json +++ b/features/src/wayland/devcontainer-feature.json @@ -6,6 +6,11 @@ "documentationURL": "https://github.com/athackst/devcontainer-templates/tree/main/features/src/wayland", "licenseURL": "https://github.com/athackst/devcontainer-templates/blob/main/LICENSE", "options": { + "waylandDisplay": { + "type": "string", + "default": "wayland-0", + "description": "Set WAYLAND_DISPLAY for Wayland clients launched from the container." + }, "softwareGL": { "type": "string", "default": "1", @@ -17,13 +22,11 @@ } }, "containerEnv": { - "WAYLAND_DISPLAY": "${localEnv:WAYLAND_DISPLAY}", - "XDG_RUNTIME_DIR": "${localEnv:XDG_RUNTIME_DIR}", - "PULSE_SERVER": "${localEnv:PULSE_SERVER}", - "LIBGL_ALWAYS_SOFTWARE": "${feature:softwareGL}" + "XDG_RUNTIME_DIR": "/tmp/devcontainer-wayland-runtime", + "PULSE_SERVER": "unix:/tmp/devcontainer-wayland-runtime/pulse-native" }, "mounts": [ - "source=${localEnv:XDG_RUNTIME_DIR},target=${localEnv:XDG_RUNTIME_DIR},type=bind" + "source=${localEnv:XDG_RUNTIME_DIR},target=/tmp/devcontainer-wayland-runtime,type=bind" ], "installsAfter": [ "ghcr.io/devcontainers/features/common-utils" diff --git a/features/src/wayland/install.sh b/features/src/wayland/install.sh index 51daee1..380654c 100755 --- a/features/src/wayland/install.sh +++ b/features/src/wayland/install.sh @@ -1,6 +1,14 @@ #!/usr/bin/env bash set -euo pipefail -# Runtime forwarding is configured through feature metadata (containerEnv/mounts). -# Keep install lightweight and deterministic. +wayland_display="${WAYLANDDISPLAY:-wayland-0}" +software_gl="${SOFTWAREGL:-1}" + +cat >/etc/profile.d/devcontainer-wayland-gui.sh </etc/profile.d/devcontainer-x11-gui.sh </dev/null 2>&1; then + echo "devcontainer CLI is required. Install it with 'npm install -g @devcontainers/cli'." >&2 + exit 1 +fi + +cleanup_paths=() + +cleanup() { + for path in "${cleanup_paths[@]}"; do + if [[ -d "${path}" ]]; then + rm -rf "${path}" + elif [[ -f "${path}" ]]; then + rm -f "${path}" + fi + done +} + +trap cleanup EXIT + +mkdir -p /tmp/.X11-unix + +export DISPLAY="${FEATURE_TEST_DISPLAY:-:99}" +export XDG_RUNTIME_DIR="${FEATURE_TEST_XDG_RUNTIME_DIR:-/tmp/devcontainers-wayland-runtime}" +export XAUTHORITY="${FEATURE_TEST_XAUTHORITY:-/tmp/devcontainers-xauthority.test}" +export WAYLAND_DISPLAY="${FEATURE_TEST_WAYLAND_DISPLAY:-wayland-host}" +export PULSE_SERVER="${FEATURE_TEST_PULSE_SERVER:-unix:${XDG_RUNTIME_DIR}/pulse-native}" +xwayland_xauthority="${FEATURE_TEST_XWAYLAND_XAUTHORITY:-${XDG_RUNTIME_DIR}/.mutter-Xwaylandauth.test}" + +cleanup_paths+=("${XAUTHORITY}" "${XDG_RUNTIME_DIR}") + +mkdir -p "$(dirname "${XAUTHORITY}")" +mkdir -p "${XDG_RUNTIME_DIR}" +chmod 700 "${XDG_RUNTIME_DIR}" +touch "${XAUTHORITY}" +touch "${xwayland_xauthority}" +touch "${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY}" +touch "${XDG_RUNTIME_DIR}/wayland-0" +touch "${XDG_RUNTIME_DIR}/pulse-native" + +for feature in x11 wayland; do + devcontainer features test \ + --base-image "${base_image}" \ + --features "${feature}" \ + --project-folder "${features_root}" done diff --git a/features/test/wayland/host_forwarding_env.sh b/features/test/wayland/host_forwarding_env.sh new file mode 100755 index 0000000..c95dde0 --- /dev/null +++ b/features/test/wayland/host_forwarding_env.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -e + +source dev-container-features-test-lib + +check "WAYLAND_DISPLAY can be overridden from feature option" bash -lc '[ "${WAYLAND_DISPLAY:-}" = "wayland-host" ]' +check "XDG_RUNTIME_DIR stays on fixed path" bash -c '[ "${XDG_RUNTIME_DIR:-}" = "/tmp/devcontainer-wayland-runtime" ]' +check "PULSE_SERVER stays on fixed path" bash -c '[ "${PULSE_SERVER:-}" = "unix:/tmp/devcontainer-wayland-runtime/pulse-native" ]' +check "software rendering defaults through shell init" bash -lc '[ "${LIBGL_ALWAYS_SOFTWARE:-}" = "1" ]' +check "wayland socket placeholder exists" bash -c '[ -e "/tmp/devcontainer-wayland-runtime/wayland-host" ]' +check "runtime directory is mounted" mountpoint -q /tmp/devcontainer-wayland-runtime + +reportResults diff --git a/features/test/wayland/scenarios.json b/features/test/wayland/scenarios.json new file mode 100644 index 0000000..a127788 --- /dev/null +++ b/features/test/wayland/scenarios.json @@ -0,0 +1,18 @@ +{ + "host_forwarding_env": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "wayland": { + "waylandDisplay": "${localEnv:WAYLAND_DISPLAY}" + } + } + }, + "software_gl_disabled": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "wayland": { + "softwareGL": "0" + } + } + } +} diff --git a/features/test/wayland/software_gl_disabled.sh b/features/test/wayland/software_gl_disabled.sh new file mode 100755 index 0000000..1b0259a --- /dev/null +++ b/features/test/wayland/software_gl_disabled.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -e + +source dev-container-features-test-lib + +check "software rendering can be disabled through shell init" bash -lc '[ "${LIBGL_ALWAYS_SOFTWARE:-}" = "0" ]' +check "runtime directory is mounted" mountpoint -q /tmp/devcontainer-wayland-runtime + +reportResults diff --git a/features/test/wayland/test.sh b/features/test/wayland/test.sh index 058c89f..7fed5be 100755 --- a/features/test/wayland/test.sh +++ b/features/test/wayland/test.sh @@ -1,25 +1,15 @@ #!/usr/bin/env bash -set -euo pipefail +set -e -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" -feature_dir="${repo_root}/features/src/wayland" -feature_metadata="${feature_dir}/devcontainer-feature.json" -feature_install="${feature_dir}/install.sh" -feature_readme="${feature_dir}/README.md" +source dev-container-features-test-lib -test -f "${feature_metadata}" -test -f "${feature_install}" -test -f "${feature_readme}" +check "XDG_RUNTIME_DIR defaults to fixed path" bash -c '[ "${XDG_RUNTIME_DIR:-}" = "/tmp/devcontainer-wayland-runtime" ]' +check "WAYLAND_DISPLAY defaults to wayland-0" bash -lc '[ "${WAYLAND_DISPLAY:-}" = "wayland-0" ]' +check "PULSE_SERVER defaults to mounted runtime path" bash -c '[ "${PULSE_SERVER:-}" = "unix:/tmp/devcontainer-wayland-runtime/pulse-native" ]' +check "software rendering defaults through shell init" bash -lc '[ "${LIBGL_ALWAYS_SOFTWARE:-}" = "1" ]' +check "runtime directory mount exists" bash -c '[ -d "/tmp/devcontainer-wayland-runtime" ]' +check "runtime directory is mounted" mountpoint -q /tmp/devcontainer-wayland-runtime +check "default wayland socket exists" bash -c '[ -e "/tmp/devcontainer-wayland-runtime/wayland-0" ]' +check "pulse socket exists" bash -c '[ -e "/tmp/devcontainer-wayland-runtime/pulse-native" ]' -jq -e '.id == "wayland"' "${feature_metadata}" >/dev/null -jq -e '.name == "Wayland GUI Forwarding"' "${feature_metadata}" >/dev/null -jq -e '.options.softwareGL.default == "1"' "${feature_metadata}" >/dev/null -jq -e '.containerEnv.WAYLAND_DISPLAY == "${localEnv:WAYLAND_DISPLAY}"' "${feature_metadata}" >/dev/null -jq -e '.containerEnv.XDG_RUNTIME_DIR == "${localEnv:XDG_RUNTIME_DIR}"' "${feature_metadata}" >/dev/null -jq -e '.containerEnv.PULSE_SERVER == "${localEnv:PULSE_SERVER}"' "${feature_metadata}" >/dev/null -jq -e '.containerEnv.LIBGL_ALWAYS_SOFTWARE == "${feature:softwareGL}"' "${feature_metadata}" >/dev/null -jq -e '.mounts | index("source=${localEnv:XDG_RUNTIME_DIR},target=${localEnv:XDG_RUNTIME_DIR},type=bind") != null' "${feature_metadata}" >/dev/null -jq -e '.containerEnv.DISPLAY == null' "${feature_metadata}" >/dev/null -jq -e '.containerEnv.XAUTHORITY == null' "${feature_metadata}" >/dev/null - -grep -q 'Wayland feature metadata configured.' "${feature_install}" +reportResults diff --git a/features/test/x11/host_forwarding_env.sh b/features/test/x11/host_forwarding_env.sh new file mode 100755 index 0000000..f2ea4a8 --- /dev/null +++ b/features/test/x11/host_forwarding_env.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -e + +source dev-container-features-test-lib + +check "DISPLAY can be overridden from feature option" bash -lc '[ "${DISPLAY:-}" = ":99" ]' +check "XAUTHORITY stays on fixed path" bash -c '[ "${XAUTHORITY:-}" = "/tmp/devcontainer-xauthority" ]' +check "PULSE_SERVER stays on mounted runtime path" bash -c '[ "${PULSE_SERVER:-}" = "unix:/tmp/devcontainer-host-runtime/pulse-native" ]' +check "software rendering defaults through shell init" bash -lc '[ "${LIBGL_ALWAYS_SOFTWARE:-}" = "1" ]' +check "Qt uses the X11 backend by default" bash -lc '[ "${QT_QPA_PLATFORM:-}" = "xcb" ]' +check "x11 socket directory is mounted" mountpoint -q /tmp/.X11-unix +check "runtime directory is mounted" mountpoint -q /tmp/devcontainer-host-runtime +check "xauthority file resolves after shell init" bash -lc '[ -e /tmp/devcontainer-xauthority ]' +check "xauthority prefers explicit host file mount" bash -lc '[ "$(readlink /tmp/devcontainer-xauthority)" = "/tmp/devcontainer-xauthority-host" ]' +check "pulse socket file is reachable through runtime mount" bash -c '[ -e /tmp/devcontainer-host-runtime/pulse-native ]' + +reportResults diff --git a/features/test/x11/scenarios.json b/features/test/x11/scenarios.json new file mode 100644 index 0000000..0ecb72d --- /dev/null +++ b/features/test/x11/scenarios.json @@ -0,0 +1,21 @@ +{ + "host_forwarding_env": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "mounts": [ + "source=${localEnv:XAUTHORITY},target=/tmp/devcontainer-xauthority-host,type=bind,readonly" + ], + "features": { + "x11": { + "display": "${localEnv:DISPLAY}" + } + } + }, + "software_gl_disabled": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "x11": { + "softwareGL": "0" + } + } + } +} diff --git a/features/test/x11/software_gl_disabled.sh b/features/test/x11/software_gl_disabled.sh new file mode 100755 index 0000000..581ae03 --- /dev/null +++ b/features/test/x11/software_gl_disabled.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -e + +source dev-container-features-test-lib + +check "software rendering can be disabled through shell init" bash -lc '[ "${LIBGL_ALWAYS_SOFTWARE:-}" = "0" ]' +check "Qt uses the X11 backend by default" bash -lc '[ "${QT_QPA_PLATFORM:-}" = "xcb" ]' +check "x11 socket directory is mounted" mountpoint -q /tmp/.X11-unix +check "runtime directory is mounted" mountpoint -q /tmp/devcontainer-host-runtime +check "xauthority file resolves after shell init" bash -lc '[ -e /tmp/devcontainer-xauthority ]' +check "pulse socket file is reachable through runtime mount" bash -c '[ -e /tmp/devcontainer-host-runtime/pulse-native ]' + +reportResults diff --git a/features/test/x11/test.sh b/features/test/x11/test.sh index b5d09b1..77aa324 100755 --- a/features/test/x11/test.sh +++ b/features/test/x11/test.sh @@ -1,25 +1,18 @@ #!/usr/bin/env bash -set -euo pipefail +set -e -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" -feature_dir="${repo_root}/features/src/x11" -feature_metadata="${feature_dir}/devcontainer-feature.json" -feature_install="${feature_dir}/install.sh" -feature_readme="${feature_dir}/README.md" +source dev-container-features-test-lib -test -f "${feature_metadata}" -test -f "${feature_install}" -test -f "${feature_readme}" +check "DISPLAY defaults to :0" bash -lc '[ "${DISPLAY:-}" = ":0" ]' +check "XAUTHORITY is set to fixed path" bash -c '[ "${XAUTHORITY:-}" = "/tmp/devcontainer-xauthority" ]' +check "PULSE_SERVER is set to mounted runtime path" bash -c '[ "${PULSE_SERVER:-}" = "unix:/tmp/devcontainer-host-runtime/pulse-native" ]' +check "software rendering defaults through shell init" bash -lc '[ "${LIBGL_ALWAYS_SOFTWARE:-}" = "1" ]' +check "Qt uses the X11 backend by default" bash -lc '[ "${QT_QPA_PLATFORM:-}" = "xcb" ]' +check "x11 socket directory exists" bash -c '[ -d /tmp/.X11-unix ]' +check "x11 socket directory is mounted" mountpoint -q /tmp/.X11-unix +check "runtime directory is mounted" mountpoint -q /tmp/devcontainer-host-runtime +check "xauthority file resolves after shell init" bash -lc '[ -e /tmp/devcontainer-xauthority ]' +check "xauthority falls back to XWayland runtime auth" bash -lc '[ "$(readlink /tmp/devcontainer-xauthority)" = "/tmp/devcontainer-host-runtime/.mutter-Xwaylandauth.test" ]' +check "pulse socket file is reachable through runtime mount" bash -c '[ -e /tmp/devcontainer-host-runtime/pulse-native ]' -jq -e '.id == "x11"' "${feature_metadata}" >/dev/null -jq -e '.name == "X11 GUI Forwarding"' "${feature_metadata}" >/dev/null -jq -e '.options.softwareGL.default == "1"' "${feature_metadata}" >/dev/null -jq -e '.containerEnv.DISPLAY == "${localEnv:DISPLAY}"' "${feature_metadata}" >/dev/null -jq -e '.containerEnv.XAUTHORITY == "${localEnv:XAUTHORITY}"' "${feature_metadata}" >/dev/null -jq -e '.containerEnv.PULSE_SERVER == "${localEnv:PULSE_SERVER}"' "${feature_metadata}" >/dev/null -jq -e '.containerEnv.LIBGL_ALWAYS_SOFTWARE == "${feature:softwareGL}"' "${feature_metadata}" >/dev/null -jq -e '.mounts | index("source=/tmp/.X11-unix,target=/tmp/.X11-unix,type=bind") != null' "${feature_metadata}" >/dev/null -jq -e '.containerEnv.WAYLAND_DISPLAY == null' "${feature_metadata}" >/dev/null -jq -e '.containerEnv.XDG_RUNTIME_DIR == null' "${feature_metadata}" >/dev/null - -grep -q 'X11 feature metadata configured.' "${feature_install}" +reportResults