From d326c8afd5c914f43fa630a9761b53c7dc509a64 Mon Sep 17 00:00:00 2001 From: Taylor Braun-Jones Date: Thu, 20 Aug 2026 14:20:41 +0000 Subject: [PATCH 01/10] devcontainer: Add a Dev Container definition for CMake development Provide a `.devcontainer` definition, following the Dev Container Specification, to give contributors a ready-made Linux environment with the tools needed to build CMake, run its test suite, build its documentation, and satisfy its style rules. Base the container on Ubuntu, which offers the broadest ecosystem of packages and tooling for development, including a recent `cmake` and the `clang-format` version our style rules require, exactly. Mirror the package lists of the Debian image our CI infrastructure uses, section by section, so that the dependencies available closely match the ones against which merge requests are tested. Build the image the way the images under `.gitlab/ci/docker/` are built: bind mount the package lists and the installation script rather than copying them in, and cache the package lists and downloaded archives so that a rebuild fetches only what has changed. Run optional `.devcontainer/hooks/root.sh` and `.devcontainer/hooks/user.sh` scripts, both ignored by Git, so that developers may customize the container without modifying tracked files. Document usage in a new `Help/dev/devcontainer.rst`. Fixes: #28043 --- .devcontainer/Dockerfile | 59 +++++++++++ .devcontainer/create_user.sh | 38 +++++++ .devcontainer/deps_packages.lst | 126 ++++++++++++++++++++++ .devcontainer/dev_packages.lst | 44 ++++++++ .devcontainer/devcontainer.json | 30 ++++++ .devcontainer/docker-clean | 0 .devcontainer/hooks/.gitignore | 5 + .devcontainer/install_deps.sh | 27 +++++ .devcontainer/setup-status.sh | 25 +++++ .gitattributes | 1 + CONTRIBUTING.rst | 3 + Help/dev/README.rst | 2 + Help/dev/devcontainer.rst | 181 ++++++++++++++++++++++++++++++++ 13 files changed, 541 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100755 .devcontainer/create_user.sh create mode 100644 .devcontainer/deps_packages.lst create mode 100644 .devcontainer/dev_packages.lst create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/docker-clean create mode 100644 .devcontainer/hooks/.gitignore create mode 100755 .devcontainer/install_deps.sh create mode 100755 .devcontainer/setup-status.sh create mode 100644 Help/dev/devcontainer.rst diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000000..aa7968152e --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,59 @@ +# syntax=docker/dockerfile:1 + +# Base image for the CMake development container. +# +# The images our CI infrastructure uses, described under `.gitlab/ci/docker/`, +# are built on the distributions we test CMake against and are minimized for +# CI use. Prepare the environment from scratch on Ubuntu instead: it offers +# the broadest ecosystem of packages and tooling for development, including a +# recent `cmake` and the `clang-format` version our style rules require. +ARG BASE_IMAGE=ubuntu:26.04 + +FROM ${BASE_IMAGE} + +ARG USERNAME=cmake-dev +ARG USER_UID=1000 +ARG USER_GID=${USER_UID} + +# Install the packages needed to build CMake, run its test suite, build its +# documentation, and satisfy its style rules, along with a few more that make +# the container a comfortable place to work. +# +# Cache the package lists and the downloaded archives, and hide the +# `docker-clean` configuration the base image provides so that it does not +# discard them, so that rebuilding the container downloads only what has +# changed since the last build. +RUN --mount=type=bind,source=install_deps.sh,target=/root/install_deps.sh \ + --mount=type=bind,source=deps_packages.lst,target=/root/deps_packages.lst \ + --mount=type=bind,source=dev_packages.lst,target=/root/dev_packages.lst \ + --mount=type=bind,source=docker-clean,target=/etc/apt/apt.conf.d/docker-clean \ + --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ + --mount=type=cache,target=/var/cache/apt,sharing=locked \ + sh /root/install_deps.sh + +# Create an unprivileged user matching the typical host account so that files +# created in the mounted source tree are not owned by `root`. +RUN --mount=type=bind,source=create_user.sh,target=/root/create_user.sh \ + sh /root/create_user.sh ${USERNAME} ${USER_UID} ${USER_GID} + +# Run the optional local customization scripts. They are ignored by Git so +# that developers may customize the container without modifying tracked files +# or risking that the customizations end up in a commit. See +# `Help/dev/devcontainer.rst`. +# +# `USER` sets neither the working directory nor `HOME`, and BuildKit passes a +# `RUN` only the environment the image records, which names just `PATH`. Say +# where each hook runs and whose home it writes to, so that a hook may spell a +# path relative to either. +WORKDIR /root +RUN --mount=type=bind,source=hooks,target=/opt/cmake-dev-hooks \ + if test -f /opt/cmake-dev-hooks/root.sh; then \ + HOME=/root sh -e /opt/cmake-dev-hooks/root.sh; \ + fi + +USER ${USERNAME} +WORKDIR /home/${USERNAME} +RUN --mount=type=bind,source=hooks,target=/opt/cmake-dev-hooks \ + if test -f /opt/cmake-dev-hooks/user.sh; then \ + HOME=/home/${USERNAME} sh -e /opt/cmake-dev-hooks/user.sh; \ + fi diff --git a/.devcontainer/create_user.sh b/.devcontainer/create_user.sh new file mode 100755 index 0000000000..fad12bcdd7 --- /dev/null +++ b/.devcontainer/create_user.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +# Create the unprivileged user that the container runs as, given its name, +# uid, and gid. See `Help/dev/devcontainer.rst`. + +set -e + +readonly username="$1" +readonly uid="$2" +readonly gid="$3" + +# The base image already ships an unprivileged user, which usually occupies +# the uid we want. Remove whoever holds it, and the group holding our gid, +# before creating ours. +if getent passwd "$uid" > /dev/null; then + userdel --remove "$(getent passwd "$uid" | cut -d: -f1)" +fi +if getent group "$gid" > /dev/null; then + groupdel "$(getent group "$gid" | cut -d: -f1)" +fi + +groupadd --gid "$gid" "$username" +useradd --uid "$uid" --gid "$gid" --create-home --shell /bin/bash "$username" + +# Let the user administer the container, e.g. to install more packages. +echo "$username ALL=(ALL) NOPASSWD:ALL" > "/etc/sudoers.d/$username" +chmod 0440 "/etc/sudoers.d/$username" + +# Pre-create the directories that `devcontainer.json` mounts volumes over so +# that the volumes inherit the ownership recorded here. Name the parents too: +# `install -d` records the ownership only of the directories it is given, and +# tools that write elsewhere under them need to own them as well. +install -d -o "$username" -g "$username" \ + "/home/$username/.cache" \ + "/home/$username/.cache/ccache" \ + "/home/$username/.config" \ + "/home/$username/.config/glab-cli" \ + "/home/$username/workspace" diff --git a/.devcontainer/deps_packages.lst b/.devcontainer/deps_packages.lst new file mode 100644 index 0000000000..f3b4654b11 --- /dev/null +++ b/.devcontainer/deps_packages.lst @@ -0,0 +1,126 @@ +# Packages needed to build CMake, run its test suite, and build its +# documentation. +# +# This list mirrors `.gitlab/ci/docker/debian13-x86_64/deps_packages.lst`, +# section by section, so that the two are easy to compare as our +# dependencies evolve. Package names differ where Ubuntu names them +# differently, and entries needed only by our CI infrastructure are left out +# with a note saying so. +# +# Additions for personal use belong in `.devcontainer/hooks/root.sh`. +# See `Help/dev/devcontainer.rst`. + +locales + +# Install build requirements. +libssl-dev + +# Install development tools. +g++ +curl +git + +# The base image carries no certificate store, which `curl` and `apt` need +# to reach anything over HTTPS. +ca-certificates + +# Install optional external build dependencies. +libarchive-dev +libbz2-dev +libcurl4-gnutls-dev +libexpat1-dev +libjsoncpp-dev +liblzma-dev +libncurses-dev +librhash-dev +libuv1-dev +libzstd-dev +zlib1g-dev + +# NOTE `include-what-you-use` is not provided here. CI builds it from +# source, which is more than a development container needs. + +# Tools needed for the test suite. +jq + +# Packages needed to test CTest. +brz +cvs +subversion +mercurial + +# Install ASM_NASM language toolchain. +nasm + +# NOTE The HIP language toolchain, `hipcc`, is not provided here. It is +# large and rarely needed while working on CMake itself. + +# NOTE The IAR compiler is not provided here, so neither are the packages it +# depends on. + +# Packages needed to test find modules. +alsa-utils +aspell +aspell-en +doxygen graphviz +freeglut3-dev +libgnutls28-dev +libarchive-dev +libaspell-dev +libblas-dev +libboost-dev +libboost-filesystem-dev +libboost-program-options-dev +libboost-python-dev +libboost-thread-dev +libbz2-dev +libcups2-dev +libcurl4-gnutls-dev +libdevil-dev +libfontconfig1-dev +libfreetype-dev +libgdal-dev +libgif-dev +libgl1-mesa-dev +libglew-dev +libgmock-dev +libgrpc++-dev libgrpc-dev +libgsl-dev +libgtest-dev +libgtk2.0-dev +libhdf5-dev +libhdf5-mpich-dev +libhdf5-openmpi-dev +libicu-dev +libinput-dev +libjpeg-dev +libjsoncpp-dev +liblapack-dev +liblzma-dev +libmagick++-dev +libopenal-dev +libopenmpi-dev openmpi-bin +libosp-dev +libpng-dev +libpq-dev postgresql-server-dev-18 +libprotobuf-dev libprotobuf-c-dev libprotoc-dev protobuf-compiler protobuf-compiler-grpc +libsdl1.2-dev +libsqlite3-dev +libtiff-dev +libuv1-dev +libwxgtk3.2-dev +libx11-dev +libxalan-c-dev +libxerces-c-dev +libxml2-dev libxml2-utils +libxslt1-dev xsltproc +openjdk-25-jdk +python3 python3-dev python3-numpy pypy3 pypy3-dev python3-venv +qtbase5-dev qtbase5-dev-tools +rbenv ruby-build +ruby ruby-dev +swig +unixodbc-dev + +# NOTE The packages needed to test ironpython are not provided here. Ubuntu +# no longer carries `libmono-system-windows-forms4.0-cil`. diff --git a/.devcontainer/dev_packages.lst b/.devcontainer/dev_packages.lst new file mode 100644 index 0000000000..5f3286d6f1 --- /dev/null +++ b/.devcontainer/dev_packages.lst @@ -0,0 +1,44 @@ +# Packages that make the container a comfortable place to work in, but that +# building CMake, running its test suite, and building its documentation do +# not need, and so are not part of `deps_packages.lst`. +# +# Additions for personal use belong in `.devcontainer/hooks/root.sh`. +# See `Help/dev/devcontainer.rst`. + +# Make the shell pleasant to work in interactively. `unminimize` restores +# the documentation of the packages the base image provides, which it ships +# without; see `Help/dev/devcontainer.rst`. +bash-completion +less +man-db +manpages +manpages-dev +nano +unminimize +vim + +# Build CMake. +ccache +cmake +ninja-build + +# Debug CMake. +gdb + +# Build the documentation. +python3-sphinx + +# Satisfy our style rules. Our `.clang-format` requires `clang-format` +# version 18, exactly, so install that version by name. Do not install the +# unversioned `clang-format` package: it provides a much newer version. +clang-format-18 +pre-commit + +# Run `glab` and `glab-axi`, and reach GitLab over SSH. `nodejs` and `npm` +# are needed both to install `glab-axi` and to run it. +nodejs +npm +openssh-client + +# Let the unprivileged container user install more packages. +sudo diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..c16cea876a --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,30 @@ +{ + "name": "CMake", + "build": { + "dockerfile": "Dockerfile" + }, + "remoteUser": "cmake-dev", + "mounts": [ + { + "type": "volume", + "source": "cmake-dev-ccache", + "target": "/home/cmake-dev/.cache/ccache" + } + ], + "postAttachCommand": "${containerWorkspaceFolder}/.devcontainer/setup-status.sh", + "customizations": { + "vscode": { + "extensions": [ + "EditorConfig.EditorConfig", + "ms-vscode.cmake-tools", + "ms-vscode.cpptools", + "lextudio.restructuredtext", + "llvm-vs-code-extensions.vscode-clangd" + ], + "settings": { + "C_Cpp.clang_format_path": "/usr/bin/clang-format-18", + "cmake.configureOnOpen": false + } + } + } +} diff --git a/.devcontainer/docker-clean b/.devcontainer/docker-clean new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.devcontainer/hooks/.gitignore b/.devcontainer/hooks/.gitignore new file mode 100644 index 0000000000..347b930096 --- /dev/null +++ b/.devcontainer/hooks/.gitignore @@ -0,0 +1,5 @@ +# Local customizations applied while building the development container. +# Everything here is ignored, except this file, so that customizations may +# bring along whatever files they need. See `Help/dev/devcontainer.rst`. +/* +!/.gitignore diff --git a/.devcontainer/install_deps.sh b/.devcontainer/install_deps.sh new file mode 100755 index 0000000000..3b80f4ff77 --- /dev/null +++ b/.devcontainer/install_deps.sh @@ -0,0 +1,27 @@ +#!/bin/sh + +# Install the packages listed in `deps_packages.lst` and `dev_packages.lst`. +# See `Help/dev/devcontainer.rst`. + +set -e + +# Install without asking questions, e.g. the time zone `tzdata` wants. +export DEBIAN_FRONTEND=noninteractive + +# Unlike our CI images, keep the documentation that packages carry: the base +# image excludes man pages and the like, which is not what one wants in a +# development environment. Packages already installed in the base image +# remain without their documentation; `unminimize` restores that. +rm -f /etc/dpkg/dpkg.cfg.d/excludes + +apt-get update +apt-get install -y $(grep -h '^[^#]\+$' /root/deps_packages.lst /root/dev_packages.lst) + +# Add locales, the way our CI images do, for the tests that need them. +sed -i -E '/^# en_US[ .](ISO-8859-1|UTF-8)( |$)/ s/^# //' /etc/locale.gen +dpkg-reconfigure --frontend=noninteractive locales + +# `Utilities/Scripts/clang-format.bash` finds `clang-format-18` by name, but +# make the unversioned name resolve to version 18 as well so that tools +# looking for it get the version our style rules require. +ln -s "$(command -v clang-format-18)" /usr/local/bin/clang-format diff --git a/.devcontainer/setup-status.sh b/.devcontainer/setup-status.sh new file mode 100755 index 0000000000..92ae3ad8b6 --- /dev/null +++ b/.devcontainer/setup-status.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +# Report whether this clone and its development container are ready to use +# and, if they are not, print what remains to be set up. Run each time a +# tool attaches to the container. See `Help/dev/devcontainer.rst`. + +set -u + +cd "$(dirname "$0")/.." + +# Check that development setup is up-to-date, the way our `pre-commit` hook +# does. `Utilities/SetupForDevelopment.sh` is interactive, so the container +# cannot run it on the developer's behalf. +eval "$(grep '^SetupForDevelopment_VERSION=' Utilities/SetupForDevelopment.sh)" +setup_done=$(git config --get hooks.SetupForDevelopment || echo 0) +if test "$setup_done" -lt "${SetupForDevelopment_VERSION:-0}"; then + cat < .devcontainer/hooks/root.sh <<'EOF' + apt-get update && apt-get install -y tmux + EOF + $ cat > .devcontainer/hooks/user.sh <<'EOF' + echo "alias b='cmake --build build'" >> ~/.bashrc + EOF + +Rebuild the container to apply them, e.g. with the +``Dev Containers: Rebuild Container`` command in Visual Studio Code. + +Larger or longer-lived changes may of course be made by editing +`.devcontainer/Dockerfile`_ or `.devcontainer/devcontainer.json`_ directly, +but take care not to commit them accidentally. + +.. _`.devcontainer/Dockerfile`: ../../.devcontainer/Dockerfile +.. _`.devcontainer/devcontainer.json`: ../../.devcontainer/devcontainer.json From 69c15636b61f5887ee8a04c591b6893e72a6fd75 Mon Sep 17 00:00:00 2001 From: Taylor Braun-Jones Date: Thu, 20 Aug 2026 15:42:28 +0000 Subject: [PATCH 02/10] devcontainer: Add the GitLab CLI and `glab-axi` Install `glab`, the GitLab CLI, and `glab-axi`, an agent-ergonomic wrapper around it, in the development container. Default `GITLAB_HOST` to `gitlab.kitware.com` so that both address our GitLab instance without further configuration, persist the `glab` configuration directory on a volume so that a credential need be created only once, and pass a `GITLAB_TOKEN` or `GITLAB_CLIENT_ID` from the host through to the container. Neither tool can authenticate on its own, so check for a working credential each time a tool attaches to the container and print what remains to be set up if there is none. --- .devcontainer/Dockerfile | 9 +++++++ .devcontainer/devcontainer.json | 12 +++++++++ .devcontainer/install_glab.sh | 43 +++++++++++++++++++++++++++++++++ .devcontainer/setup-status.sh | 37 ++++++++++++++++++++++++++-- Help/dev/devcontainer.rst | 26 ++++++++++++++++++++ 5 files changed, 125 insertions(+), 2 deletions(-) create mode 100755 .devcontainer/install_glab.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index aa7968152e..1bae771f79 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -31,6 +31,15 @@ RUN --mount=type=bind,source=install_deps.sh,target=/root/install_deps.sh \ --mount=type=cache,target=/var/cache/apt,sharing=locked \ sh /root/install_deps.sh +# Install the GitLab CLI, `glab`, and `glab-axi`, an agent-ergonomic wrapper +# around it, for interacting with our GitLab instance. Keep the packages +# `npm` downloads in a cache, and unpack the `glab` release in a `tmpfs`, so +# that neither leaves anything behind in the image. +RUN --mount=type=bind,source=install_glab.sh,target=/root/install_glab.sh \ + --mount=type=cache,target=/root/.npm,sharing=locked \ + --mount=type=tmpfs,target=/tmp \ + sh /root/install_glab.sh + # Create an unprivileged user matching the typical host account so that files # created in the mounted source tree are not owned by `root`. RUN --mount=type=bind,source=create_user.sh,target=/root/create_user.sh \ diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c16cea876a..d7f7fad576 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,11 +4,23 @@ "dockerfile": "Dockerfile" }, "remoteUser": "cmake-dev", + "containerEnv": { + "GITLAB_HOST": "gitlab.kitware.com" + }, + "remoteEnv": { + "GITLAB_CLIENT_ID": "${localEnv:GITLAB_CLIENT_ID}", + "GITLAB_TOKEN": "${localEnv:GITLAB_TOKEN}" + }, "mounts": [ { "type": "volume", "source": "cmake-dev-ccache", "target": "/home/cmake-dev/.cache/ccache" + }, + { + "type": "volume", + "source": "cmake-dev-glab-cli", + "target": "/home/cmake-dev/.config/glab-cli" } ], "postAttachCommand": "${containerWorkspaceFolder}/.devcontainer/setup-status.sh", diff --git a/.devcontainer/install_glab.sh b/.devcontainer/install_glab.sh new file mode 100755 index 0000000000..644e131490 --- /dev/null +++ b/.devcontainer/install_glab.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +# Install the GitLab CLI, `glab`, and `glab-axi`, an agent-ergonomic wrapper +# around it. Neither is provided by the distribution. +# See `Help/dev/devcontainer.rst`. + +set -e + +readonly glab_version="1.114.0" +readonly glab_axi_version="0.6.0" + +case "$(uname -m)" in + x86_64) + arch="amd64" + sha256sum="00e892a80d586a1e8b8fdc035321923db99dce0caa3b0c4fd72c5337ffdb1c48" + ;; + aarch64) + arch="arm64" + sha256sum="d34d7ddb96ce5e5f3423d7e8053cb14c36bd93984e4b96320f7e20a341b83498" + ;; + *) + echo "Unsupported architecture: $(uname -m)" >&2 + exit 1 + ;; +esac + +readonly filename="glab_${glab_version}_linux_${arch}.tar.gz" +readonly baseurl="https://gitlab.com/gitlab-org/cli/-/releases/v${glab_version}/downloads" + +cd /tmp +curl -L -o "$filename" "$baseurl/$filename" +echo "$sha256sum $filename" > glab.sha256sum +sha256sum --check glab.sha256sum +tar -C /usr/local -xzf "$filename" bin/glab +rm "$filename" glab.sha256sum + +# Enable shell completion for interactive use. +mkdir -p /etc/bash_completion.d +/usr/local/bin/glab completion --shell bash > /etc/bash_completion.d/glab + +# `glab-axi` is distributed only through npm. Its command surface is +# documented at https://axi.md. +npm install --global --no-audit --no-fund "glab-axi@${glab_axi_version}" diff --git a/.devcontainer/setup-status.sh b/.devcontainer/setup-status.sh index 92ae3ad8b6..63bd29b46d 100755 --- a/.devcontainer/setup-status.sh +++ b/.devcontainer/setup-status.sh @@ -20,6 +20,39 @@ Run 'Utilities/SetupForDevelopment.sh' to configure your Git identity and install the project's commit hooks. The work tree is shared with the host, so running it here sets up both. MESSAGE -else - echo "git: this work tree is set up for development." fi + +readonly host="${GITLAB_HOST:-gitlab.com}" + +if glab auth status --hostname "$host" > /dev/null 2>&1; then + exit 0 +fi + +# The OAuth flows need the application ID of an OAuth application registered +# on the instance. Offer them only when one is available, and prefer the +# device flow because it needs no redirect back into the container. +if test -n "${GITLAB_CLIENT_ID:-}"; then + readonly login="glab auth login --hostname $host --device" + readonly hint="" +else + readonly login="glab auth login --hostname $host" + readonly hint=" +Setting GITLAB_CLIENT_ID, on the host, to the application ID of a GitLab +OAuth application offers to sign in through that application instead. +" +fi + +cat < Date: Mon, 24 Aug 2026 16:23:33 -0400 Subject: [PATCH 03/10] devcontainer: Add the Kitware APT repository Configure `apt.kitware.com` in the development container so that `cmake` is the latest CMake release rather than the older one Ubuntu carries, and so that developers reach each further release through `apt-get` alone. Fetch the repository's signing key, verified against the hash the `Dockerfile` pins, and trust it just long enough to install `kitware-archive-keyring`, which then provides the key, so that `apt` follows the rotations Kitware makes to it. Interpolating the hash into the step that fetches the key also busts the build cache when the hash changes, so a rotation is picked up rather than served from an old layer. Reaching either the key or the repository needs `curl` and a certificate store, neither of which the base image carries, so install them first. --- .devcontainer/Dockerfile | 20 ++++++++- .devcontainer/install_kitware_archive.sh | 56 ++++++++++++++++++++++++ Help/dev/devcontainer.rst | 15 ++++++- 3 files changed, 88 insertions(+), 3 deletions(-) create mode 100755 .devcontainer/install_kitware_archive.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 1bae771f79..a652b1ea5d 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -5,8 +5,9 @@ # The images our CI infrastructure uses, described under `.gitlab/ci/docker/`, # are built on the distributions we test CMake against and are minimized for # CI use. Prepare the environment from scratch on Ubuntu instead: it offers -# the broadest ecosystem of packages and tooling for development, including a -# recent `cmake` and the `clang-format` version our style rules require. +# the broadest ecosystem of packages and tooling for development, including +# the `clang-format` version our style rules require and the Kitware APT +# repository through which CMake itself is published. ARG BASE_IMAGE=ubuntu:26.04 FROM ${BASE_IMAGE} @@ -15,6 +16,21 @@ ARG USERNAME=cmake-dev ARG USER_UID=1000 ARG USER_GID=${USER_UID} +# The SHA-256 of the signing key `apt.kitware.com` publishes. Kitware rotates +# that key every few years; updating this hash both approves the new key and +# forces the step below to fetch it again rather than reuse a cached layer. +# Read the current value with: +# curl -fsSL https://apt.kitware.com/keys/kitware-archive-latest.asc | sha256sum +ARG KITWARE_PUBLIC_KEY_SHA256=801bc629e356c3c96f184351272914222ce427777400fa7d1baed3ab180b3e3b + +# Add the Kitware APT repository, which carries CMake releases newer than the +# ones the distribution provides, before installing anything from it below. +RUN --mount=type=bind,source=install_kitware_archive.sh,target=/root/install_kitware_archive.sh \ + --mount=type=bind,source=docker-clean,target=/etc/apt/apt.conf.d/docker-clean \ + --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ + --mount=type=cache,target=/var/cache/apt,sharing=locked \ + sh /root/install_kitware_archive.sh ${KITWARE_PUBLIC_KEY_SHA256} + # Install the packages needed to build CMake, run its test suite, build its # documentation, and satisfy its style rules, along with a few more that make # the container a comfortable place to work. diff --git a/.devcontainer/install_kitware_archive.sh b/.devcontainer/install_kitware_archive.sh new file mode 100755 index 0000000000..d120d7cc08 --- /dev/null +++ b/.devcontainer/install_kitware_archive.sh @@ -0,0 +1,56 @@ +#!/bin/sh + +# Add the Kitware APT repository, which carries CMake releases newer than the +# ones the distribution provides. See `Help/dev/devcontainer.rst`. + +set -e + +readonly key_sha256="$1" + +if test -z "$key_sha256"; then + echo "usage: $0 " >&2 + exit 1 +fi + +# Install without asking questions. +export DEBIAN_FRONTEND=noninteractive + +# `VERSION_CODENAME` names the suite the repository provides for the +# distribution the container is based on. +. /etc/os-release + +readonly sources=/etc/apt/sources.list.d/kitware.sources +readonly keyring=/usr/share/keyrings/kitware-archive-keyring +readonly key_url=https://apt.kitware.com/keys/kitware-archive-latest.asc + +# Describe the repository, verified with the keyring named as the argument. +write_sources() { + cat > "$sources" < Date: Mon, 24 Aug 2026 16:41:38 -0400 Subject: [PATCH 04/10] devcontainer: Reword the comment on caching package downloads The sentence packed the two purposes of the mounts and their effect into one clause chain, which was hard to follow. Split it in two. Suggested-by: Tyler Yankee --- .devcontainer/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index a652b1ea5d..c6b5343aa7 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -37,8 +37,8 @@ RUN --mount=type=bind,source=install_kitware_archive.sh,target=/root/install_kit # # Cache the package lists and the downloaded archives, and hide the # `docker-clean` configuration the base image provides so that it does not -# discard them, so that rebuilding the container downloads only what has -# changed since the last build. +# discard them. A rebuild then downloads only what has changed since the +# last build. RUN --mount=type=bind,source=install_deps.sh,target=/root/install_deps.sh \ --mount=type=bind,source=deps_packages.lst,target=/root/deps_packages.lst \ --mount=type=bind,source=dev_packages.lst,target=/root/dev_packages.lst \ From a8872df6492466e20d26209dbe8f72461a9f5a03 Mon Sep 17 00:00:00 2001 From: Taylor Braun-Jones Date: Mon, 24 Aug 2026 16:42:14 -0400 Subject: [PATCH 05/10] devcontainer: Add `clang`, `gfortran`, and `valgrind` Provide `clang` for developers who prefer to build with it rather than with the default `g++`, and `valgrind` to run CMake and its tests under a memory checker. The sanitizer runtimes already come with both compilers, so building with `-fsanitize=` needs nothing installed. Name `gfortran` as well, without which Clang cannot link at all. Ubuntu 26.04 carries a Fortran-only `gcc-16` build that provides no C++ standard library, `libopenmpi-dev` lists `gfortran-16` first among the alternatives it accepts, and Clang selects the newest GCC installation it finds. Naming `gfortran` lets `apt` satisfy that dependency with `gfortran-15` instead, so the GCC 16 installation never appears. Suggested-by: Tyler Yankee --- .devcontainer/dev_packages.lst | 15 +++++++++++++-- Help/dev/devcontainer.rst | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/.devcontainer/dev_packages.lst b/.devcontainer/dev_packages.lst index 5f3286d6f1..1e4c62cb29 100644 --- a/.devcontainer/dev_packages.lst +++ b/.devcontainer/dev_packages.lst @@ -17,13 +17,24 @@ nano unminimize vim -# Build CMake. +# Build CMake. `g++` comes from `deps_packages.lst`, as CI builds with it; +# `clang` is here for developers who prefer to build with it instead. +# +# `gfortran` is named only to keep the Fortran-only `gcc-16` build that +# Ubuntu 26.04 carries out of the container: `libopenmpi-dev` lists +# `gfortran-16` first among the alternatives it accepts, and Clang, which +# selects the newest GCC installation it finds, cannot link against one that +# provides no C++ standard library. ccache +clang cmake +gfortran ninja-build -# Debug CMake. +# Debug and diagnose CMake. The sanitizer runtimes come with the compilers, +# so building with `-fsanitize=` needs nothing installed here. gdb +valgrind # Build the documentation. python3-sphinx diff --git a/Help/dev/devcontainer.rst b/Help/dev/devcontainer.rst index bbc35f77cb..ff0b63572b 100644 --- a/Help/dev/devcontainer.rst +++ b/Help/dev/devcontainer.rst @@ -111,6 +111,13 @@ against, the container provides: Ubuntu release with ``-rc`` appended. Add it to the ``Suites`` field of ``/etc/apt/sources.list.d/kitware.sources`` to install those as well. +* ``clang``, for developers who would rather build with it than with the + default ``g++``: + + .. code-block:: console + + $ cmake -G Ninja -B build-clang -S . -DCMAKE_CXX_COMPILER=clang++ + * ``ccache``, to speed up repeated builds, e.g.: .. code-block:: console @@ -140,6 +147,18 @@ against, the container provides: * ``gdb``, to debug CMake as described in the `CMake Debugging Guide`_. +* ``valgrind``, and the sanitizer runtimes that come with ``g++`` and + ``clang``, to run CMake and its tests under a memory checker, the way the + sanitizer and Valgrind jobs of our CI do: + + .. code-block:: console + + $ cmake -G Ninja -B build-asan -S . \ + -DCMAKE_C_FLAGS=-fsanitize=address \ + -DCMAKE_CXX_FLAGS=-fsanitize=address + $ cmake --build build-asan + $ ctest --test-dir build-asan + * ``glab``, the `GitLab CLI`_, to work with merge requests, issues, and pipelines on our GitLab instance, and `glab-axi`_, a wrapper around it whose output follows the `AXI`_ conventions: From 9c1733260107eafddac6cb333e6f18d246efb3b4 Mon Sep 17 00:00:00 2001 From: Taylor Braun-Jones Date: Mon, 24 Aug 2026 18:25:34 -0400 Subject: [PATCH 06/10] devcontainer: Add `clang-tidy`, `clang-tools`, and `clazy` Provide the analysis tools our CI images carry: `clang-tidy` to run the checks in `.clang-tidy`, `clang-tools` for `scan-build`, and `clazy`, the compiler our Clazy job builds with. Name the version of `clang-tidy` our checks are written against rather than install Ubuntu's unversioned package, which is a release behind and reports `cmake-use-cmsys-fstream` where it should not. `CMakeLists.txt` searches for `clang-tidy` only under the unversioned name, so link the version installed by name to it, as we already do for `clang-format`. `clazy` and `clang-tools` remain whatever versions Ubuntu carries rather than the ones the CI image does, so expect their diagnostics to differ from those jobs'. CMake's own clang-tidy checks are not available either way: `CMake_USE_CLANG_TIDY_MODULE` needs `Utilities/ClangTidyModule` built against Clang's development files, which are not installed here. Suggested-by: Tyler Yankee --- .devcontainer/dev_packages.lst | 15 +++++++++++++++ .devcontainer/install_deps.sh | 5 ++++- Help/dev/devcontainer.rst | 17 +++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/.devcontainer/dev_packages.lst b/.devcontainer/dev_packages.lst index 1e4c62cb29..9d74a1dfba 100644 --- a/.devcontainer/dev_packages.lst +++ b/.devcontainer/dev_packages.lst @@ -36,6 +36,21 @@ ninja-build gdb valgrind +# Analyze CMake. `clazy` is the compiler our Clazy CI job builds with, +# `clang-tools` provides `scan-build`, and `clang-tidy` runs the checks in +# `.clang-tidy`. Name the version of `clang-tidy` our checks are written +# against: `Utilities/ClangTidyModule` tracks the Clang release our CI image +# carries, and older versions report `cmake-use-cmsys-fstream` where they +# should not. Ubuntu's unversioned `clang-tidy` package is a release behind. +# `clazy` and `clang-tools` are whatever versions Ubuntu carries, so expect +# their diagnostics to differ from those jobs'. CMake's own checks are not +# among them either way: `CMake_USE_CLANG_TIDY_MODULE` needs +# `Utilities/ClangTidyModule` built against Clang's development files, which +# are not installed here. +clang-tidy-22 +clang-tools +clazy + # Build the documentation. python3-sphinx diff --git a/.devcontainer/install_deps.sh b/.devcontainer/install_deps.sh index 3b80f4ff77..1a6f19b5a2 100755 --- a/.devcontainer/install_deps.sh +++ b/.devcontainer/install_deps.sh @@ -23,5 +23,8 @@ dpkg-reconfigure --frontend=noninteractive locales # `Utilities/Scripts/clang-format.bash` finds `clang-format-18` by name, but # make the unversioned name resolve to version 18 as well so that tools -# looking for it get the version our style rules require. +# looking for it get the version our style rules require. `CMakeLists.txt` +# searches for `clang-tidy` only under the unversioned name, so the version +# installed by name needs one too. ln -s "$(command -v clang-format-18)" /usr/local/bin/clang-format +ln -s "$(command -v clang-tidy-22)" /usr/local/bin/clang-tidy diff --git a/Help/dev/devcontainer.rst b/Help/dev/devcontainer.rst index ff0b63572b..ef88bfe2af 100644 --- a/Help/dev/devcontainer.rst +++ b/Help/dev/devcontainer.rst @@ -159,6 +159,22 @@ against, the container provides: $ cmake --build build-asan $ ctest --test-dir build-asan +* ``clang-tidy``, ``scan-build``, and ``clazy``, the compiler our Clazy CI + job builds with, to analyze CMake rather than only compile it: + + .. code-block:: console + + $ cmake -G Ninja -B build-tidy -S . -DCMake_RUN_CLANG_TIDY=ON + $ cmake -G Ninja -B build-clazy -S . -DCMAKE_CXX_COMPILER=clazy + + ``clang-tidy`` is the version our checks are written against, which is + not the one Ubuntu's unversioned package provides. ``scan-build`` and + ``clazy`` are whatever versions Ubuntu carries rather than the ones our + CI image does, so expect their diagnostics to differ from those jobs'. + CMake's own checks are not available either way: + ``CMake_USE_CLANG_TIDY_MODULE`` needs `Utilities/ClangTidyModule`_ built + against Clang's development files, which the container does not install. + * ``glab``, the `GitLab CLI`_, to work with merge requests, issues, and pipelines on our GitLab instance, and `glab-axi`_, a wrapper around it whose output follows the `AXI`_ conventions: @@ -173,6 +189,7 @@ against, the container provides: .. _`Kitware APT repository`: https://apt.kitware.com .. _`C++ Code Style`: source.rst#c-code-style .. _`.pre-commit-config.yaml`: ../../.pre-commit-config.yaml +.. _`Utilities/ClangTidyModule`: ../../Utilities/ClangTidyModule .. _`CMake Documentation Guide`: documentation.rst .. _`CMake Debugging Guide`: debug.rst .. _`GitLab CLI`: https://docs.gitlab.com/editor_extensions/gitlab_cli/ From ea0667e7798a8600d115be07836a2a9828fda5a8 Mon Sep 17 00:00:00 2001 From: Ben Boeckel Date: Sun, 23 Aug 2026 21:59:56 -0400 Subject: [PATCH 07/10] ci: add scripts to run and verify the devcontainer Co-authored-by: Taylor Braun-Jones --- .gitlab/ci/devcontainer-run.sh | 45 +++++++++++++++++++ .gitlab/ci/devcontainer-verify.sh | 75 +++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100755 .gitlab/ci/devcontainer-run.sh create mode 100755 .gitlab/ci/devcontainer-verify.sh diff --git a/.gitlab/ci/devcontainer-run.sh b/.gitlab/ci/devcontainer-run.sh new file mode 100755 index 0000000000..18eab9c8cc --- /dev/null +++ b/.gitlab/ci/devcontainer-run.sh @@ -0,0 +1,45 @@ +#!/bin/sh + +set -e + +# This job runs in a container whose filesystem is `overlayfs`, over which +# `podman` cannot stack its own `overlay` storage driver: +# +# 'overlay' is not supported over overlayfs, a mount_program is required +# +# `vfs` copies each layer rather than stacking it, which is slower but asks +# nothing of the filesystem underneath. Name it in the environment so that +# every `podman` command below addresses the same storage, the build included. +export STORAGE_DRIVER=vfs + +readonly dockerfile=".devcontainer/Dockerfile" +readonly name="cmake-dev-container" + +# Build the container image from the devcontainer Dockerfile +echo "# Building devcontainer image" +podman build -t "$name" -f "$dockerfile" .devcontainer + +# Ensure named volumes exist for ccache and glab-cli cache persistence +echo "# Ensuring volumes exist" +podman volume create cmake-dev-ccache 2>/dev/null || true +podman volume create cmake-dev-glab-cli 2>/dev/null || true + +# Run the container with volumes mounted. +# +# The job's own container has no cgroup controllers delegated to it, so +# `podman` cannot place the container it starts under one: +# +# crun: controller `pids` is not available under /sys/fs/cgroup/... +# +# Ask for no cgroup at all. Verification needs no resource limits, and there +# is nothing to limit them with. There is no terminal either, so do not ask +# for one. +echo "# Starting devcontainer" +podman run --rm \ + --cgroups=disabled \ + -v "$PWD:/home/cmake-dev/workspace:Z" \ + -v cmake-dev-ccache:/home/cmake-dev/.cache/ccache:Z \ + -v cmake-dev-glab-cli:/home/cmake-dev/.config/glab-cli:Z \ + --workdir /home/cmake-dev/workspace \ + "$name" \ + .gitlab/ci/devcontainer-verify.sh diff --git a/.gitlab/ci/devcontainer-verify.sh b/.gitlab/ci/devcontainer-verify.sh new file mode 100755 index 0000000000..fbce3c2ba5 --- /dev/null +++ b/.gitlab/ci/devcontainer-verify.sh @@ -0,0 +1,75 @@ +#!/bin/sh + +set -e + +# Source CI environment scripts +. .gitlab/ci/env.sh + +# Verify devcontainer volumes are accessible +echo "# Verifying devcontainer volumes" + +# Check ccache volume +if test -d /home/cmake-dev/.cache/ccache; then + echo "ccache volume found at /home/cmake-dev/.cache/ccache" + ccache -s 2>/dev/null || echo "ccache stats unavailable (may be fresh)" +else + echo "WARNING: ccache volume not found at expected path" +fi + +# Check glab-cli volume +if test -d /home/cmake-dev/.config/glab-cli; then + echo "glab-cli volume found at /home/cmake-dev/.config/glab-cli" +else + echo "WARNING: glab-cli volume not found at expected path" +fi + +# Verify cmake is available +echo "# Verifying cmake" +cmake --version + +# Verify ccache is available +echo "# Verifying ccache" +ccache --version + +# Build a simple test project to verify the devcontainer works +echo "# Building test project" + +# Create a temporary build directory +mkdir -p /tmp/devcontainer-test +cd /tmp/devcontainer-test + +# Create a minimal C project +cat > CMakeLists.txt < main.c < + +int main(void) { + printf("Devcontainer CMake build OK\n"); + return 0; +} +cmake + +# Configure with cmake using ccache cache path +echo "# Configuring with cmake" +cmake 2>&1 \ + -GNinja \ + -S. \ + -Bbuild + +# Build +echo "# Building" +ninja -C build 2>&1 + +# Verify cache was preserved +echo "# Verifying ccache hit" +ccache -s 2>/dev/null | head -5 || true + +echo "# Devcontainer verification complete" From f8f4dc7f43d2feed2e85ef8aa043c68ca58de6c5 Mon Sep 17 00:00:00 2001 From: Ben Boeckel Date: Sun, 23 Aug 2026 22:00:10 -0400 Subject: [PATCH 08/10] gitlab-ci: add a job to verify the devcontainer in CI --- .gitlab-ci.yml | 10 ++++++++++ .gitlab/os-linux.yml | 15 +++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6418ad4cab..6e333b05bb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -193,6 +193,16 @@ l:clazy-fedora44: - .rules needs: [] +t:devcontainer-fedora44: + extends: + - .fedora44 + - .cmake_test_devcontainer + - .linux_x86_64_priv_tags + - .rules + variables: + CMAKE_CI_JOB_NIGHTLY: "true" + needs: [] + # Coverage builds b:fedora44-gcc-gcov: diff --git a/.gitlab/os-linux.yml b/.gitlab/os-linux.yml index 2d66350260..1432537338 100644 --- a/.gitlab/os-linux.yml +++ b/.gitlab/os-linux.yml @@ -651,6 +651,13 @@ - docker - linux-x86_64 +.linux_x86_64_priv_tags: + tags: + - cmake + - docker + - linux-x86_64 + - privileged + .linux_x86_64_v3_tags: tags: - cmake @@ -963,3 +970,11 @@ -DCMake_SPHINX_CMAKE_ORG_OUTDATED=$CMAKE_CI_SPHINX_OUTDATED -DCMake_VERSION_NO_GIT=$CMAKE_CI_VERSION_NO_GIT - ninja + +### Devcontainer testing + +.cmake_test_devcontainer: + stage: test + script: + - dnf install -y --setopt=install_weak_deps=False podman + - .gitlab/ci/devcontainer-run.sh From 3cfbea10e4a4882b65f448eae64d544492f5690d Mon Sep 17 00:00:00 2001 From: Taylor Braun-Jones Date: Wed, 9 Sep 2026 18:35:56 -0400 Subject: [PATCH 09/10] devcontainer: Name the workspace path explicitly A tool that opens the container picks a workspace path of its own when the configuration names none, and `.gitlab/ci/devcontainer-run.sh` mounts the source tree at a path it spells itself. The two disagreed: an editor opened the tree at `/workspaces/cmake` while CI used the `/home/cmake-dev/workspace` that `create_user.sh` prepares for it. Say which one it is, so that a path means the same thing everywhere. --- .devcontainer/devcontainer.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d7f7fad576..dc2f900463 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,6 +4,13 @@ "dockerfile": "Dockerfile" }, "remoteUser": "cmake-dev", + // Name the workspace path explicitly rather than take the default a tool + // picks for itself, so that the path is the same under every tool and in + // the CI job of `.gitlab/ci/devcontainer-run.sh`, which mounts the source + // tree itself. `create_user.sh` creates the directory and gives it to the + // container user. + "workspaceFolder": "/home/cmake-dev/workspace", + "workspaceMount": "source=${localWorkspaceFolder},target=/home/cmake-dev/workspace,type=bind", "containerEnv": { "GITLAB_HOST": "gitlab.kitware.com" }, From f1b6b3a6f05229d257a17e35749cfbc5763f2f3a Mon Sep 17 00:00:00 2001 From: Taylor Braun-Jones Date: Wed, 9 Sep 2026 18:35:57 -0400 Subject: [PATCH 10/10] devcontainer: Run local customization hooks through the container's life The container ran a developer's own `hooks/root.sh` and `hooks/user.sh` while the image was built, and nothing of theirs afterwards. A customization that has to start something, or to reach the source tree, had nowhere to run: the tree is not mounted during the build, and the one lifecycle command the configuration used was spoken for by the status report. Run an optional script per phase through `run-hooks.sh`, and offer five: `initialize` on the host, `build` in the image, and `post-create`, `post-start` and `post-attach` in the container. A failing `build` hook fails the image build, because an image whose customizations did not apply is quietly wrong; the rest are reported and otherwise ignored, so that a typo in a personal hook cannot leave its author unable to open the container in order to fix it. The two build hooks become one, running as the container user, who may `sudo`: one hook that reaches either user is simpler to write against than two that each reach one. Every hook is told where it lives, and every hook but the build one is given a directory to keep state in, beside the hooks rather than among them because state is written by whatever they start rather than by hand. `.dockerignore` keeps that directory out of the build context, which state written as `root` would otherwise make unreadable. --- .devcontainer/.dockerignore | 7 +++ .devcontainer/.gitignore | 1 + .devcontainer/Dockerfile | 31 ++++++----- .devcontainer/devcontainer.json | 38 ++++++++++++- .devcontainer/run-hooks.sh | 47 +++++++++++++++++ Help/dev/devcontainer.rst | 94 ++++++++++++++++++++++++++------- 6 files changed, 181 insertions(+), 37 deletions(-) create mode 100644 .devcontainer/.dockerignore create mode 100644 .devcontainer/.gitignore create mode 100755 .devcontainer/run-hooks.sh diff --git a/.devcontainer/.dockerignore b/.devcontainer/.dockerignore new file mode 100644 index 0000000000..b12276e573 --- /dev/null +++ b/.devcontainer/.dockerignore @@ -0,0 +1,7 @@ +# The image build sends this directory to the container engine as its build +# context. The state hooks keep at run time is of no use to a build and, +# having been written inside the container, may well be owned by a user the +# build cannot even read it as. Leave it behind. +# +# See `Help/dev/devcontainer.rst`. +state/ diff --git a/.devcontainer/.gitignore b/.devcontainer/.gitignore new file mode 100644 index 0000000000..a27475ad10 --- /dev/null +++ b/.devcontainer/.gitignore @@ -0,0 +1 @@ +/state/ diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index c6b5343aa7..2e105dcba7 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -61,24 +61,23 @@ RUN --mount=type=bind,source=install_glab.sh,target=/root/install_glab.sh \ RUN --mount=type=bind,source=create_user.sh,target=/root/create_user.sh \ sh /root/create_user.sh ${USERNAME} ${USER_UID} ${USER_GID} -# Run the optional local customization scripts. They are ignored by Git so -# that developers may customize the container without modifying tracked files -# or risking that the customizations end up in a commit. See -# `Help/dev/devcontainer.rst`. +# Run the optional local customization hook for the build, if the developer +# has written one. It is ignored by Git so that developers may customize the +# container without modifying tracked files or risking that the customizations +# end up in a commit. See `Help/dev/devcontainer.rst`. +# +# It runs as the container user, who may `sudo`, rather than as `root`: one +# hook that can reach either is simpler to write against than two that each +# reach one. Mount it beside the dispatcher that runs it, under the same +# parent it has in the source tree, so that the dispatcher locates it here the +# same way it does when a container lifecycle command runs it. # # `USER` sets neither the working directory nor `HOME`, and BuildKit passes a # `RUN` only the environment the image records, which names just `PATH`. Say -# where each hook runs and whose home it writes to, so that a hook may spell a -# path relative to either. -WORKDIR /root -RUN --mount=type=bind,source=hooks,target=/opt/cmake-dev-hooks \ - if test -f /opt/cmake-dev-hooks/root.sh; then \ - HOME=/root sh -e /opt/cmake-dev-hooks/root.sh; \ - fi - +# where the hook runs and whose home it writes to, so that it may spell a path +# relative to either. USER ${USERNAME} WORKDIR /home/${USERNAME} -RUN --mount=type=bind,source=hooks,target=/opt/cmake-dev-hooks \ - if test -f /opt/cmake-dev-hooks/user.sh; then \ - HOME=/home/${USERNAME} sh -e /opt/cmake-dev-hooks/user.sh; \ - fi +RUN --mount=type=bind,source=run-hooks.sh,target=/opt/cmake-dev/run-hooks.sh \ + --mount=type=bind,source=hooks,target=/opt/cmake-dev/hooks \ + HOME=/home/${USERNAME} sh /opt/cmake-dev/run-hooks.sh build diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index dc2f900463..80a508c23d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -30,7 +30,43 @@ "target": "/home/cmake-dev/.config/glab-cli" } ], - "postAttachCommand": "${containerWorkspaceFolder}/.devcontainer/setup-status.sh", + // Run the optional local customization hooks. Each phase is a no-op unless + // the developer has written that hook. See `Help/dev/devcontainer.rst`. + // + // Each is written as a named entry, the form that runs entries concurrently + // and says which one is speaking. Only `postAttachCommand` has two, but + // naming the hook everywhere keeps its output labeled the same way, and + // leaves room for a second entry beside it. + // + // `initializeCommand` runs on the host, so unlike the others it needs a + // POSIX shell there; naming `sh` explicitly, in the form that starts no + // shell of its own, is what makes that work outside a Unix host. + "initializeCommand": { + "hooks": [ + "sh", + "${localWorkspaceFolder}/.devcontainer/run-hooks.sh", + "initialize" + ] + }, + "postCreateCommand": { + "hooks": [ + "${containerWorkspaceFolder}/.devcontainer/run-hooks.sh", + "post-create" + ] + }, + "postStartCommand": { + "hooks": [ + "${containerWorkspaceFolder}/.devcontainer/run-hooks.sh", + "post-start" + ] + }, + "postAttachCommand": { + "setup-status": ["${containerWorkspaceFolder}/.devcontainer/setup-status.sh"], + "hooks": [ + "${containerWorkspaceFolder}/.devcontainer/run-hooks.sh", + "post-attach" + ] + }, "customizations": { "vscode": { "extensions": [ diff --git a/.devcontainer/run-hooks.sh b/.devcontainer/run-hooks.sh new file mode 100755 index 0000000000..ef311ffdae --- /dev/null +++ b/.devcontainer/run-hooks.sh @@ -0,0 +1,47 @@ +#!/bin/sh + +# Run the optional local customization hook for one phase of the development +# container's life, named as the sole argument, if the developer has written +# one. See `Help/dev/devcontainer.rst`. +# +# The hooks live beside this script, in a directory Git ignores in its +# entirety, so customizations never appear in a commit and survive updates to +# the tracked container definition. + +set -eu + +readonly phase="$1" +readonly devcontainer_dir="$(cd -- "$(dirname -- "$0")" && pwd)" +readonly hooks_dir="$devcontainer_dir/hooks" +readonly hook="$hooks_dir/$phase.sh" + +test -f "$hook" || exit 0 + +# Tell the hook where its own directory is, so that a hook needing a file it +# brought along need not work out where it was installed. +CMAKE_DEVCONTAINER_HOOKS_DIR="$hooks_dir" +export CMAKE_DEVCONTAINER_HOOKS_DIR + +# A failed `build` hook fails the image build: the image must be reproducible, +# and a customization that did not apply would leave it quietly wrong. Every +# other phase runs against a container that already exists, where the same +# strictness would turn a typo in a personal hook into an environment its +# author can no longer open in order to fix it. Report and carry on instead. +if test "$phase" = build; then + # The build sees this directory through a read-only bind mount, and keeps + # nothing a later phase could read back: whatever this hook writes it + # writes into the image. So there is no state directory to offer it. + exec sh -e "$hook" +fi + +# Every other phase runs against the bind-mounted source tree, where a hook +# may keep state that outlives the container. It sits beside the hooks rather +# than among them: the hooks are written by hand and worth carrying to another +# clone, while this is written by whatever they start and worth carrying +# nowhere. `.dockerignore` also leaves it out of the build context, which a +# hook writing here as `root` would otherwise make unreadable to the build. +CMAKE_DEVCONTAINER_STATE_DIR="$devcontainer_dir/state" +export CMAKE_DEVCONTAINER_STATE_DIR +mkdir -p "$CMAKE_DEVCONTAINER_STATE_DIR" + +sh -e "$hook" || echo "run-hooks.sh: $phase hook failed; continuing" >&2 diff --git a/Help/dev/devcontainer.rst b/Help/dev/devcontainer.rst index ef88bfe2af..c73f462e26 100644 --- a/Help/dev/devcontainer.rst +++ b/Help/dev/devcontainer.rst @@ -217,40 +217,94 @@ Local Customization =================== The container is meant to be an unconstrained space that each developer may -adapt. Two optional scripts, if present, run while the container image is -built: +adapt. `.devcontainer/run-hooks.sh`_ runs an optional script, if one is +present, at each of five points in the container's life: -``.devcontainer/hooks/root.sh`` - Runs as ``root``, e.g. to install additional packages. +``.devcontainer/hooks/initialize.sh`` + Runs on the host, before the container is created or started, e.g. to + prepare something the container goes on to use. -``.devcontainer/hooks/user.sh`` - Runs as the unprivileged container user, e.g. to populate a shell - configuration file. +``.devcontainer/hooks/build.sh`` + Runs while the image is built, e.g. to install additional packages. -Each runs in the home directory of the user it runs as, with ``HOME`` naming -that directory. +``.devcontainer/hooks/post-create.sh`` + Runs once, when the container is created, and unlike ``build.sh`` runs with + the source tree mounted, e.g. to prepare something in the work tree itself. + +``.devcontainer/hooks/post-start.sh`` + Runs each time the container starts, e.g. to start a background service. + Note that a container may be started by a tool that never attaches to it. + +``.devcontainer/hooks/post-attach.sh`` + Runs each time a tool attaches to the container, concurrently with the + report described under `GitLab Authentication`_ above rather than before or + after it, so expect whatever it prints to interleave with that report. + +``build.sh`` runs as the container user, in that user's home directory, rather +than as ``root``; reach for ``sudo`` for whatever needs privilege. One hook +that can be either user is simpler to write against than two that each can be +one. Bear in mind that ``sudo`` resets ``HOME`` to ``root``'s, so pass ``-H`` +or ``-E`` where a command cares which home it writes to. The three +container hooks that follow it likewise run as the container user, in the +workspace directory; ``post-start.sh`` and ``post-attach.sh`` run again on +every start and attach, so write those two to be repeatable. + +Each hook is given ``CMAKE_DEVCONTAINER_HOOKS_DIR``, naming the ``hooks`` +directory itself, so that a hook needing a file it brought along need not work +out where it was installed. Every hook but ``build.sh`` is given +``CMAKE_DEVCONTAINER_STATE_DIR`` as well, a directory to keep runtime state +in: it is part of the source tree, bind-mounted from the host, so what a hook +leaves there outlives the container. It sits beside the ``hooks`` directory +rather than inside it, because the two are worth different things: hooks are +written by hand and worth carrying to another clone, while state is written by +whatever they start and worth carrying nowhere. +`.devcontainer/.dockerignore`_ also keeps it out of the image build context, +which state written as ``root`` would otherwise make unreadable. ``build.sh`` +is given neither a state directory nor a writable ``hooks`` directory, because +a build keeps nothing a later phase could read back: whatever it writes, it +writes into the image. + +A failing ``build.sh`` fails the image build, because an image whose +customizations did not apply is quietly wrong. The other three are reported +and otherwise ignored: they run against a container that already exists, where +the same strictness would turn a typo into an environment its author can no +longer open in order to fix it. The whole ``hooks`` directory is ignored by Git, apart from its ``.gitignore``, so customizations never appear in a commit, may bring along whatever other files they need, and are preserved across updates to the -tracked container definition. For example, to add a package and a shell -alias: +tracked container definition. For example, to add a package, a shell alias, +and a service that runs for as long as the container does: .. code-block:: console - $ cat > .devcontainer/hooks/root.sh <<'EOF' - apt-get update && apt-get install -y tmux - EOF - $ cat > .devcontainer/hooks/user.sh <<'EOF' + $ cat > .devcontainer/hooks/build.sh <<'EOF' + sudo apt-get update && sudo apt-get install -y tmux echo "alias b='cmake --build build'" >> ~/.bashrc EOF + $ cat > .devcontainer/hooks/post-start.sh <<'EOF' + pidof my-service > /dev/null || + my-service --daemon --state "$CMAKE_DEVCONTAINER_STATE_DIR/my-service" + EOF -Rebuild the container to apply them, e.g. with the -``Dev Containers: Rebuild Container`` command in Visual Studio Code. +Rebuild the container to apply a new or changed ``build.sh``, e.g. with the +``Dev Containers: Rebuild Container`` command in Visual Studio Code. The +other three hooks are read afresh each time they run. -Larger or longer-lived changes may of course be made by editing -`.devcontainer/Dockerfile`_ or `.devcontainer/devcontainer.json`_ directly, -but take care not to commit them accidentally. +``initialize.sh`` is the one hook that runs outside the container, so it is +also the one that depends on the host: it needs ``sh`` on the ``PATH`` there. +That is a given on a Unix host and, on Windows, comes with Git for Windows. +Some things a container needs must be settled before it exists, and so cannot +come from a hook: added capabilities, extra mounts, `Dev Container Features`_, +and arguments to the container engine all belong to +`.devcontainer/devcontainer.json`_. Those, and any larger or longer-lived +change, may of course be made by editing that file or +`.devcontainer/Dockerfile`_ directly, but take care not to commit them +accidentally. + +.. _`.devcontainer/run-hooks.sh`: ../../.devcontainer/run-hooks.sh +.. _`.devcontainer/.dockerignore`: ../../.devcontainer/.dockerignore +.. _`Dev Container Features`: https://containers.dev/features .. _`.devcontainer/Dockerfile`: ../../.devcontainer/Dockerfile .. _`.devcontainer/devcontainer.json`: ../../.devcontainer/devcontainer.json