diff --git a/.devcontainer/gz-smoke/devcontainer.json b/.devcontainer/gz-smoke/devcontainer.json new file mode 100644 index 0000000..4389beb --- /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/linux-x11-forwarding": {} + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-vscode.cpptools", + "ms-python.python", + "twxs.cmake", + "redhat.vscode-yaml" + ] + } + } +} diff --git a/.devcontainer/linux-x11-forwarding-smoke/devcontainer.json b/.devcontainer/linux-x11-forwarding-smoke/devcontainer.json new file mode 100644 index 0000000..9498c44 --- /dev/null +++ b/.devcontainer/linux-x11-forwarding-smoke/devcontainer.json @@ -0,0 +1,10 @@ +{ + "name": "linux-x11-forwarding-smoke", + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "../local-features/linux-x11-forwarding": { + "x11Display": "${localEnv:DISPLAY}" + } + }, + "postCreateCommand": "sudo apt-get update && sudo apt-get install -y x11-apps" +} 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..e1ed0c0 --- /dev/null +++ b/.devcontainer/prepare-local-features.sh @@ -0,0 +1,12 @@ +#!/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}/linux-x11-forwarding" "${local_features_dir}/x11" "${local_features_dir}/wayland" +cp -R "${repo_root}/features/src/linux-x11-forwarding" "${local_features_dir}/linux-x11-forwarding" + +echo "Prepared local smoke-test features in ${local_features_dir}" diff --git a/.github/workflows/test-pr.yaml b/.github/workflows/test-pr.yaml index a975c73..6af8a8d 100644 --- a/.github/workflows/test-pr.yaml +++ b/.github/workflows/test-pr.yaml @@ -23,6 +23,9 @@ jobs: steps: - uses: actions/checkout@v6 + - name: Install devcontainer CLI + run: npm install -g @devcontainers/cli + - name: Validate feature metadata run: bash features/test/test_all.sh diff --git a/README.md b/README.md index 10323b2..057ad3e 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,22 @@ Devcontainer templates repository focused on robotics and simulation. - `templates/src/gz` -## Included features +## Included feature -- `features/src/x11` -- `features/src/wayland` +- `features/src/linux-x11-forwarding` (local Linux hosts only) + +## Sample configs + +- `.devcontainer/gz-smoke/devcontainer.json` +- `.devcontainer/linux-x11-forwarding-smoke/devcontainer.json` + +Before using the samples locally, run +`bash .devcontainer/prepare-local-features.sh` so `devcontainer up` can resolve +the unpublished Feature. + +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 diff --git a/features/src/linux-x11-forwarding/NOTES.md b/features/src/linux-x11-forwarding/NOTES.md new file mode 100644 index 0000000..60bb038 --- /dev/null +++ b/features/src/linux-x11-forwarding/NOTES.md @@ -0,0 +1,31 @@ +## Linux host requirements + +This Feature supports containers running locally on a Linux desktop with Xorg or +Wayland/XWayland. It does not configure a host X server and does not support +Docker Desktop on macOS or Windows, remote Docker daemons, or Codespaces. + +The container is granted access to the host X server. Only use it with +development containers you trust. + +## Xauthority + +For modern Wayland/XWayland desktops, the feature auto-discovers the mounted `.mutter-Xwaylandauth.*` file from `XDG_RUNTIME_DIR`. + +For classic Xorg setups where the authority file lives outside `XDG_RUNTIME_DIR` (often `~/.Xauthority`), add an explicit mount in the consuming `devcontainer.json`: + +```json +"mounts": [ + "source=${localEnv:XAUTHORITY},target=/tmp/devcontainer-xauthority-host,type=bind,readonly" +] +``` + +The feature always exposes the stable in-container path `/tmp/devcontainer-xauthority` through `XAUTHORITY`. + +Qt applications are forced onto the X11 backend with `QT_QPA_PLATFORM=xcb` so they do not try to boot the Wayland plugin in X11-only containers. + +If the container does not already define `XDG_RUNTIME_DIR`, the Feature creates +a private per-user directory at `/tmp/devcontainer-runtime-`. The mounted +host runtime directory remains separate and is used only for host sockets and +XWayland authentication. + +Use the `x11Display` option to set the in-container `DISPLAY` value. diff --git a/features/src/linux-x11-forwarding/README.md b/features/src/linux-x11-forwarding/README.md new file mode 100644 index 0000000..f169dcb --- /dev/null +++ b/features/src/linux-x11-forwarding/README.md @@ -0,0 +1,56 @@ + +# Linux X11 Forwarding (linux-x11-forwarding) + +Forward a local Linux host's X11 display into a development container. + +## Example Usage + +```json +"features": { + "ghcr.io/althack/devcontainers/linux-x11-forwarding:0": {} +} +``` + +## Options + +| Options Id | Description | Type | Default Value | +|-----|-----|-----|-----| +| x11Display | Set DISPLAY for X11 clients launched from the container. | string | :0 | +| softwareGL | Set LIBGL_ALWAYS_SOFTWARE for GPU compatibility. | string | 1 | + +## Linux host requirements + +This Feature supports containers running locally on a Linux desktop with Xorg or +Wayland/XWayland. It does not configure a host X server and does not support +Docker Desktop on macOS or Windows, remote Docker daemons, or Codespaces. + +The container is granted access to the host X server. Only use it with +development containers you trust. + +## Xauthority + +For modern Wayland/XWayland desktops, the feature auto-discovers the mounted `.mutter-Xwaylandauth.*` file from `XDG_RUNTIME_DIR`. + +For classic Xorg setups where the authority file lives outside `XDG_RUNTIME_DIR` (often `~/.Xauthority`), add an explicit mount in the consuming `devcontainer.json`: + +```json +"mounts": [ + "source=${localEnv:XAUTHORITY},target=/tmp/devcontainer-xauthority-host,type=bind,readonly" +] +``` + +The feature always exposes the stable in-container path `/tmp/devcontainer-xauthority` through `XAUTHORITY`. + +Qt applications are forced onto the X11 backend with `QT_QPA_PLATFORM=xcb` so they do not try to boot the Wayland plugin in X11-only containers. + +If the container does not already define `XDG_RUNTIME_DIR`, the Feature creates +a private per-user directory at `/tmp/devcontainer-runtime-`. The mounted +host runtime directory remains separate and is used only for host sockets and +XWayland authentication. + +Use the `x11Display` option to set the in-container `DISPLAY` value. + + +--- + +_Note: This file was auto-generated from the [devcontainer-feature.json](https://github.com/althack/devcontainers/blob/main/features/src/linux-x11-forwarding/devcontainer-feature.json). Add additional notes to a `NOTES.md`._ diff --git a/features/src/linux-x11-forwarding/devcontainer-feature.json b/features/src/linux-x11-forwarding/devcontainer-feature.json new file mode 100644 index 0000000..3375ae5 --- /dev/null +++ b/features/src/linux-x11-forwarding/devcontainer-feature.json @@ -0,0 +1,35 @@ +{ + "id": "linux-x11-forwarding", + "version": "0.1.0", + "name": "Linux X11 Forwarding", + "description": "Forward a local Linux host's X11 display into a development container.", + "documentationURL": "https://github.com/althack/devcontainers/tree/main/features/src/linux-x11-forwarding", + "licenseURL": "https://github.com/althack/devcontainers/blob/main/LICENSE", + "options": { + "x11Display": { + "type": "string", + "default": ":0", + "description": "Set DISPLAY for X11 clients launched from the container." + }, + "softwareGL": { + "type": "string", + "default": "1", + "proposals": [ + "0", + "1" + ], + "description": "Set LIBGL_ALWAYS_SOFTWARE for GPU compatibility." + } + }, + "containerEnv": { + "XAUTHORITY": "/tmp/devcontainer-xauthority", + "PULSE_SERVER": "unix:/tmp/devcontainer-host-runtime/pulse-native" + }, + "mounts": [ + "source=/tmp/.X11-unix,target=/tmp/.X11-unix,type=bind", + "source=${localEnv:XDG_RUNTIME_DIR},target=/tmp/devcontainer-host-runtime,type=bind" + ], + "installsAfter": [ + "ghcr.io/devcontainers/features/common-utils" + ] +} diff --git a/features/src/linux-x11-forwarding/install.sh b/features/src/linux-x11-forwarding/install.sh new file mode 100755 index 0000000..318df6f --- /dev/null +++ b/features/src/linux-x11-forwarding/install.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +x11_display="${X11DISPLAY:-:0}" +software_gl="${SOFTWAREGL:-1}" + +cat >/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-x11-runtime}" +export XAUTHORITY="${FEATURE_TEST_XAUTHORITY:-/tmp/devcontainers-xauthority.test}" +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}/pulse-native" + +for feature in linux-x11-forwarding; do + devcontainer features test \ + --base-image "${base_image}" \ + --features "${feature}" \ + --project-folder "${features_root}" done diff --git a/features/test/wayland/test.sh b/features/test/wayland/test.sh deleted file mode 100755 index 058c89f..0000000 --- a/features/test/wayland/test.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -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" - -test -f "${feature_metadata}" -test -f "${feature_install}" -test -f "${feature_readme}" - -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}" diff --git a/features/test/x11/test.sh b/features/test/x11/test.sh deleted file mode 100755 index b5d09b1..0000000 --- a/features/test/x11/test.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -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" - -test -f "${feature_metadata}" -test -f "${feature_install}" -test -f "${feature_readme}" - -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}"