diff --git a/src/cpp/.devcontainer/Dockerfile b/src/cpp/.devcontainer/Dockerfile index c105b812..8cea3457 100644 --- a/src/cpp/.devcontainer/Dockerfile +++ b/src/cpp/.devcontainer/Dockerfile @@ -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 diff --git a/src/cpp/.devcontainer/reinstall-gcc.sh b/src/cpp/.devcontainer/reinstall-gcc.sh new file mode 100644 index 00000000..bd997441 --- /dev/null +++ b/src/cpp/.devcontainer/reinstall-gcc.sh @@ -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 + "B215C1633BCA0477615F1B35A5B3A004745C015A" + # 1024D/B75C61B8 2003-04-10 Mark Mitchell + "B3C42148A44E6983B3E4CC0793FA9B1AB75C61B8" + # 1024D/902C9419 2004-12-06 Gabriel Dos Reis + "90AA470469D3965A87A5DCB494D03953902C9419" + # 1024D/F71EDF1C 2000-02-13 Joseph Samuel Myers + "80F98B2E0DAB6C8281BDF541A7C8C3B2F71EDF1C" + # 2048R/FC26A641 2005-09-13 Richard Guenther + "7F74F97C103468EE5D750B583AB00996FC26A641" + # 1024D/C3C45C06 2004-04-21 Jakub Jelinek + "33C235A34C46AA3FFB293709A328C3A2C3C45C06" + # 4096R/09B5FA62 2020-05-28 Jakub Jelinek + "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." +# 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}!" diff --git a/src/cpp/README.md b/src/cpp/README.md index c2a7000c..65e19346 100644 --- a/src/cpp/README.md +++ b/src/cpp/README.md @@ -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. diff --git a/src/cpp/devcontainer-template.json b/src/cpp/devcontainer-template.json index 31ca873c..fef1b8d3 100644 --- a/src/cpp/devcontainer-template.json +++ b/src/cpp/devcontainer-template.json @@ -1,6 +1,6 @@ { "id": "cpp", - "version": "3.0.2", + "version": "3.0.3", "name": "C++", "description": "Develop C++ applications on Linux. Includes Debian C++ build tools.", "documentationURL": "https://github.com/devcontainers/templates/tree/main/src/cpp", @@ -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": "8.1.0" } }, "platforms": ["C++"], diff --git a/test/cpp/test-utils.sh b/test/cpp/test-utils.sh new file mode 100755 index 00000000..9fc6b342 --- /dev/null +++ b/test/cpp/test-utils.sh @@ -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 +} + diff --git a/test/cpp/test.sh b/test/cpp/test.sh index b8537448..3ee10ed1 100755 --- a/test/cpp/test.sh +++ b/test/cpp/test.sh @@ -11,7 +11,7 @@ checkCommon # Run template specific tests checkExtension "ms-vscode.cpptools" -checkOSPackages "command-line-tools" build-essential cmake cppcheck valgrind clang lldb llvm gdb +checkOSPackages "command-line-tools" build-essential cppcheck valgrind clang lldb llvm gdb checkOSPackages "tools-for-vcpkg" tar curl zip unzip pkg-config bash-completion ninja-build VCPKG_UNSUPPORTED_ARM64_VERSION_CODENAMES="stretch bionic" if [ "$(dpkg --print-architecture)" = "amd64" ] || [[ ! "${VCPKG_UNSUPPORTED_ARM64_VERSION_CODENAMES}" = *"${VERSION_CODENAME}"* ]]; then @@ -21,6 +21,7 @@ if [ "$(dpkg --print-architecture)" = "amd64" ] || [[ ! "${VCPKG_UNSUPPORTED_ARM VCPKG_FORCE_SYSTEM_BINARIES=1 check "vcpkg-from-bin" vcpkg --version fi check "g++" g++ -g main.cpp -o main.out +check "g++ version" g++ --version rm main.out mkdir -p build cd build