Skip to content

[cpp] - Introducing new option for c++ template to install gcc from source. #337

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/cpp/.devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
FROM mcr.microsoft.com/devcontainers/cpp:1-${templateOption:imageVariant}

ARG REINSTALL_GCC_VERSION_FROM_SOURCE="${templateOption:reinstallGccVersionFromSource}"

# Optionally install the gcc from source & override the default version
COPY ./reinstall-gcc.sh /tmp/

RUN if [ "${REINSTALL_GCC_VERSION_FROM_SOURCE}" != "none" ]; then \
chmod +x /tmp/reinstall-gcc.sh && /tmp/reinstall-gcc.sh ${REINSTALL_GCC_VERSION_FROM_SOURCE}; \
fi \
&& rm -f /tmp/reinstall-gcc.sh

ARG REINSTALL_CMAKE_VERSION_FROM_SOURCE="${templateOption:reinstallCmakeVersionFromSource}"

# Optionally install the cmake for vcpkg
Expand Down
167 changes: 167 additions & 0 deletions src/cpp/.devcontainer/reinstall-gcc.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
#!/usr/bin/env bash
#-------------------------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
#-------------------------------------------------------------------------------------------------------------
#
set -euo pipefail

# Default GCC version
GCC_VERSION="${1:-12.2.0}"

# Set source and installation directories
GCC_SRC_DIR="/usr/local/src/gcc-${GCC_VERSION}"
GCC_INSTALL_DIR="/usr/local/gcc-${GCC_VERSION}"

# Define GPG keys for verification
# Source: https://gcc.gnu.org/releases.html
# The gnu mirros sites and GPG keys https://gcc.gnu.org/mirrors.html
# Note: The keys are subject to change, please verify from the official source as given above.
# The keys are used to verify the downloaded GCC tarball.
GCC_KEYS=(
# 1024D/745C015A 1999-11-09 Gerald Pfeifer <[email protected]>
"B215C1633BCA0477615F1B35A5B3A004745C015A"
# 1024D/B75C61B8 2003-04-10 Mark Mitchell <[email protected]>
"B3C42148A44E6983B3E4CC0793FA9B1AB75C61B8"
# 1024D/902C9419 2004-12-06 Gabriel Dos Reis <[email protected]>
"90AA470469D3965A87A5DCB494D03953902C9419"
# 1024D/F71EDF1C 2000-02-13 Joseph Samuel Myers <[email protected]>
"80F98B2E0DAB6C8281BDF541A7C8C3B2F71EDF1C"
# 2048R/FC26A641 2005-09-13 Richard Guenther <[email protected]>
"7F74F97C103468EE5D750B583AB00996FC26A641"
# 1024D/C3C45C06 2004-04-21 Jakub Jelinek <[email protected]>
"33C235A34C46AA3FFB293709A328C3A2C3C45C06"
# 4096R/09B5FA62 2020-05-28 Jakub Jelinek <[email protected]>
"D3A93CAD751C2AF4F8C7AD516C35B99309B5FA62"
)
GCC_MIRRORS=(
"https://ftpmirror.gnu.org/gcc"
"https://mirrors.kernel.org/gnu/gcc"
"https://bigsearcher.com/mirrors/gcc/releases"
"http://www.netgull.com/gcc/releases"
"https://sourceware.org/pub/gcc/releases"
"ftp://ftp.gnu.org/gnu/gcc"
)

# Check if input GCC_VERSION is greater than installed GCC version
if command -v gcc &>/dev/null; then
installed_version=$(gcc -dumpfullversion)
if [ "$(printf '%s\n' "$installed_version" "$GCC_VERSION" | sort -V | tail -n1)" = "$installed_version" ]; then
echo "Installed GCC version ($installed_version) is equal or newer than requested version ($GCC_VERSION). Skipping installation."

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder why the installation should be skipped in this scenario? I interpret the introduced reinstallGccVersionFromSource option as a request from the image user to install one specific GCC version. This condition here turns that option into a specifying a minimum GCC version instead of a specific version.

Copy link
Contributor Author

@Kaniska244 Kaniska244 May 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rationale behind this is that if the default GCC version is higher than that of the one specified in the template, then its better to retain the default one as it would already contain better features compared to the given GCC version in the template. Besides the actual issue was created to be able to use GCC 14 & 15 versions which were not available by default with the image.

exit 0
fi
fi

# Executes the provided command with 'sudo' if the current user is not root; otherwise, runs the command directly.
sudo_if() {
COMMAND="$*"

if [ "$(id -u)" -ne 0 ]; then
sudo $COMMAND
else
$COMMAND
fi
}

# Install required dependencies
sudo_if apt-get update
sudo_if apt-get install -y \
dpkg-dev flex gnupg build-essential wget curl

# Function to fetch GCC source and signature
fetch_gcc() {
local file="$1"
for mirror in "${GCC_MIRRORS[@]}"; do
if curl -fL "${mirror}/gcc-${GCC_VERSION}/${file}" -o "${file}"; then
return 0
fi
done
echo "Error: Failed to download ${file} from all mirrors" >&2
exit 1
}

# Function to robustly download files with retries
robust_wget() {
local url="$1"
local output="$2"
local retries=5
local wait_seconds=5

for ((i=1; i<=retries; i++)); do
if wget -O "$output" "$url"; then
return 0
else
echo "Attempt $i failed for $url. Retrying in $wait_seconds seconds..."
sleep "$wait_seconds"
fi
done

echo "Failed to download $url after $retries attempts."
exit 1
}

cleanup() {
echo "Cleaning up temporary files..."
rm -rf "${build_dir:-}" "${GCC_SRC_DIR:-}"
sudo_if apt-get clean
}

# Trap EXIT signal to ensure cleanup runs
trap cleanup EXIT

# Download GCC source and signature
fetch_gcc "gcc-${GCC_VERSION}.tar.xz"
fetch_gcc "gcc-${GCC_VERSION}.tar.xz.sig"

# Verify the signature
export GNUPGHOME=$(mktemp -d)
for key in "${GCC_KEYS[@]}"; do
gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key"
done
gpg --batch --verify "gcc-${GCC_VERSION}.tar.xz.sig" "gcc-${GCC_VERSION}.tar.xz"
rm -rf "$GNUPGHOME"

# Extract GCC source
sudo_if mkdir -p "${GCC_SRC_DIR}"
sudo_if tar -xf "gcc-${GCC_VERSION}.tar.xz" -C "${GCC_SRC_DIR}" --strip-components=1
rm "gcc-${GCC_VERSION}.tar.xz" "gcc-${GCC_VERSION}.tar.xz.sig"

# Prepare GCC source
cd "${GCC_SRC_DIR}"
./contrib/download_prerequisites
for f in config.guess config.sub; do
robust_wget "https://git.savannah.gnu.org/cgit/config.git/plain/$f?id=7d3d27baf8107b630586c962c057e22149653deb" "$f"
find -mindepth 2 -name "$f" -exec cp -v "$f" '{}' ';'
done

# Build and install GCC
build_dir=$(mktemp -d)
cd "$build_dir"
"${GCC_SRC_DIR}/configure" \
--prefix="${GCC_INSTALL_DIR}" \
--disable-multilib \
--enable-languages=c,c++
make -j "$(nproc)"
sudo_if make install-strip

# Update alternatives to use the new GCC version as the default version.
sudo_if update-alternatives --install /usr/bin/gcc gcc ${GCC_INSTALL_DIR}/bin/gcc 999
sudo_if update-alternatives --install /usr/bin/g++ g++ ${GCC_INSTALL_DIR}/bin/g++ 999
sudo_if update-alternatives --install /usr/bin/gcc-ar gcc-ar ${GCC_INSTALL_DIR}/bin/gcc-ar 999
sudo_if update-alternatives --install /usr/bin/gcc-nm gcc-nm ${GCC_INSTALL_DIR}/bin/gcc-nm 999
sudo_if update-alternatives --install /usr/bin/gcc-ranlib gcc-ranlib ${GCC_INSTALL_DIR}/bin/gcc-ranlib 999
sudo_if update-alternatives --install /usr/bin/gcov gcov ${GCC_INSTALL_DIR}/bin/gcov 999
sudo_if update-alternatives --install /usr/bin/gcov-dump gcov-dump ${GCC_INSTALL_DIR}/bin/gcov-dump 999
sudo_if update-alternatives --install /usr/bin/gcov-tool gcov-tool ${GCC_INSTALL_DIR}/bin/gcov-tool 999


# Verify installation
echo "Verifying GCC installation..."
gcc --version
g++ --version
if [ $? -ne 0 ]; then
echo "GCC installation failed."
exit 1
fi

echo "GCC ${GCC_VERSION} has been installed successfully in ${GCC_INSTALL_DIR}!"
1 change: 1 addition & 0 deletions src/cpp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Develop C++ applications on Linux. Includes Debian C++ build tools.
|-----|-----|-----|-----|
| imageVariant | Debian / Ubuntu version (use Debian 12, Debian 11, Ubuntu 24.04, and Ubuntu 22.04 on local arm64/Apple Silicon): | string | debian-11 |
| reinstallCmakeVersionFromSource | Install CMake version different from what base image has already installed. | string | none |
| reinstallGccVersionFromSource | Install gcc version from source different from what base image has already installed. | string | none |

This template references an image that was [pre-built](https://containers.dev/implementors/reference/#prebuilding) to automatically include needed devcontainer.json metadata.

Expand Down
14 changes: 13 additions & 1 deletion src/cpp/devcontainer-template.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"id": "cpp",
"version": "3.0.2",
"version": "3.1.0",
"name": "C++",
"description": "Develop C++ applications on Linux. Includes Debian C++ build tools.",
"documentationURL": "https://github.com/devcontainers/templates/tree/main/src/cpp",
Expand Down Expand Up @@ -28,6 +28,18 @@
"3.22.2"
],
"default": "none"
},
"reinstallGccVersionFromSource": {
"type": "string",
"description": "Install GCC version different from what base image has already installed.",
"proposals": [
"none",
"15.1.0",
"14.2.0",
"14.1.0",
"13.3.0"
],
"default": "none"
}
},
"platforms": ["C++"],
Expand Down
166 changes: 166 additions & 0 deletions test/cpp/test-utils.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#!/bin/bash
SCRIPT_FOLDER="$(cd "$(dirname $0)" && pwd)"
USERNAME=${1:-vscode}

if [ -z $HOME ]; then
HOME="/root"
fi

FAILED=()

echoStderr()
{
echo "$@" 1>&2
}

check() {
LABEL=$1
shift
echo -e "\n🧪 Testing $LABEL"
if "$@"; then
echo "✅ Passed!"
return 0
else
echoStderr "❌ $LABEL check failed."
FAILED+=("$LABEL")
return 1
fi
}

check-version-ge() {
LABEL=$1
CURRENT_VERSION=$2
REQUIRED_VERSION=$3
shift
echo -e "\n🧪 Testing $LABEL: '$CURRENT_VERSION' is >= '$REQUIRED_VERSION'"
local GREATER_VERSION=$((echo ${CURRENT_VERSION}; echo ${REQUIRED_VERSION}) | sort -V | tail -1)
if [ "${CURRENT_VERSION}" == "${GREATER_VERSION}" ]; then
echo "✅ Passed!"
return 0
else
echoStderr "❌ $LABEL check failed."
FAILED+=("$LABEL")
return 1
fi
}

checkMultiple() {
PASSED=0
LABEL="$1"
echo -e "\n🧪 Testing $LABEL."
shift; MINIMUMPASSED=$1
shift; EXPRESSION="$1"
while [ "$EXPRESSION" != "" ]; do
if $EXPRESSION; then ((PASSED++)); fi
shift; EXPRESSION=$1
done
if [ $PASSED -ge $MINIMUMPASSED ]; then
echo "✅ Passed!"
return 0
else
echoStderr "❌ $LABEL check failed."
FAILED+=("$LABEL")
return 1
fi
}

checkOSPackages() {
LABEL=$1
shift
echo -e "\n🧪 Testing $LABEL"
if dpkg-query --show -f='${Package}: ${Version}\n' "$@"; then
echo "✅ Passed!"
return 0
else
echoStderr "❌ $LABEL check failed."
FAILED+=("$LABEL")
return 1
fi
}

checkExtension() {
# Happens asynchronusly, so keep retrying 10 times with an increasing delay
EXTN_ID="$1"
TIMEOUT_SECONDS="${2:-10}"
RETRY_COUNT=0
echo -e -n "\n🧪 Looking for extension $1 for maximum of ${TIMEOUT_SECONDS}s"
until [ "${RETRY_COUNT}" -eq "${TIMEOUT_SECONDS}" ] || \
[ ! -e $HOME/.vscode-server/extensions/${EXTN_ID}* ] || \
[ ! -e $HOME/.vscode-server-insiders/extensions/${EXTN_ID}* ] || \
[ ! -e $HOME/.vscode-test-server/extensions/${EXTN_ID}* ] || \
[ ! -e $HOME/.vscode-remote/extensions/${EXTN_ID}* ]
do
sleep 1s
(( RETRY_COUNT++ ))
echo -n "."
done

if [ ${RETRY_COUNT} -lt ${TIMEOUT_SECONDS} ]; then
echo -e "\n✅ Passed!"
return 0
else
echoStderr -e "\n❌ Extension $EXTN_ID not found."
FAILED+=("$LABEL")
return 1
fi
}

checkCommon()
{
PACKAGE_LIST="apt-utils \
git \
openssh-client \
less \
iproute2 \
procps \
curl \
wget \
unzip \
nano \
jq \
lsb-release \
ca-certificates \
apt-transport-https \
dialog \
gnupg2 \
libc6 \
libgcc1 \
libgssapi-krb5-2 \
liblttng-ust1 \
libstdc++6 \
zlib1g \
locales \
sudo"

# Actual tests
checkOSPackages "common-os-packages" ${PACKAGE_LIST}
check "non-root-user" id ${USERNAME}
check "locale" [ $(locale -a | grep en_US.utf8) ]
check "sudo" sudo echo "sudo works."
check "zsh" zsh --version
check "oh-my-zsh" [ -d "$HOME/.oh-my-zsh" ]
check "login-shell-path" [ -f "/etc/profile.d/00-restore-env.sh" ]
check "code" which code
}

reportResults() {
if [ ${#FAILED[@]} -ne 0 ]; then
echoStderr -e "\n💥 Failed tests: ${FAILED[@]}"
exit 1
else
echo -e "\n💯 All passed!"
exit 0
fi
}

fixTestProjectFolderPrivs() {
if [ "${USERNAME}" != "root" ]; then
TEST_PROJECT_FOLDER="${1:-$SCRIPT_FOLDER}"
FOLDER_USER="$(stat -c '%U' "${TEST_PROJECT_FOLDER}")"
if [ "${FOLDER_USER}" != "${USERNAME}" ]; then
echoStderr "WARNING: Test project folder is owned by ${FOLDER_USER}. Updating to ${USERNAME}."
sudo chown -R ${USERNAME} "${TEST_PROJECT_FOLDER}"
fi
fi
}

Loading