Visual Studio Code, C++, and a Docker Container

Illustration: Visual Studio Code, C++, and a Docker Container

Microsoft announced a while ago that it added support for using Visual Studio Code remotely. This means the UI can run on your local computer, but all the heavy lifting can be done on a remote server. At PSPDFKit, we have a pretty big C++ codebase, which is something that can easily suffer from too little computer power, so we figured we’d look into using the new remote support!

The Idea

We automatically test our C++ code on each platform we support, but the easiest setup we have is for testing on Linux using Docker. With this in mind, we started looking at the remote Docker support of VS Code. It also made a lot of sense because we could use the same compiler and development environment we use for our tests.

The things we wanted to have were:

  • CMake support
  • Code navigation
  • Quick auto-complete
  • Debugger support (LLDB preferred)
  • Google Test runner

If all of this were to work out, we’d be able to work more quickly than we can now, even using an old laptop!

Local Setup

Before attempting to do all of this remotely, we tried getting a complete environment set up for local development. We needed extensions for this, and luckily, VS Code has a ton of them! We won’t go too much into the specifics of configuring them because this is out of scope for this blog post, but in general, they all mostly just worked.

Here’s what we decided on:

For CMake, we used the now-Microsoft-maintained CMake Tools.

Our code works on many different compilers, but most platforms use Clang. This made us decide to try the clangd extension first, and we’re happy to report that it works really, really well. On macOS, we had to add --query-driver=/usr/bin/clang++ as an argument for clangd to work correctly, but apart from that, we didn’t need to do anything else.

We prefer using LLDB as our debugging tool of choice, mostly because this is what is automatically available when using Xcode, and we wrote our own small extensions to make things easier. We found the VS Code extension CodeLLDB worked pretty much out of the box.

For testing, we found C++ TestMate, which also just worked. After compiling our test binary, it simply picked it up and showed the available tests, and we were able to run them.

With all this done, we were able to move on to getting this to work in a Docker container!

Docker Setup

The first thing we needed to do was install the remote development extension. This installed three extensions: Remote - SSH, Remote - Containers, and Remote - WSL. In this post, we’ll be using the Remote - Containers extension. Microsoft has documentation on how to get Docker installed here.

The Remote - Containers extension allows you to add a new container configuration, and it will set up default values for you. This can all be found in the documentation. Here we’ll show how our setup looks.

The configuration consists of two files. First, there’s a Dockerfile that will be our development environment, and second, there’s a devcontainer.json that contains information on how to run the Dockerfile.

Dockerfile

Microsoft has several base Docker images for use with VS Code, one of which we used as our base. Because we’re using Debian, we used mcr.microsoft.com/vscode/devcontainers/base:0-debian-10.

Here’s the Dockerfile we ended up with:

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#-------------------------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
#-------------------------------------------------------------------------------------------------------------

# To fully customize the contents of this image, use the following Dockerfile as a base and add the RUN statement from this file:
# https://github.com/microsoft/vscode-dev-containers/blob/v0.112.0/containers/debian-10-git/.devcontainer/Dockerfile
FROM mcr.microsoft.com/vscode/devcontainers/base:0-debian-10

# This Dockerfile's base image has a non-root user with sudo access. Use the "remoteUser"
# property in devcontainer.json to use it. On Linux, the container user's GID/UIDs
# will be updated to match your local UID/GID (when using the dockerFile property).
# See https://aka.ms/vscode-remote/containers/non-root-user for details.
ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=$USER_UID

# Avoid warnings by switching to noninteractive
ENV DEBIAN_FRONTEND=noninteractive

ARG LLVM_VERSION=10
ARG LLVM_GPG_FINGERPRINT=6084F3CF814B57C1CF12EFD515CF4D18AF4F7421

# Configure apt and install packages
RUN apt-get update \
    #
    # Install C++ tools
    && apt-get -y install \
        build-essential \
        cmake \
        git \
        git-lfs \
        ninja-build \
        ccache \
        zsh \
    #
    # [Optional] Update UID/GID if needed
    && if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \
        groupmod --gid $USER_GID $USERNAME \
        && usermod --uid $USER_UID --gid $USER_GID $USERNAME \
        && chown -R $USER_UID:$USER_GID /home/$USERNAME; \
    fi

RUN apt-get update \
    && wget -O- https://apt.llvm.org/llvm-snapshot.gpg.key| apt-key add - \
    && echo "deb http://apt.llvm.org/buster/ llvm-toolchain-buster-${LLVM_VERSION} main" >> /etc/apt/sources.list \
    && apt-get update \
    && apt-get -y install --no-install-recommends \
        llvm-${LLVM_VERSION} \
        clang-${LLVM_VERSION} \
        lldb-${LLVM_VERSION} \
        libc++-${LLVM_VERSION}-dev \
        libc++abi-${LLVM_VERSION}-dev \
        clang-tidy-${LLVM_VERSION} \
        clangd-${LLVM_VERSION} \
    && ln -s /usr/bin/clang-tidy-${LLVM_VERSION} /usr/bin/clang-tidy \
    && ln -s /usr/bin/lldb-${LLVM_VERSION} /usr/bin/lldb \
    && ln -sf /usr/bin/lldb-server-${LLVM_VERSION} /usr/lib/llvm-10/bin/lldb-server-${LLVM_VERSION}.0.1 \
    # Fixes clangd
    && ln -sf /usr/lib/llvm-${LLVM_VERSION}/include/c++/v1 /usr/include/c++/v1

RUN apt-get autoremove -y \
    && apt-get clean -y

# Switch back to dialog for any ad-hoc use of apt-get
ENV DEBIAN_FRONTEND=dialog

ENV CC="/usr/bin/clang-${LLVM_VERSION}" \
    CXX="/usr/bin/clang++-${LLVM_VERSION}" \
    COV="/usr/bin/llvm-cov-${LLVM_VERSION}" \
    LLDB="/usr/bin/lldb-${LLVM_VERSION}"

It’s based on an example Dockerfile created by VS Code, along with additions from our own Dockerfile that we use to compile our code on our build servers.

devcontainer.json

Configuring the devcontainer.json was the next step. Here’s the one we’re using, with extra comments:

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.112.0/containers/cpp
{
  "name": "Core development",
  // Specifies where to find the Dockerfile to use.
  "dockerFile": "Dockerfile",
  // We add extra permissions which are needed for debugging, etc.
  "runArgs": [
    "--cap-add=SYS_PTRACE",
    "--security-opt",
    "seccomp=unconfined",
    "--userns=host"
  ],

  // Set *default* container-specific `settings.json` values on container creation.
  "settings": {
    "terminal.integrated.shell.linux": "/bin/zsh",
    "cmake.environment": {
      // We configure clang-10 as our default compiler.
      "CC": "/usr/bin/clang-10",
      "CXX": "/usr/bin/clang++-10"
    },
    // Tell the CMake extensions where to find CMake.
    "cmake.cmakePath": "/usr/local/bin/cmake"
  },

  // Add the IDs of extensions you want installed when the container is created.
  "extensions": [
    "ms-vscode.cmake-tools",
    "llvm-vs-code-extensions.vscode-clangd",
    "vadimcn.vscode-lldb",
    "matepek.vscode-catch2-test-adapter"
  ],

  // Specifies where the workspace can be found in the running container.
  "workspaceFolder": "/workspace/core",
  // This mounts the workspace. There are several options on how to configure this, but
  // we decided to checkout our repository in `$HOME/Work/PSPDFKit`.
  "workspaceMount": "source=${localEnv:HOME}/Work/PSPDFKit,target=/workspace,type=bind,consistency=cached"
}

With both of these files, we were able to try running this on a local Docker!

Running Locally

We opened the Command Palette (F1) and searched for Remote-Container: Open Folder in Container...

This asked for the folder. We navigated to the folder containing our .devcontainer and opened it.

This caused VS Code to build and start our container locally, so we were able to begin trying out if it all worked.

We could see which environment we were running in by looking at the bottom-left of VS Code:

Once we got everything running locally in Docker, we were able to move on to getting it to run remotely!

Remote Docker

We needed SSH access to a host that had Docker installed, and we had to make sure our code was checked out at the location specified in the devcontainer.json.

In our local VS Code configuration, we added the following:

1
2
3
{
  "docker.host": "ssh://pat@your-server"
}

And surprisingly, that was it! We simply chose Remote-Container: Open Folder in Container... again and VS Code connected to our configured server, built the Docker container, and connected to it.

Microsoft has some more information about this here.

Summary

It was surprisingly simple to configure VS Code for remote C++ development. This enables us to have much more CPU power available, which big C++ projects definitely need. In addition, VS Code is one of the most responsive editors I’ve ever used.

PSPDFKit Newsletter

Subscribe to our newsletter for more articles like this.