From d326c8afd5c914f43fa630a9761b53c7dc509a64 Mon Sep 17 00:00:00 2001 From: Taylor Braun-Jones Date: Thu, 20 Aug 2026 14:20:41 +0000 Subject: [PATCH] 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